databasic 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- databasic-0.1.0/PKG-INFO +202 -0
- databasic-0.1.0/README.md +190 -0
- databasic-0.1.0/pyproject.toml +41 -0
- databasic-0.1.0/pyproject.toml.orig +38 -0
- databasic-0.1.0/src/databasic/__init__.py +198 -0
- databasic-0.1.0/src/databasic/py.typed +0 -0
databasic-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: databasic
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Database connection helper
|
|
5
|
+
Author: ansipunk
|
|
6
|
+
Author-email: ansipunk <ansipunk@proton.me>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Requires-Dist: psycopg[binary,pool]>=3,<4
|
|
9
|
+
Requires-Dist: sqlalchemy>=2,<2.2
|
|
10
|
+
Requires-Python: >=3.11, <3.16
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# Databasic
|
|
14
|
+
|
|
15
|
+
A small async database interface built on SQLAlchemy Core and psycopg.
|
|
16
|
+
|
|
17
|
+
SQLAlchemy Core handles query construction and compilation. Databasic handles
|
|
18
|
+
connection and transaction lifetimes and executes the resulting queries directly
|
|
19
|
+
through psycopg.
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
import sqlalchemy as sa
|
|
25
|
+
|
|
26
|
+
from databasic import Databasic
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
users = sa.Table(
|
|
30
|
+
"users",
|
|
31
|
+
sa.MetaData(),
|
|
32
|
+
sa.Column("id", sa.Integer, primary_key=True),
|
|
33
|
+
sa.Column("name", sa.Text, nullable=False),
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
db = Databasic("postgresql://user:password@localhost/database")
|
|
37
|
+
|
|
38
|
+
async with db:
|
|
39
|
+
async with db.session() as session:
|
|
40
|
+
rows = await session.fetch_all(
|
|
41
|
+
users.select().where(users.c.name == "Alice")
|
|
42
|
+
)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`Databasic` can also be connected and disconnected explicitly. Both forms have
|
|
46
|
+
the same semantics; explicit lifecycle management is useful when the lifetime is
|
|
47
|
+
controlled by an application or framework.
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
db = Databasic("postgresql://user:password@localhost/database")
|
|
51
|
+
|
|
52
|
+
await db.connect()
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
async with db.session() as session:
|
|
56
|
+
rows = await session.fetch_all(users.select())
|
|
57
|
+
finally:
|
|
58
|
+
await db.disconnect()
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
A session is a transaction. It commits when the context exits successfully and
|
|
62
|
+
rolls back when it exits with an exception.
|
|
63
|
+
|
|
64
|
+
## API
|
|
65
|
+
|
|
66
|
+
### `Databasic`
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
db = Databasic(conninfo, force_rollback=False)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
#### `connect()`
|
|
73
|
+
|
|
74
|
+
Open the connection pool.
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
await db.connect()
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
#### `disconnect()`
|
|
81
|
+
|
|
82
|
+
Close the connection pool.
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
await db.disconnect()
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
#### `session()`
|
|
89
|
+
|
|
90
|
+
Create a transactional session.
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
async with db.session() as session:
|
|
94
|
+
...
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Successful exit commits the transaction. An exception rolls it back.
|
|
98
|
+
|
|
99
|
+
### `Session`
|
|
100
|
+
|
|
101
|
+
#### `execute()`
|
|
102
|
+
|
|
103
|
+
Execute a statement without returning rows.
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
await session.execute(
|
|
107
|
+
users.insert().values(name="Alice")
|
|
108
|
+
)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
#### `execute_many()`
|
|
112
|
+
|
|
113
|
+
Execute the same statement multiple times with different parameters.
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
query = users.insert().values(
|
|
117
|
+
name=sa.bindparam("name"),
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
await session.execute_many(
|
|
121
|
+
query,
|
|
122
|
+
[
|
|
123
|
+
{"name": "Alice"},
|
|
124
|
+
{"name": "Bob"},
|
|
125
|
+
],
|
|
126
|
+
)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
The statement must define its values using explicit bind parameters. Values are
|
|
130
|
+
supplied exclusively through the parameter mappings.
|
|
131
|
+
|
|
132
|
+
#### `fetch_one()`
|
|
133
|
+
|
|
134
|
+
Execute a statement and return one row, or `None` if there is no row.
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
user = await session.fetch_one(
|
|
138
|
+
users.select().where(users.c.id == 1)
|
|
139
|
+
)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
A row is simply a `dict[str, Any]`:
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
{"id": 1, "name": "Alice"}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
There is no result or row wrapper.
|
|
149
|
+
|
|
150
|
+
#### `fetch_all()`
|
|
151
|
+
|
|
152
|
+
Execute a statement and return all rows as a list of dictionaries.
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
users = await session.fetch_all(
|
|
156
|
+
users.select()
|
|
157
|
+
)
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
The result has the shape:
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
[
|
|
164
|
+
{"id": 1, "name": "Alice"},
|
|
165
|
+
{"id": 2, "name": "Bob"},
|
|
166
|
+
]
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
#### `transaction()`
|
|
170
|
+
|
|
171
|
+
Create a nested transaction within the current session.
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
async with db.session() as session:
|
|
175
|
+
await session.execute(
|
|
176
|
+
users.insert().values(name="Alice")
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
async with session.transaction():
|
|
180
|
+
await session.execute(
|
|
181
|
+
users.insert().values(name="Bob")
|
|
182
|
+
)
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Nested transactions use database savepoints. An exception rolls back the nested
|
|
186
|
+
transaction without rolling back earlier work in the session.
|
|
187
|
+
|
|
188
|
+
## Testing
|
|
189
|
+
|
|
190
|
+
`force_rollback=True` runs all sessions within an outer transaction that is
|
|
191
|
+
rolled back when Databasic is disconnected.
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
async with Databasic(
|
|
195
|
+
"postgresql://user:password@localhost/test_database",
|
|
196
|
+
force_rollback=True,
|
|
197
|
+
) as db:
|
|
198
|
+
...
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
This allows integration tests to use normal sessions and transactions without
|
|
202
|
+
leaving persistent database state.
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# Databasic
|
|
2
|
+
|
|
3
|
+
A small async database interface built on SQLAlchemy Core and psycopg.
|
|
4
|
+
|
|
5
|
+
SQLAlchemy Core handles query construction and compilation. Databasic handles
|
|
6
|
+
connection and transaction lifetimes and executes the resulting queries directly
|
|
7
|
+
through psycopg.
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
```python
|
|
12
|
+
import sqlalchemy as sa
|
|
13
|
+
|
|
14
|
+
from databasic import Databasic
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
users = sa.Table(
|
|
18
|
+
"users",
|
|
19
|
+
sa.MetaData(),
|
|
20
|
+
sa.Column("id", sa.Integer, primary_key=True),
|
|
21
|
+
sa.Column("name", sa.Text, nullable=False),
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
db = Databasic("postgresql://user:password@localhost/database")
|
|
25
|
+
|
|
26
|
+
async with db:
|
|
27
|
+
async with db.session() as session:
|
|
28
|
+
rows = await session.fetch_all(
|
|
29
|
+
users.select().where(users.c.name == "Alice")
|
|
30
|
+
)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`Databasic` can also be connected and disconnected explicitly. Both forms have
|
|
34
|
+
the same semantics; explicit lifecycle management is useful when the lifetime is
|
|
35
|
+
controlled by an application or framework.
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
db = Databasic("postgresql://user:password@localhost/database")
|
|
39
|
+
|
|
40
|
+
await db.connect()
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
async with db.session() as session:
|
|
44
|
+
rows = await session.fetch_all(users.select())
|
|
45
|
+
finally:
|
|
46
|
+
await db.disconnect()
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
A session is a transaction. It commits when the context exits successfully and
|
|
50
|
+
rolls back when it exits with an exception.
|
|
51
|
+
|
|
52
|
+
## API
|
|
53
|
+
|
|
54
|
+
### `Databasic`
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
db = Databasic(conninfo, force_rollback=False)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
#### `connect()`
|
|
61
|
+
|
|
62
|
+
Open the connection pool.
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
await db.connect()
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
#### `disconnect()`
|
|
69
|
+
|
|
70
|
+
Close the connection pool.
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
await db.disconnect()
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
#### `session()`
|
|
77
|
+
|
|
78
|
+
Create a transactional session.
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
async with db.session() as session:
|
|
82
|
+
...
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Successful exit commits the transaction. An exception rolls it back.
|
|
86
|
+
|
|
87
|
+
### `Session`
|
|
88
|
+
|
|
89
|
+
#### `execute()`
|
|
90
|
+
|
|
91
|
+
Execute a statement without returning rows.
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
await session.execute(
|
|
95
|
+
users.insert().values(name="Alice")
|
|
96
|
+
)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
#### `execute_many()`
|
|
100
|
+
|
|
101
|
+
Execute the same statement multiple times with different parameters.
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
query = users.insert().values(
|
|
105
|
+
name=sa.bindparam("name"),
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
await session.execute_many(
|
|
109
|
+
query,
|
|
110
|
+
[
|
|
111
|
+
{"name": "Alice"},
|
|
112
|
+
{"name": "Bob"},
|
|
113
|
+
],
|
|
114
|
+
)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
The statement must define its values using explicit bind parameters. Values are
|
|
118
|
+
supplied exclusively through the parameter mappings.
|
|
119
|
+
|
|
120
|
+
#### `fetch_one()`
|
|
121
|
+
|
|
122
|
+
Execute a statement and return one row, or `None` if there is no row.
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
user = await session.fetch_one(
|
|
126
|
+
users.select().where(users.c.id == 1)
|
|
127
|
+
)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
A row is simply a `dict[str, Any]`:
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
{"id": 1, "name": "Alice"}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
There is no result or row wrapper.
|
|
137
|
+
|
|
138
|
+
#### `fetch_all()`
|
|
139
|
+
|
|
140
|
+
Execute a statement and return all rows as a list of dictionaries.
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
users = await session.fetch_all(
|
|
144
|
+
users.select()
|
|
145
|
+
)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
The result has the shape:
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
[
|
|
152
|
+
{"id": 1, "name": "Alice"},
|
|
153
|
+
{"id": 2, "name": "Bob"},
|
|
154
|
+
]
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
#### `transaction()`
|
|
158
|
+
|
|
159
|
+
Create a nested transaction within the current session.
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
async with db.session() as session:
|
|
163
|
+
await session.execute(
|
|
164
|
+
users.insert().values(name="Alice")
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
async with session.transaction():
|
|
168
|
+
await session.execute(
|
|
169
|
+
users.insert().values(name="Bob")
|
|
170
|
+
)
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Nested transactions use database savepoints. An exception rolls back the nested
|
|
174
|
+
transaction without rolling back earlier work in the session.
|
|
175
|
+
|
|
176
|
+
## Testing
|
|
177
|
+
|
|
178
|
+
`force_rollback=True` runs all sessions within an outer transaction that is
|
|
179
|
+
rolled back when Databasic is disconnected.
|
|
180
|
+
|
|
181
|
+
```python
|
|
182
|
+
async with Databasic(
|
|
183
|
+
"postgresql://user:password@localhost/test_database",
|
|
184
|
+
force_rollback=True,
|
|
185
|
+
) as db:
|
|
186
|
+
...
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
This allows integration tests to use normal sessions and transactions without
|
|
190
|
+
leaving persistent database state.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "databasic"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Database connection helper"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
requires-python = ">=3.11,<3.16"
|
|
8
|
+
dependencies = [
|
|
9
|
+
"psycopg[binary,pool]>=3,<4",
|
|
10
|
+
"sqlalchemy>=2,<2.2",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
[[project.authors]]
|
|
14
|
+
name = "ansipunk"
|
|
15
|
+
email = "ansipunk@proton.me"
|
|
16
|
+
|
|
17
|
+
[build-system]
|
|
18
|
+
requires = ["uv_build>=0.12.17,<0.13.0"]
|
|
19
|
+
build-backend = "uv_build"
|
|
20
|
+
|
|
21
|
+
[dependency-groups]
|
|
22
|
+
dev = [
|
|
23
|
+
"pytest>=9,<10",
|
|
24
|
+
"pytest-asyncio>=1,<2",
|
|
25
|
+
"pytest-cov>=7,<8",
|
|
26
|
+
"ruff>=0.16.10",
|
|
27
|
+
"ty>=0.0.84",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[tool.pytest]
|
|
31
|
+
addopts = [
|
|
32
|
+
"--cov",
|
|
33
|
+
"databasic",
|
|
34
|
+
"--cov-report",
|
|
35
|
+
"term-missing",
|
|
36
|
+
"--cov-report",
|
|
37
|
+
"html",
|
|
38
|
+
"--cov-fail-under",
|
|
39
|
+
"100",
|
|
40
|
+
]
|
|
41
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "databasic"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Database connection helper"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
authors = [
|
|
8
|
+
{ name = "ansipunk", email = "ansipunk@proton.me" }
|
|
9
|
+
]
|
|
10
|
+
requires-python = ">=3.11,<3.16"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"psycopg[binary,pool]>=3,<4",
|
|
13
|
+
"sqlalchemy>=2,<2.2",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[build-system]
|
|
17
|
+
requires = ["uv_build>=0.12.17,<0.13.0"]
|
|
18
|
+
build-backend = "uv_build"
|
|
19
|
+
|
|
20
|
+
[dependency-groups]
|
|
21
|
+
dev = [
|
|
22
|
+
"pytest>=9,<10",
|
|
23
|
+
"pytest-asyncio>=1,<2",
|
|
24
|
+
"pytest-cov>=7,<8",
|
|
25
|
+
"ruff>=0.16.10",
|
|
26
|
+
"ty>=0.0.84",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[tool.pytest]
|
|
30
|
+
addopts = [
|
|
31
|
+
"--cov", "databasic",
|
|
32
|
+
"--cov-report", "term-missing",
|
|
33
|
+
"--cov-report", "html",
|
|
34
|
+
"--cov-fail-under", "100",
|
|
35
|
+
]
|
|
36
|
+
testpaths = [
|
|
37
|
+
"tests",
|
|
38
|
+
]
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
from collections.abc import AsyncIterator, Callable, Iterable, Mapping, Sequence
|
|
2
|
+
from contextlib import asynccontextmanager
|
|
3
|
+
from types import TracebackType
|
|
4
|
+
from typing import Any, LiteralString, Self, cast
|
|
5
|
+
|
|
6
|
+
from psycopg import AsyncConnection as BaseAsyncConnection
|
|
7
|
+
from psycopg import AsyncCursor as BaseAsyncCursor
|
|
8
|
+
from psycopg import AsyncTransaction
|
|
9
|
+
from psycopg_pool import AsyncConnectionPool as BaseAsyncConnectionPool
|
|
10
|
+
from sqlalchemy.dialects import postgresql
|
|
11
|
+
from sqlalchemy.sql.elements import ClauseElement
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"DatabaseAlreadyConnectedError",
|
|
15
|
+
"DatabaseNotConnectedError",
|
|
16
|
+
"Databasic",
|
|
17
|
+
"DatabasicError",
|
|
18
|
+
"Row",
|
|
19
|
+
"Session",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
Row = dict[str, Any]
|
|
23
|
+
AsyncConnection = BaseAsyncConnection[Row]
|
|
24
|
+
AsyncConnectionPool = BaseAsyncConnectionPool[AsyncConnection]
|
|
25
|
+
AsyncCursor = BaseAsyncCursor[Row]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Databasic:
|
|
29
|
+
_pool: AsyncConnectionPool
|
|
30
|
+
_conn: AsyncConnection | None = None
|
|
31
|
+
_global_transaction: AsyncTransaction | None = None
|
|
32
|
+
_force_rollback: bool = False
|
|
33
|
+
|
|
34
|
+
def __init__(self, conninfo: str, *, force_rollback: bool = False):
|
|
35
|
+
self._pool = AsyncConnectionPool(
|
|
36
|
+
conninfo,
|
|
37
|
+
open=False,
|
|
38
|
+
kwargs={"row_factory": _dict_row_factory},
|
|
39
|
+
)
|
|
40
|
+
self._force_rollback = force_rollback
|
|
41
|
+
|
|
42
|
+
async def __aenter__(self) -> Self:
|
|
43
|
+
await self.connect()
|
|
44
|
+
return self
|
|
45
|
+
|
|
46
|
+
async def __aexit__(
|
|
47
|
+
self,
|
|
48
|
+
exc_type: type[BaseException] | None,
|
|
49
|
+
exc_value: BaseException | None,
|
|
50
|
+
traceback: TracebackType | None,
|
|
51
|
+
) -> None:
|
|
52
|
+
_ = exc_type, exc_value, traceback
|
|
53
|
+
await self.disconnect()
|
|
54
|
+
|
|
55
|
+
async def connect(self) -> None:
|
|
56
|
+
if not self._pool.closed:
|
|
57
|
+
raise DatabaseAlreadyConnectedError
|
|
58
|
+
|
|
59
|
+
await self._pool.open(wait=True)
|
|
60
|
+
|
|
61
|
+
if self._force_rollback:
|
|
62
|
+
try:
|
|
63
|
+
self._conn = await self._pool.getconn()
|
|
64
|
+
except BaseException: # pragma: no cover
|
|
65
|
+
await self.disconnect()
|
|
66
|
+
raise
|
|
67
|
+
|
|
68
|
+
self._global_transaction = AsyncTransaction(
|
|
69
|
+
self._conn,
|
|
70
|
+
force_rollback=True,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
await self._global_transaction.__aenter__()
|
|
75
|
+
except BaseException: # pragma: no cover
|
|
76
|
+
await self.disconnect()
|
|
77
|
+
raise
|
|
78
|
+
|
|
79
|
+
async def disconnect(self) -> None:
|
|
80
|
+
if self._pool.closed:
|
|
81
|
+
raise DatabaseNotConnectedError
|
|
82
|
+
|
|
83
|
+
if self._conn is not None:
|
|
84
|
+
if self._global_transaction is not None:
|
|
85
|
+
await self._global_transaction.__aexit__(None, None, None)
|
|
86
|
+
self._global_transaction = None
|
|
87
|
+
|
|
88
|
+
await self._pool.putconn(self._conn)
|
|
89
|
+
self._conn = None
|
|
90
|
+
|
|
91
|
+
await self._pool.close()
|
|
92
|
+
|
|
93
|
+
@asynccontextmanager
|
|
94
|
+
async def session(self) -> AsyncIterator["Session"]:
|
|
95
|
+
if self._pool.closed:
|
|
96
|
+
raise DatabaseNotConnectedError
|
|
97
|
+
|
|
98
|
+
if self._conn is not None:
|
|
99
|
+
async with self._conn.transaction():
|
|
100
|
+
yield Session(self._conn)
|
|
101
|
+
else:
|
|
102
|
+
async with self._pool.connection() as conn: # noqa: SIM117
|
|
103
|
+
async with conn.transaction():
|
|
104
|
+
yield Session(conn)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class Session:
|
|
108
|
+
_conn: AsyncConnection
|
|
109
|
+
|
|
110
|
+
def __init__(self, conn: AsyncConnection):
|
|
111
|
+
self._conn = conn
|
|
112
|
+
|
|
113
|
+
async def fetch_one(self, query: ClauseElement) -> Row | None:
|
|
114
|
+
async with self._execute(query) as cursor:
|
|
115
|
+
return await cursor.fetchone()
|
|
116
|
+
|
|
117
|
+
async def fetch_all(self, query: ClauseElement) -> list[Row]:
|
|
118
|
+
async with self._execute(query) as cursor:
|
|
119
|
+
return await cursor.fetchall()
|
|
120
|
+
|
|
121
|
+
async def execute(self, query: ClauseElement) -> int:
|
|
122
|
+
async with self._execute(query) as cursor:
|
|
123
|
+
return cursor.rowcount
|
|
124
|
+
|
|
125
|
+
async def execute_many(
|
|
126
|
+
self,
|
|
127
|
+
query: ClauseElement,
|
|
128
|
+
params: Iterable[Mapping[str, Any]],
|
|
129
|
+
) -> int:
|
|
130
|
+
"""Execute a query multiple times with different parameters.
|
|
131
|
+
|
|
132
|
+
The query must define all values as explicit bind parameters. Values
|
|
133
|
+
must be provided exclusively through `params`, not embedded in the
|
|
134
|
+
SQLAlchemy query itself.
|
|
135
|
+
|
|
136
|
+
Usage would roughly be like:
|
|
137
|
+
|
|
138
|
+
query = sa.insert(table).values(
|
|
139
|
+
val_a=sa.bindparam("val_a"),
|
|
140
|
+
val_b=sa.bindparam("val_b"),
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
await session.execute_many(
|
|
144
|
+
query,
|
|
145
|
+
[
|
|
146
|
+
{"val_a": 1, "val_b": 2},
|
|
147
|
+
{"val_a": 3, "val_b": 4},
|
|
148
|
+
],
|
|
149
|
+
)
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
compiled_query, _ = _compile_query(query)
|
|
153
|
+
|
|
154
|
+
async with self._conn.cursor() as cursor:
|
|
155
|
+
await cursor.executemany(compiled_query, params)
|
|
156
|
+
return cursor.rowcount
|
|
157
|
+
|
|
158
|
+
@asynccontextmanager
|
|
159
|
+
async def _execute(self, statement: ClauseElement) -> AsyncIterator[AsyncCursor]:
|
|
160
|
+
query, params = _compile_query(statement)
|
|
161
|
+
|
|
162
|
+
async with self._conn.cursor() as cursor:
|
|
163
|
+
await cursor.execute(query, params)
|
|
164
|
+
yield cursor
|
|
165
|
+
|
|
166
|
+
@asynccontextmanager
|
|
167
|
+
async def transaction(self) -> AsyncIterator[None]:
|
|
168
|
+
async with self._conn.transaction():
|
|
169
|
+
yield
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class DatabasicError(Exception):
|
|
173
|
+
pass
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class DatabaseAlreadyConnectedError(DatabasicError):
|
|
177
|
+
pass
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class DatabaseNotConnectedError(DatabasicError):
|
|
181
|
+
pass
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _dict_row_factory(cursor: AsyncCursor) -> Callable[[Sequence[Any]], Row]:
|
|
185
|
+
fields = [c.name for c in cursor.description or ()]
|
|
186
|
+
|
|
187
|
+
def make_row(values: Sequence[Any]) -> Row:
|
|
188
|
+
return dict(zip(fields, values))
|
|
189
|
+
|
|
190
|
+
return make_row
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _compile_query(statement: ClauseElement) -> tuple[LiteralString, dict[str, Any]]:
|
|
194
|
+
compiled = statement.compile(
|
|
195
|
+
dialect=postgresql.dialect(paramstyle="pyformat"),
|
|
196
|
+
compile_kwargs={"render_postcompile": True},
|
|
197
|
+
)
|
|
198
|
+
return cast(LiteralString, compiled.string), compiled.params
|
|
File without changes
|