Skip to content

Commit c0f1307

Browse files
authored
Merge pull request #58 from qaspen-python/feature/better_connection_managment
Better connection management
2 parents 5481d73 + eb3bcfc commit c0f1307

File tree

16 files changed

+1332
-729
lines changed

16 files changed

+1332
-729
lines changed

docs/components/connection.md

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,37 @@ title: Connection
55
`Connection` object represents single connection to the `PostgreSQL`. You must work with database within it.
66
`Connection` get be made with `ConnectionPool().connection()` method.
77

8+
## Usage
9+
::: tabs
10+
11+
@tab default
812
```python
913
from psqlpy import ConnectionPool
1014

11-
1215
db_pool: Final = ConnectionPool(
1316
dsn="postgres://postgres:postgres@localhost:5432/postgres",
1417
)
1518

16-
1719
async def main() -> None:
18-
...
1920
connection = await db_pool.connection()
2021
```
2122

23+
@tab async context manager
24+
```python
25+
from psqlpy import ConnectionPool
26+
27+
db_pool: Final = ConnectionPool(
28+
dsn="postgres://postgres:postgres@localhost:5432/postgres",
29+
)
30+
31+
async def main() -> None:
32+
async with db_pool.acquire() as connection:
33+
# connection is valid here
34+
...
35+
# connection is invalid here
36+
```
37+
:::
38+
2239
## Connection methods
2340

2441
### Execute
@@ -158,3 +175,19 @@ async def main() -> None:
158175
deferrable=True,
159176
)
160177
```
178+
179+
### Back To Pool
180+
Returns connection to the pool.
181+
It's crucial to commit all transactions and close all cursor which are made from the connection.
182+
Otherwise, this method won't do anything useful.
183+
184+
::: tip
185+
There is no need in this method if you use async context manager.
186+
:::
187+
188+
```python
189+
async def main() -> None:
190+
...
191+
connection = await db_pool.connection()
192+
connection.back_to_pool()
193+
```

docs/components/connection_pool.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,29 @@ db_pool: Final = connect(
141141
```
142142
`connect` function has the same parameters as `ConnectionPool`.
143143

144+
### Resize
145+
Resize connection pool capacity.
146+
147+
This change the max_size of the pool dropping excess objects and/or making space for new ones.
148+
149+
#### Parameters:
150+
- `new_max_size`: new size of the pool.
151+
152+
```python
153+
async def main() -> None:
154+
...
155+
db_pool.resize(15)
156+
```
157+
158+
### Status
159+
Retrieve status of the connection pool.
160+
161+
It has 4 parameters:
162+
- `max_size` - maximum possible size of the connection pool.
163+
- `size` - current size of the connection pool.
164+
- `available` - available connection in the connection pool.
165+
- `waiting` - waiting requests to retrieve connection from connection pool.
166+
144167
### Execute
145168

146169
#### Parameters:
@@ -190,6 +213,18 @@ async def main() -> None:
190213
dict_results: list[dict[str, Any]] = results.result()
191214
```
192215

216+
### Acquire
217+
218+
Get single connection for async context manager.
219+
Must be used only in async context manager.
220+
221+
```python
222+
async def main() -> None:
223+
...
224+
async with db_pool.acquire() as connection:
225+
...
226+
```
227+
193228
### Connection
194229

195230
To get single connection from the `ConnectionPool` there is method named `connection()`.

docs/components/exceptions.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,18 +28,21 @@ stateDiagram-v2
2828
}
2929
state BaseConnectionError {
3030
[*] --> ConnectionExecuteError
31+
[*] --> ConnectionClosedError
3132
}
3233
state BaseTransactionError {
3334
[*] --> TransactionBeginError
3435
[*] --> TransactionCommitError
3536
[*] --> TransactionRollbackError
3637
[*] --> TransactionSavepointError
3738
[*] --> TransactionExecuteError
39+
[*] --> TransactionClosedError
3840
}
3941
state BaseCursorError {
4042
[*] --> CursorStartError
4143
[*] --> CursorCloseError
4244
[*] --> CursorFetchError
45+
[*] --> CursorClosedError
4346
}
4447
state RustException {
4548
[*] --> DriverError
@@ -86,6 +89,9 @@ Base error for Connection errors.
8689
#### ConnectionExecuteError
8790
Error in connection execution.
8891

92+
#### ConnectionClosedError
93+
Error if underlying connection is closed.
94+
8995
### BaseTransactionError
9096
Base error for all transaction errors.
9197

@@ -104,6 +110,9 @@ Error in transaction savepoint.
104110
#### TransactionExecuteError
105111
Error in transaction execution.
106112

113+
#### TransactionClosedError
114+
Error if underlying connection is closed.
115+
107116
### BaseCursorError
108117
Base error for Cursor errors.
109118

@@ -115,3 +124,6 @@ Error in cursor close.
115124

116125
#### CursorFetchError
117126
Error in cursor fetch (any fetch).
127+
128+
#### CursorClosedError
129+
Error if underlying connection is closed.

python/psqlpy/_internal/__init__.pyi

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -816,6 +816,13 @@ class Connection:
816816
It can be created only from connection pool.
817817
"""
818818

819+
async def __aenter__(self: Self) -> Self: ...
820+
async def __aexit__(
821+
self: Self,
822+
exception_type: type[BaseException] | None,
823+
exception: BaseException | None,
824+
traceback: types.TracebackType | None,
825+
) -> None: ...
819826
async def execute(
820827
self: Self,
821828
querystring: str,
@@ -1040,6 +1047,18 @@ class Connection:
10401047
... # do something with this result.
10411048
```
10421049
"""
1050+
def back_to_pool(self: Self) -> None:
1051+
"""Return connection back to the pool.
1052+
1053+
It necessary to commit all transactions and close all cursor
1054+
made by this connection. Otherwise, it won't have any practical usage.
1055+
"""
1056+
1057+
class ConnectionPoolStatus:
1058+
max_size: int
1059+
size: int
1060+
available: int
1061+
waiting: int
10431062

10441063
class ConnectionPool:
10451064
"""Connection pool for executing queries.
@@ -1142,6 +1161,21 @@ class ConnectionPool:
11421161
- `max_db_pool_size`: maximum size of the connection pool.
11431162
- `conn_recycling_method`: how a connection is recycled.
11441163
"""
1164+
def status(self: Self) -> ConnectionPoolStatus:
1165+
"""Return information about connection pool.
1166+
1167+
### Returns
1168+
`ConnectionPoolStatus`
1169+
"""
1170+
def resize(self: Self, new_max_size: int) -> None:
1171+
"""Resize the connection pool.
1172+
1173+
This change the max_size of the pool dropping
1174+
excess objects and/or making space for new ones.
1175+
1176+
### Parameters:
1177+
- `new_max_size`: new size for the connection pool.
1178+
"""
11451179
async def execute(
11461180
self: Self,
11471181
querystring: str,
@@ -1202,6 +1236,24 @@ class ConnectionPool:
12021236
12031237
It acquires new connection from the database pool.
12041238
"""
1239+
def acquire(self: Self) -> Connection:
1240+
"""Create new connection for async context manager.
1241+
1242+
Must be used only in async context manager.
1243+
1244+
### Example:
1245+
```python
1246+
import asyncio
1247+
1248+
from psqlpy import PSQLPool, QueryResult
1249+
1250+
1251+
async def main() -> None:
1252+
db_pool = PSQLPool()
1253+
async with db_pool.acquire() as connection:
1254+
res = await connection.execute(...)
1255+
```
1256+
"""
12051257
def close(self: Self) -> None:
12061258
"""Close the connection pool."""
12071259

python/psqlpy/_internal/exceptions.pyi

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ class BaseConnectionError(RustPSQLDriverPyBaseError):
1919
class ConnectionExecuteError(BaseConnectionError):
2020
"""Error in connection execution."""
2121

22+
class ConnectionClosedError(BaseConnectionError):
23+
"""Error if underlying connection is already closed."""
24+
2225
class BaseTransactionError(RustPSQLDriverPyBaseError):
2326
"""Base error for all transaction errors."""
2427

@@ -37,6 +40,9 @@ class TransactionSavepointError(BaseTransactionError):
3740
class TransactionExecuteError(BaseTransactionError):
3841
"""Error in transaction execution."""
3942

43+
class TransactionClosedError(BaseTransactionError):
44+
"""Error if underlying connection is already closed."""
45+
4046
class BaseCursorError(RustPSQLDriverPyBaseError):
4147
"""Base error for Cursor errors."""
4248

@@ -49,6 +55,9 @@ class CursorCloseError(BaseCursorError):
4955
class CursorFetchError(BaseCursorError):
5056
"""Error in cursor fetch (any fetch)."""
5157

58+
class CursorClosedError(BaseCursorError):
59+
"""Error if underlying connection is already closed."""
60+
5261
class UUIDValueConvertError(RustPSQLDriverPyBaseError):
5362
"""Error if it's impossible to convert py string UUID into rust UUID."""
5463

python/psqlpy/exceptions.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
BaseConnectionPoolError,
44
BaseCursorError,
55
BaseTransactionError,
6+
ConnectionClosedError,
67
ConnectionExecuteError,
78
ConnectionPoolBuildError,
89
ConnectionPoolConfigurationError,
910
ConnectionPoolExecuteError,
11+
CursorClosedError,
1012
CursorCloseError,
1113
CursorFetchError,
1214
CursorStartError,
@@ -15,6 +17,7 @@
1517
RustPSQLDriverPyBaseError,
1618
RustToPyValueMappingError,
1719
TransactionBeginError,
20+
TransactionClosedError,
1821
TransactionCommitError,
1922
TransactionExecuteError,
2023
TransactionRollbackError,
@@ -29,16 +32,19 @@
2932
"ConnectionPoolExecuteError",
3033
"BaseConnectionError",
3134
"ConnectionExecuteError",
35+
"ConnectionClosedError",
3236
"BaseTransactionError",
3337
"TransactionBeginError",
3438
"TransactionCommitError",
3539
"TransactionRollbackError",
3640
"TransactionSavepointError",
3741
"TransactionExecuteError",
42+
"TransactionClosedError",
3843
"BaseCursorError",
3944
"CursorStartError",
4045
"CursorCloseError",
4146
"CursorFetchError",
47+
"CursorClosedError",
4248
"RustPSQLDriverPyBaseError",
4349
"RustToPyValueMappingError",
4450
"PyToRustValueMappingError",

python/tests/test_connection.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@
66
from tests.helpers import count_rows_in_test_table
77

88
from psqlpy import ConnectionPool, Cursor, QueryResult, Transaction
9-
from psqlpy.exceptions import ConnectionExecuteError, TransactionExecuteError
9+
from psqlpy.exceptions import (
10+
ConnectionClosedError,
11+
ConnectionExecuteError,
12+
TransactionExecuteError,
13+
)
1014

1115
pytestmark = pytest.mark.anyio
1216

@@ -147,3 +151,32 @@ async def test_connection_cursor(
147151
all_results.extend(cur_res.result())
148152

149153
assert len(all_results) == number_database_records
154+
155+
156+
async def test_connection_async_context_manager(
157+
psql_pool: ConnectionPool,
158+
table_name: str,
159+
number_database_records: int,
160+
) -> None:
161+
"""Test connection as a async context manager."""
162+
async with psql_pool.acquire() as connection:
163+
conn_result = await connection.execute(
164+
querystring=f"SELECT * FROM {table_name}",
165+
)
166+
assert not psql_pool.status().available
167+
168+
assert psql_pool.status().available == 1
169+
170+
assert isinstance(conn_result, QueryResult)
171+
assert len(conn_result.result()) == number_database_records
172+
173+
174+
async def test_closed_connection_error(
175+
psql_pool: ConnectionPool,
176+
) -> None:
177+
"""Test exception when connection is closed."""
178+
connection = await psql_pool.connection()
179+
connection.back_to_pool()
180+
181+
with pytest.raises(expected_exception=ConnectionClosedError):
182+
await connection.execute("SELECT 1")

python/tests/test_cursor.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,3 +170,37 @@ async def test_cursor_as_async_manager(
170170
all_results.append(result)
171171

172172
assert len(all_results) == expected_num_results
173+
174+
175+
async def test_cursor_send_underlying_connection_to_pool(
176+
psql_pool: ConnectionPool,
177+
table_name: str,
178+
) -> None:
179+
"""Test send underlying connection to the pool."""
180+
async with psql_pool.acquire() as connection:
181+
async with connection.transaction() as transaction:
182+
async with transaction.cursor(
183+
querystring=f"SELECT * FROM {table_name}",
184+
) as cursor:
185+
await cursor.fetch(10)
186+
assert not psql_pool.status().available
187+
assert not psql_pool.status().available
188+
assert not psql_pool.status().available
189+
assert psql_pool.status().available == 1
190+
191+
192+
async def test_cursor_send_underlying_connection_to_pool_manually(
193+
psql_pool: ConnectionPool,
194+
table_name: str,
195+
) -> None:
196+
"""Test send underlying connection to the pool."""
197+
async with psql_pool.acquire() as connection:
198+
async with connection.transaction() as transaction:
199+
cursor = transaction.cursor(querystring=f"SELECT * FROM {table_name}")
200+
await cursor.start()
201+
await cursor.fetch(10)
202+
assert not psql_pool.status().available
203+
await cursor.close()
204+
assert not psql_pool.status().available
205+
assert not psql_pool.status().available
206+
assert psql_pool.status().available == 1

0 commit comments

Comments
 (0)