ddsql 0.0.4__tar.gz → 0.2.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.
- {ddsql-0.0.4 → ddsql-0.2.0}/PKG-INFO +107 -30
- {ddsql-0.0.4 → ddsql-0.2.0}/README.md +99 -20
- ddsql-0.2.0/ddsql/__init__.py +3 -0
- ddsql-0.2.0/ddsql/adapter.py +81 -0
- ddsql-0.2.0/ddsql/connections.py +70 -0
- {ddsql-0.0.4 → ddsql-0.2.0}/ddsql/query.py +17 -21
- {ddsql-0.0.4 → ddsql-0.2.0}/ddsql/serializers/base.py +3 -3
- {ddsql-0.0.4 → ddsql-0.2.0}/ddsql/serializers/clickhouse.py +1 -2
- {ddsql-0.0.4 → ddsql-0.2.0}/ddsql/serializers/postgres.py +1 -2
- {ddsql-0.0.4 → ddsql-0.2.0}/ddsql/sqlbase.py +8 -13
- ddsql-0.2.0/pyproject.toml +53 -0
- ddsql-0.2.0/pyproject.toml.orig +49 -0
- ddsql-0.0.4/ddsql/__init__.py +0 -3
- ddsql-0.0.4/ddsql/adapter.py +0 -56
- ddsql-0.0.4/pyproject.toml +0 -33
- {ddsql-0.0.4 → ddsql-0.2.0}/LICENSE +0 -0
- {ddsql-0.0.4 → ddsql-0.2.0}/ddsql/py.typed +0 -0
- {ddsql-0.0.4 → ddsql-0.2.0}/ddsql/serializers/__init__.py +0 -0
|
@@ -1,22 +1,20 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ddsql
|
|
3
|
-
Version: 0.0
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: SQL Query Library
|
|
5
|
-
License: MIT
|
|
6
|
-
License-File: LICENSE
|
|
7
5
|
Keywords: python,ddsql
|
|
8
6
|
Author: davyddd
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
11
9
|
Classifier: Programming Language :: Python :: 3
|
|
12
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
14
10
|
Classifier: Programming Language :: Python :: 3.11
|
|
15
11
|
Classifier: Programming Language :: Python :: 3.12
|
|
16
12
|
Classifier: Programming Language :: Python :: 3.13
|
|
17
13
|
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
-
|
|
19
|
-
Requires-Dist:
|
|
14
|
+
Classifier: Typing :: Typed
|
|
15
|
+
Requires-Dist: ddutils>=0.2.0,<0.3.0
|
|
16
|
+
Requires-Dist: jinja2>=3.0.0,<4.0.0
|
|
17
|
+
Requires-Python: >=3.11, <3.15
|
|
20
18
|
Project-URL: Repository, https://github.com/davyddd/ddsql
|
|
21
19
|
Description-Content-Type: text/markdown
|
|
22
20
|
|
|
@@ -38,6 +36,21 @@ Install the library using pip:
|
|
|
38
36
|
pip install ddsql
|
|
39
37
|
```
|
|
40
38
|
|
|
39
|
+
## Development
|
|
40
|
+
|
|
41
|
+
The project runs entirely in Docker. Requires Docker and [Fabric](https://www.fabfile.org/) on the host:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
fab build # build the dev image
|
|
45
|
+
fab tests # run pytest
|
|
46
|
+
fab linters # run ruff (with --fix), ty and complexipy
|
|
47
|
+
fab shell # IPython inside the container
|
|
48
|
+
fab bash # bash inside the container
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
This project was generated from [dd-lib-stub](https://github.com/davyddd/dd-lib-stub);
|
|
52
|
+
run `copier update` to pull in template updates.
|
|
53
|
+
|
|
41
54
|
## Serializer
|
|
42
55
|
|
|
43
56
|
The serializer converts Python types to their SQL representations.
|
|
@@ -120,14 +133,67 @@ class CustomSerializer(BaseSerializer):
|
|
|
120
133
|
}
|
|
121
134
|
```
|
|
122
135
|
|
|
136
|
+
## Connections
|
|
137
|
+
|
|
138
|
+
`ddsql.connections` sits between a `ScopedRegistry` (from [ddutils](https://github.com/davyddd/ddutils)) and
|
|
139
|
+
the code that runs queries. A registry holds one connection per scope: a pooled client per process, a session
|
|
140
|
+
per task. The connections module adds two things on top:
|
|
141
|
+
|
|
142
|
+
- `ConnectionManagerFactory(registries, default, connection_manager_class=ConnectionManager)` resolves a
|
|
143
|
+
connection alias to its registry, like Django's `using`. Calling it opens a connection manager on the
|
|
144
|
+
chosen registry; `registry(using)` returns the registry itself.
|
|
145
|
+
- `ConnectionManager(registry)` is the default connection manager: an async context manager that hands out
|
|
146
|
+
the connection of the registry's current scope and releases nothing on exit. Enough for clients that are
|
|
147
|
+
pools themselves, such as ClickHouse.
|
|
148
|
+
|
|
149
|
+
Databases with transactions pass their own `connection_manager_class`: a unit of work that begins, commits
|
|
150
|
+
or rolls back and closes the session on exit. It depends on the driver, so it lives in the project.
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
import os
|
|
154
|
+
from enum import Enum
|
|
155
|
+
|
|
156
|
+
from ddutils.scoped_registry import ScopedRegistry
|
|
157
|
+
|
|
158
|
+
from ddsql.connections import ConnectionManagerFactory
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class ClickhouseDB(str, Enum):
|
|
162
|
+
PRIMARY = 'primary'
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
client_registries = {
|
|
166
|
+
db: ScopedRegistry(create_func=make_client_for(db), scope_func=os.getpid, destructor_method_name='close')
|
|
167
|
+
for db in ClickhouseDB
|
|
168
|
+
}
|
|
169
|
+
clickhouse = ConnectionManagerFactory(client_registries, default=ClickhouseDB.PRIMARY)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
async with clickhouse() as client:
|
|
173
|
+
result = await client.query(sql)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
# a transactional database: `Atomic` is your unit of work over `registry.scope()`
|
|
177
|
+
atomic = ConnectionManagerFactory(session_registries, default=PostgresDB.PRIMARY, connection_manager_class=Atomic)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
async with atomic(using=PostgresDB.REPLICA) as session:
|
|
181
|
+
...
|
|
182
|
+
```
|
|
183
|
+
|
|
123
184
|
## Adapter
|
|
124
185
|
|
|
125
|
-
`Adapter` encapsulates database interactions. To create an adapter, inherit from the `Adapter` base class
|
|
186
|
+
`Adapter` encapsulates database interactions. To create an adapter, inherit from the `Adapter` base class
|
|
126
187
|
and define two required elements:
|
|
127
188
|
- **serializer** – an instance of a serializer for converting Python types to SQL representations;
|
|
128
189
|
- **_execute** method – the database-specific query execution logic.
|
|
129
190
|
|
|
130
191
|
```python
|
|
192
|
+
from collections.abc import Sequence
|
|
193
|
+
from typing import Any
|
|
194
|
+
|
|
195
|
+
from sqlalchemy import text
|
|
196
|
+
|
|
131
197
|
from ddsql.adapter import Adapter
|
|
132
198
|
from ddsql.serializers import PostgresSerializer
|
|
133
199
|
|
|
@@ -135,13 +201,26 @@ from ddsql.serializers import PostgresSerializer
|
|
|
135
201
|
class PostgresAdapter(Adapter):
|
|
136
202
|
serializer = PostgresSerializer()
|
|
137
203
|
|
|
138
|
-
async def _execute(self) -> Sequence[
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
result = await
|
|
204
|
+
async def _execute(self) -> Sequence[dict[str, Any]]:
|
|
205
|
+
async with atomic(using=self.db) as session:
|
|
206
|
+
query = await self.get_query() # the rendered SQL query
|
|
207
|
+
result = await session.execute(text(query))
|
|
142
208
|
return [dict(zip(result.keys(), row)) for row in result.fetchall()]
|
|
143
209
|
```
|
|
144
210
|
|
|
211
|
+
Intermediate adapter classes without `_execute` are abstract and need no `serializer`; only concrete
|
|
212
|
+
adapters are validated.
|
|
213
|
+
|
|
214
|
+
### Choosing the database
|
|
215
|
+
|
|
216
|
+
`Adapter.using(db)` picks the database right before executing, like Django's `using`. `_execute` reads
|
|
217
|
+
`self.db` (`None` means the default) and passes it to whatever opens the connection, as in the example above:
|
|
218
|
+
|
|
219
|
+
```python
|
|
220
|
+
await SQL(query).postgres.execute() # default connection
|
|
221
|
+
await SQL(query).postgres.using(PostgresDB.REPLICA).execute()
|
|
222
|
+
```
|
|
223
|
+
|
|
145
224
|
## SQLBase
|
|
146
225
|
|
|
147
226
|
`SQLBase` is configured once per project and defines which adapters are available for query execution.
|
|
@@ -177,7 +256,7 @@ which wraps raw database rows into the specified model (called internally by `Ad
|
|
|
177
256
|
|
|
178
257
|
Required parameters:
|
|
179
258
|
- **model** – a declarative class (e.g., dataclass) describing the output result structure;
|
|
180
|
-
- **text** or **path** – the SQL template source.
|
|
259
|
+
- **text** or **path** – the SQL template source (inline string or path to a file).
|
|
181
260
|
|
|
182
261
|
### Inline Template (text)
|
|
183
262
|
|
|
@@ -193,25 +272,24 @@ query = Query(
|
|
|
193
272
|
|
|
194
273
|
### File Template (path)
|
|
195
274
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
```bash
|
|
199
|
-
export SQL_TEMPLATES_DIR=/app/src/templates/sql/
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
Then use a relative path:
|
|
275
|
+
Pass the path to the SQL file as `Path`. The file is checked at construction time, so a wrong path fails
|
|
276
|
+
on import rather than on the first query:
|
|
203
277
|
|
|
204
278
|
```python
|
|
279
|
+
from pathlib import Path
|
|
280
|
+
|
|
205
281
|
from ddsql.query import Query
|
|
206
282
|
|
|
283
|
+
SQL_TEMPLATES_DIR = Path(__file__).parent / 'templates' / 'sql'
|
|
207
284
|
|
|
208
|
-
# Loads template from /app/src/templates/sql/users/get_by_id.sql
|
|
209
285
|
query = Query(
|
|
210
|
-
model=User,
|
|
211
|
-
path='users/get_by_id.sql'
|
|
286
|
+
model=User,
|
|
287
|
+
path=SQL_TEMPLATES_DIR / 'users' / 'get_by_id.sql',
|
|
212
288
|
)
|
|
213
289
|
```
|
|
214
290
|
|
|
291
|
+
`{% include %}` and `{% import %}` inside the template resolve relative to the template's directory.
|
|
292
|
+
|
|
215
293
|
### Result
|
|
216
294
|
|
|
217
295
|
The result of query execution is a `Result` object that wraps the data into the specified model:
|
|
@@ -224,12 +302,11 @@ The result of query execution is a `Result` object that wraps the data into the
|
|
|
224
302
|
```python
|
|
225
303
|
from dataclasses import dataclass
|
|
226
304
|
from datetime import datetime
|
|
227
|
-
from typing import Optional
|
|
228
305
|
|
|
229
|
-
from ddsql.query import Query
|
|
230
|
-
from ddsql.sqlbase import SQLBase
|
|
231
306
|
from ddsql.adapter import Adapter, AdapterDescriptor
|
|
307
|
+
from ddsql.query import Query
|
|
232
308
|
from ddsql.serializers import PostgresSerializer
|
|
309
|
+
from ddsql.sqlbase import SQLBase
|
|
233
310
|
|
|
234
311
|
|
|
235
312
|
class PostgresAdapter(Adapter):
|
|
@@ -247,7 +324,7 @@ class SQL(SQLBase):
|
|
|
247
324
|
class User:
|
|
248
325
|
user_id: int
|
|
249
326
|
name: str
|
|
250
|
-
email:
|
|
327
|
+
email: str | None
|
|
251
328
|
created_at: datetime
|
|
252
329
|
is_deleted: bool
|
|
253
330
|
|
|
@@ -273,4 +350,4 @@ async def get_users():
|
|
|
273
350
|
.execute()
|
|
274
351
|
)
|
|
275
352
|
return result.get_list()
|
|
276
|
-
```
|
|
353
|
+
```
|
|
@@ -16,6 +16,21 @@ Install the library using pip:
|
|
|
16
16
|
pip install ddsql
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
+
## Development
|
|
20
|
+
|
|
21
|
+
The project runs entirely in Docker. Requires Docker and [Fabric](https://www.fabfile.org/) on the host:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
fab build # build the dev image
|
|
25
|
+
fab tests # run pytest
|
|
26
|
+
fab linters # run ruff (with --fix), ty and complexipy
|
|
27
|
+
fab shell # IPython inside the container
|
|
28
|
+
fab bash # bash inside the container
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
This project was generated from [dd-lib-stub](https://github.com/davyddd/dd-lib-stub);
|
|
32
|
+
run `copier update` to pull in template updates.
|
|
33
|
+
|
|
19
34
|
## Serializer
|
|
20
35
|
|
|
21
36
|
The serializer converts Python types to their SQL representations.
|
|
@@ -98,14 +113,67 @@ class CustomSerializer(BaseSerializer):
|
|
|
98
113
|
}
|
|
99
114
|
```
|
|
100
115
|
|
|
116
|
+
## Connections
|
|
117
|
+
|
|
118
|
+
`ddsql.connections` sits between a `ScopedRegistry` (from [ddutils](https://github.com/davyddd/ddutils)) and
|
|
119
|
+
the code that runs queries. A registry holds one connection per scope: a pooled client per process, a session
|
|
120
|
+
per task. The connections module adds two things on top:
|
|
121
|
+
|
|
122
|
+
- `ConnectionManagerFactory(registries, default, connection_manager_class=ConnectionManager)` resolves a
|
|
123
|
+
connection alias to its registry, like Django's `using`. Calling it opens a connection manager on the
|
|
124
|
+
chosen registry; `registry(using)` returns the registry itself.
|
|
125
|
+
- `ConnectionManager(registry)` is the default connection manager: an async context manager that hands out
|
|
126
|
+
the connection of the registry's current scope and releases nothing on exit. Enough for clients that are
|
|
127
|
+
pools themselves, such as ClickHouse.
|
|
128
|
+
|
|
129
|
+
Databases with transactions pass their own `connection_manager_class`: a unit of work that begins, commits
|
|
130
|
+
or rolls back and closes the session on exit. It depends on the driver, so it lives in the project.
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
import os
|
|
134
|
+
from enum import Enum
|
|
135
|
+
|
|
136
|
+
from ddutils.scoped_registry import ScopedRegistry
|
|
137
|
+
|
|
138
|
+
from ddsql.connections import ConnectionManagerFactory
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class ClickhouseDB(str, Enum):
|
|
142
|
+
PRIMARY = 'primary'
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
client_registries = {
|
|
146
|
+
db: ScopedRegistry(create_func=make_client_for(db), scope_func=os.getpid, destructor_method_name='close')
|
|
147
|
+
for db in ClickhouseDB
|
|
148
|
+
}
|
|
149
|
+
clickhouse = ConnectionManagerFactory(client_registries, default=ClickhouseDB.PRIMARY)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
async with clickhouse() as client:
|
|
153
|
+
result = await client.query(sql)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# a transactional database: `Atomic` is your unit of work over `registry.scope()`
|
|
157
|
+
atomic = ConnectionManagerFactory(session_registries, default=PostgresDB.PRIMARY, connection_manager_class=Atomic)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
async with atomic(using=PostgresDB.REPLICA) as session:
|
|
161
|
+
...
|
|
162
|
+
```
|
|
163
|
+
|
|
101
164
|
## Adapter
|
|
102
165
|
|
|
103
|
-
`Adapter` encapsulates database interactions. To create an adapter, inherit from the `Adapter` base class
|
|
166
|
+
`Adapter` encapsulates database interactions. To create an adapter, inherit from the `Adapter` base class
|
|
104
167
|
and define two required elements:
|
|
105
168
|
- **serializer** – an instance of a serializer for converting Python types to SQL representations;
|
|
106
169
|
- **_execute** method – the database-specific query execution logic.
|
|
107
170
|
|
|
108
171
|
```python
|
|
172
|
+
from collections.abc import Sequence
|
|
173
|
+
from typing import Any
|
|
174
|
+
|
|
175
|
+
from sqlalchemy import text
|
|
176
|
+
|
|
109
177
|
from ddsql.adapter import Adapter
|
|
110
178
|
from ddsql.serializers import PostgresSerializer
|
|
111
179
|
|
|
@@ -113,13 +181,26 @@ from ddsql.serializers import PostgresSerializer
|
|
|
113
181
|
class PostgresAdapter(Adapter):
|
|
114
182
|
serializer = PostgresSerializer()
|
|
115
183
|
|
|
116
|
-
async def _execute(self) -> Sequence[
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
result = await
|
|
184
|
+
async def _execute(self) -> Sequence[dict[str, Any]]:
|
|
185
|
+
async with atomic(using=self.db) as session:
|
|
186
|
+
query = await self.get_query() # the rendered SQL query
|
|
187
|
+
result = await session.execute(text(query))
|
|
120
188
|
return [dict(zip(result.keys(), row)) for row in result.fetchall()]
|
|
121
189
|
```
|
|
122
190
|
|
|
191
|
+
Intermediate adapter classes without `_execute` are abstract and need no `serializer`; only concrete
|
|
192
|
+
adapters are validated.
|
|
193
|
+
|
|
194
|
+
### Choosing the database
|
|
195
|
+
|
|
196
|
+
`Adapter.using(db)` picks the database right before executing, like Django's `using`. `_execute` reads
|
|
197
|
+
`self.db` (`None` means the default) and passes it to whatever opens the connection, as in the example above:
|
|
198
|
+
|
|
199
|
+
```python
|
|
200
|
+
await SQL(query).postgres.execute() # default connection
|
|
201
|
+
await SQL(query).postgres.using(PostgresDB.REPLICA).execute()
|
|
202
|
+
```
|
|
203
|
+
|
|
123
204
|
## SQLBase
|
|
124
205
|
|
|
125
206
|
`SQLBase` is configured once per project and defines which adapters are available for query execution.
|
|
@@ -155,7 +236,7 @@ which wraps raw database rows into the specified model (called internally by `Ad
|
|
|
155
236
|
|
|
156
237
|
Required parameters:
|
|
157
238
|
- **model** – a declarative class (e.g., dataclass) describing the output result structure;
|
|
158
|
-
- **text** or **path** – the SQL template source.
|
|
239
|
+
- **text** or **path** – the SQL template source (inline string or path to a file).
|
|
159
240
|
|
|
160
241
|
### Inline Template (text)
|
|
161
242
|
|
|
@@ -171,25 +252,24 @@ query = Query(
|
|
|
171
252
|
|
|
172
253
|
### File Template (path)
|
|
173
254
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
```bash
|
|
177
|
-
export SQL_TEMPLATES_DIR=/app/src/templates/sql/
|
|
178
|
-
```
|
|
179
|
-
|
|
180
|
-
Then use a relative path:
|
|
255
|
+
Pass the path to the SQL file as `Path`. The file is checked at construction time, so a wrong path fails
|
|
256
|
+
on import rather than on the first query:
|
|
181
257
|
|
|
182
258
|
```python
|
|
259
|
+
from pathlib import Path
|
|
260
|
+
|
|
183
261
|
from ddsql.query import Query
|
|
184
262
|
|
|
263
|
+
SQL_TEMPLATES_DIR = Path(__file__).parent / 'templates' / 'sql'
|
|
185
264
|
|
|
186
|
-
# Loads template from /app/src/templates/sql/users/get_by_id.sql
|
|
187
265
|
query = Query(
|
|
188
|
-
model=User,
|
|
189
|
-
path='users/get_by_id.sql'
|
|
266
|
+
model=User,
|
|
267
|
+
path=SQL_TEMPLATES_DIR / 'users' / 'get_by_id.sql',
|
|
190
268
|
)
|
|
191
269
|
```
|
|
192
270
|
|
|
271
|
+
`{% include %}` and `{% import %}` inside the template resolve relative to the template's directory.
|
|
272
|
+
|
|
193
273
|
### Result
|
|
194
274
|
|
|
195
275
|
The result of query execution is a `Result` object that wraps the data into the specified model:
|
|
@@ -202,12 +282,11 @@ The result of query execution is a `Result` object that wraps the data into the
|
|
|
202
282
|
```python
|
|
203
283
|
from dataclasses import dataclass
|
|
204
284
|
from datetime import datetime
|
|
205
|
-
from typing import Optional
|
|
206
285
|
|
|
207
|
-
from ddsql.query import Query
|
|
208
|
-
from ddsql.sqlbase import SQLBase
|
|
209
286
|
from ddsql.adapter import Adapter, AdapterDescriptor
|
|
287
|
+
from ddsql.query import Query
|
|
210
288
|
from ddsql.serializers import PostgresSerializer
|
|
289
|
+
from ddsql.sqlbase import SQLBase
|
|
211
290
|
|
|
212
291
|
|
|
213
292
|
class PostgresAdapter(Adapter):
|
|
@@ -225,7 +304,7 @@ class SQL(SQLBase):
|
|
|
225
304
|
class User:
|
|
226
305
|
user_id: int
|
|
227
306
|
name: str
|
|
228
|
-
email:
|
|
307
|
+
email: str | None
|
|
229
308
|
created_at: datetime
|
|
230
309
|
is_deleted: bool
|
|
231
310
|
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Generic, Self, TypeVar, overload
|
|
6
|
+
|
|
7
|
+
from ddutils.annotation_helpers import is_subclass
|
|
8
|
+
|
|
9
|
+
from ddsql.serializers import BaseSerializer
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from collections.abc import Hashable, Sequence
|
|
13
|
+
|
|
14
|
+
from ddsql.query import Result
|
|
15
|
+
from ddsql.sqlbase import SQLBase
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Adapter(ABC):
|
|
19
|
+
serializer: BaseSerializer
|
|
20
|
+
|
|
21
|
+
def __init_subclass__(cls, **kwargs):
|
|
22
|
+
super().__init_subclass__(**kwargs) # keeps Generic / mixins in the MRO working
|
|
23
|
+
if inspect.isabstract(cls): # intermediate base classes declare no serializer
|
|
24
|
+
return
|
|
25
|
+
serializer = getattr(cls, 'serializer', None)
|
|
26
|
+
serializer_class = getattr(serializer, '__class__', None)
|
|
27
|
+
if not is_subclass(serializer_class, BaseSerializer):
|
|
28
|
+
raise NotImplementedError(
|
|
29
|
+
'Subclass of Adapter must define a valid serializer attribute that is a subclass of Serializer'
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
def __init__(self, sql: SQLBase) -> None:
|
|
33
|
+
self.sql = sql
|
|
34
|
+
self.db: Hashable | None = None
|
|
35
|
+
|
|
36
|
+
def using(self, db: Hashable) -> Self:
|
|
37
|
+
"""
|
|
38
|
+
Picks the database for this execution, like Django's `using`; `_execute` reads `self.db`
|
|
39
|
+
(`None` = default) and passes it to whatever opens the connection:
|
|
40
|
+
|
|
41
|
+
await SQL(query).postgres.execute() # default connection
|
|
42
|
+
await SQL(query).postgres.using(PostgresDB.REPLICA).execute()
|
|
43
|
+
"""
|
|
44
|
+
self.db = db
|
|
45
|
+
return self
|
|
46
|
+
|
|
47
|
+
async def get_query(self) -> str:
|
|
48
|
+
return await self.sql.query.render_template(
|
|
49
|
+
params=self.sql.params, template_functions=self.serializer.template_functions
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
async def execute(self) -> Result:
|
|
53
|
+
return self.sql.query.build_result(await self._execute())
|
|
54
|
+
|
|
55
|
+
@abstractmethod
|
|
56
|
+
async def _execute(self) -> Sequence[dict[str, Any]]: ...
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
AdapterT = TypeVar('AdapterT', bound=Adapter)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class AdapterDescriptor(Generic[AdapterT]):
|
|
63
|
+
adapter_class: type[AdapterT]
|
|
64
|
+
|
|
65
|
+
def __init__(self, adapter_class: type[AdapterT]):
|
|
66
|
+
self.adapter_class = adapter_class
|
|
67
|
+
|
|
68
|
+
@overload
|
|
69
|
+
def __get__(self, sql: None, sql_class: type[SQLBase]) -> AdapterDescriptor[AdapterT]: ...
|
|
70
|
+
|
|
71
|
+
@overload
|
|
72
|
+
def __get__(self, sql: SQLBase, sql_class: type[SQLBase] | None = None) -> AdapterT: ...
|
|
73
|
+
|
|
74
|
+
def __get__(self, sql: SQLBase | None, sql_class: type[SQLBase] | None = None) -> AdapterT | AdapterDescriptor[AdapterT]:
|
|
75
|
+
# accessed on the class, like `SQL.postgres`: return the descriptor itself, as `property` does
|
|
76
|
+
if sql is None:
|
|
77
|
+
return self
|
|
78
|
+
return self.adapter_class(sql)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
__all__ = ('Adapter', 'AdapterDescriptor')
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from collections.abc import Callable, Hashable, Mapping
|
|
2
|
+
from typing import Any, Generic, TypeVar
|
|
3
|
+
|
|
4
|
+
from ddutils.scoped_registry import ScopedRegistry
|
|
5
|
+
|
|
6
|
+
# The name a connection is registered under and selected by, like Django's `using` alias
|
|
7
|
+
ConnectionAliasT = TypeVar('ConnectionAliasT', bound=Hashable)
|
|
8
|
+
# The object a registry holds for a connection: a pooled client, a session
|
|
9
|
+
ConnectionT = TypeVar('ConnectionT')
|
|
10
|
+
# The context manager the factory opens on the chosen registry
|
|
11
|
+
ConnectionManagerT = TypeVar('ConnectionManagerT')
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ConnectionManager(Generic[ConnectionT]):
|
|
15
|
+
"""
|
|
16
|
+
Default connection manager: hands out the connection of the registry's current scope and releases nothing on exit.
|
|
17
|
+
|
|
18
|
+
The connection is created on first use via `registry.set()` and kept for the life of its scope,
|
|
19
|
+
typically a process, since a client is itself a connection pool. Databases with transactions need their
|
|
20
|
+
own connection manager on top of the registry, e.g. a unit of work that begins, commits and closes a session on exit.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, registry: ScopedRegistry[ConnectionT]) -> None:
|
|
24
|
+
self._registry = registry
|
|
25
|
+
|
|
26
|
+
async def __aenter__(self) -> ConnectionT:
|
|
27
|
+
return await self._registry.set()
|
|
28
|
+
|
|
29
|
+
async def __aexit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: Any) -> None:
|
|
30
|
+
return None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ConnectionManagerFactory(Generic[ConnectionAliasT, ConnectionT, ConnectionManagerT]):
|
|
34
|
+
"""
|
|
35
|
+
Resolves a connection alias to its registry and opens a connection manager on it, like Django's `using`.
|
|
36
|
+
|
|
37
|
+
`connection_manager_class` is the context manager opened on the chosen registry: `ConnectionManager` by
|
|
38
|
+
default, a project class for anything that needs setup and teardown around the connection (transactions):
|
|
39
|
+
|
|
40
|
+
clickhouse = ConnectionManagerFactory(client_registries, default=ClickhouseDB.PRIMARY)
|
|
41
|
+
atomic = ConnectionManagerFactory(session_registries, default=PostgresDB.PRIMARY, connection_manager_class=Atomic)
|
|
42
|
+
|
|
43
|
+
async with clickhouse() as client: ...
|
|
44
|
+
async with atomic(using=PostgresDB.REPLICA) as session: ...
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(
|
|
48
|
+
self,
|
|
49
|
+
registries: Mapping[ConnectionAliasT, ScopedRegistry[ConnectionT]],
|
|
50
|
+
default: ConnectionAliasT,
|
|
51
|
+
connection_manager_class: Callable[[ScopedRegistry[ConnectionT]], ConnectionManagerT] = ConnectionManager, # type: ignore[assignment]
|
|
52
|
+
) -> None:
|
|
53
|
+
if default not in registries:
|
|
54
|
+
raise ValueError(f'Default connection {default!r} is not among the registries')
|
|
55
|
+
self.registries = registries
|
|
56
|
+
self.default = default
|
|
57
|
+
self.connection_manager_class = connection_manager_class
|
|
58
|
+
|
|
59
|
+
def registry(self, using: Hashable | None = None) -> ScopedRegistry[ConnectionT]:
|
|
60
|
+
"""The registry of `using`, or of the default connection; `Hashable` because `Adapter.db` is untyped."""
|
|
61
|
+
alias = self.default if using is None else using
|
|
62
|
+
if alias not in self.registries:
|
|
63
|
+
raise KeyError(f'Unknown connection {alias!r}; expected one of {list(self.registries)!r}')
|
|
64
|
+
return self.registries[alias] # type: ignore[index]
|
|
65
|
+
|
|
66
|
+
def __call__(self, using: Hashable | None = None) -> ConnectionManagerT:
|
|
67
|
+
return self.connection_manager_class(self.registry(using))
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
__all__ = ('ConnectionManager', 'ConnectionManagerFactory')
|
|
@@ -1,55 +1,51 @@
|
|
|
1
|
-
import
|
|
1
|
+
from collections.abc import Sequence
|
|
2
2
|
from pathlib import Path
|
|
3
|
-
from typing import Any,
|
|
3
|
+
from typing import Any, Generic, TypeVar
|
|
4
4
|
|
|
5
5
|
from jinja2 import Environment, FileSystemLoader, Template
|
|
6
6
|
|
|
7
|
-
SQL_TEMPLATES_DIR = os.getenv('SQL_TEMPLATES_DIR')
|
|
8
|
-
|
|
9
|
-
|
|
10
7
|
DataT = TypeVar('DataT')
|
|
11
8
|
|
|
12
9
|
|
|
13
10
|
class Result(Generic[DataT]):
|
|
14
11
|
rows: Sequence[dict[str, Any]]
|
|
15
|
-
model:
|
|
12
|
+
model: type[DataT]
|
|
16
13
|
|
|
17
|
-
def __init__(self, rows: Sequence[dict[str, Any]], model:
|
|
14
|
+
def __init__(self, rows: Sequence[dict[str, Any]], model: type[DataT]):
|
|
18
15
|
self.rows = rows
|
|
19
16
|
self.model = model
|
|
20
17
|
|
|
21
|
-
def get(self) ->
|
|
18
|
+
def get(self) -> DataT | None:
|
|
22
19
|
if not self.rows:
|
|
23
20
|
return None
|
|
24
21
|
return self.model(**self.rows[0])
|
|
25
22
|
|
|
26
|
-
def get_list(self) ->
|
|
23
|
+
def get_list(self) -> tuple[DataT, ...]:
|
|
27
24
|
return tuple(self.model(**row) for row in self.rows)
|
|
28
25
|
|
|
29
26
|
|
|
30
27
|
class Query(Generic[DataT]):
|
|
31
|
-
model:
|
|
28
|
+
model: type[DataT]
|
|
32
29
|
template: Template
|
|
33
30
|
|
|
34
|
-
def __init__(self, model:
|
|
31
|
+
def __init__(self, model: type[DataT], text: str | None = None, path: Path | None = None):
|
|
35
32
|
self.model = model
|
|
36
33
|
|
|
37
34
|
self.template = self.get_template(text, path)
|
|
38
35
|
|
|
39
36
|
@staticmethod
|
|
40
|
-
def get_template(text:
|
|
37
|
+
def get_template(text: str | None = None, path: Path | None = None) -> Template:
|
|
41
38
|
if text:
|
|
42
|
-
file_system_loader =
|
|
39
|
+
file_system_loader = None
|
|
43
40
|
method = 'from_string'
|
|
44
41
|
query = text
|
|
45
42
|
elif path:
|
|
46
|
-
if
|
|
47
|
-
raise
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
file_system_loader = FileSystemLoader(Path(SQL_TEMPLATES_DIR))
|
|
43
|
+
if not path.is_file():
|
|
44
|
+
raise FileNotFoundError(f'SQL template not found: {path.resolve()}')
|
|
45
|
+
# the loader is rooted at the template directory, so `{% include %}` resolves relative to the file
|
|
46
|
+
file_system_loader = FileSystemLoader(path.parent)
|
|
51
47
|
method = 'get_template'
|
|
52
|
-
query = path
|
|
48
|
+
query = path.name
|
|
53
49
|
else:
|
|
54
50
|
raise ValueError('One of `text` or `path` must be specified')
|
|
55
51
|
|
|
@@ -59,7 +55,7 @@ class Query(Generic[DataT]):
|
|
|
59
55
|
|
|
60
56
|
return getattr(environment, method)(query)
|
|
61
57
|
|
|
62
|
-
def update_globals(self, envs:
|
|
58
|
+
def update_globals(self, envs: dict[str, Any] | None = None):
|
|
63
59
|
if envs:
|
|
64
60
|
self.template.environment.globals.update(envs)
|
|
65
61
|
|
|
@@ -68,7 +64,7 @@ class Query(Generic[DataT]):
|
|
|
68
64
|
strings = sql.split('\n')
|
|
69
65
|
return '\n'.join([string.strip() for string in strings])
|
|
70
66
|
|
|
71
|
-
async def render_template(self, params:
|
|
67
|
+
async def render_template(self, params: dict[str, Any], template_functions: dict[str, Any] | None = None) -> str:
|
|
72
68
|
self.update_globals(template_functions)
|
|
73
69
|
|
|
74
70
|
rendered_template = await self.template.render_async(**params)
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
from collections.abc import Callable, Collection, Mapping
|
|
2
2
|
from datetime import date, datetime
|
|
3
3
|
from decimal import Decimal
|
|
4
|
-
from typing import Any
|
|
4
|
+
from typing import Any
|
|
5
5
|
from uuid import UUID
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class BaseSerializer:
|
|
9
9
|
@property
|
|
10
|
-
def template_functions(self) ->
|
|
10
|
+
def template_functions(self) -> dict[str, Callable[[Any], str]]:
|
|
11
11
|
return {'serialize_value': self.serialize_value}
|
|
12
12
|
|
|
13
13
|
def serialize_value(self, value: Any) -> str:
|
|
@@ -41,7 +41,7 @@ class BaseSerializer:
|
|
|
41
41
|
return f'{value}'.lower()
|
|
42
42
|
|
|
43
43
|
@staticmethod
|
|
44
|
-
def serialize_number(value:
|
|
44
|
+
def serialize_number(value: int | float | Decimal) -> str:
|
|
45
45
|
return f'{value}'
|
|
46
46
|
|
|
47
47
|
@staticmethod
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
from datetime import date, datetime
|
|
2
|
-
from typing import Dict
|
|
3
2
|
from uuid import UUID
|
|
4
3
|
|
|
5
4
|
from ddsql.serializers import BaseSerializer
|
|
6
5
|
|
|
7
6
|
|
|
8
|
-
def _build_escape_translation() ->
|
|
7
|
+
def _build_escape_translation() -> dict[int, str]:
|
|
9
8
|
# https://clickhouse.com/docs/en/sql-reference/syntax#string
|
|
10
9
|
translation = {
|
|
11
10
|
ord('\\'): '\\\\',
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import re
|
|
2
2
|
from datetime import date, datetime
|
|
3
|
-
from typing import Dict
|
|
4
3
|
from uuid import UUID
|
|
5
4
|
|
|
6
5
|
from ddsql.serializers import BaseSerializer
|
|
@@ -11,7 +10,7 @@ from ddsql.serializers import BaseSerializer
|
|
|
11
10
|
_ESCAPE_SYNTAX_REQUIRED = re.compile(r'[\\\x00-\x1F\x7F]')
|
|
12
11
|
|
|
13
12
|
|
|
14
|
-
def _build_escape_translation() ->
|
|
13
|
+
def _build_escape_translation() -> dict[int, str]:
|
|
15
14
|
# https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-STRINGS-ESCAPE
|
|
16
15
|
translation = {
|
|
17
16
|
ord('\\'): '\\\\',
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from abc import ABC
|
|
4
|
-
from typing import TYPE_CHECKING, Any,
|
|
4
|
+
from typing import TYPE_CHECKING, Any, TypeVar
|
|
5
5
|
|
|
6
|
-
from ddutils.annotation_helpers import is_subclass
|
|
7
6
|
from ddutils.class_helpers import classproperty
|
|
8
7
|
|
|
9
|
-
from ddsql.adapter import
|
|
8
|
+
from ddsql.adapter import AdapterDescriptor
|
|
10
9
|
|
|
11
10
|
if TYPE_CHECKING:
|
|
12
11
|
from ddsql.query import Query
|
|
@@ -29,22 +28,18 @@ class SQLBase(ABC):
|
|
|
29
28
|
"""
|
|
30
29
|
|
|
31
30
|
query: Query
|
|
32
|
-
params:
|
|
31
|
+
params: dict[str, Any]
|
|
33
32
|
|
|
34
|
-
@classmethod
|
|
35
33
|
def __init_subclass__(cls, **kwargs):
|
|
34
|
+
super().__init_subclass__(**kwargs) # keeps Generic / mixins in the MRO working
|
|
36
35
|
if not cls.has_adapters:
|
|
37
36
|
raise NotImplementedError('Subclasses must define at least one adapter')
|
|
38
37
|
|
|
39
38
|
@classproperty
|
|
40
|
-
def has_adapters(cls) -> bool:
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
if adapter_class and is_subclass(adapter_class, Adapter):
|
|
45
|
-
return True
|
|
46
|
-
|
|
47
|
-
return False
|
|
39
|
+
def has_adapters(cls: type[SQLBase]) -> bool:
|
|
40
|
+
# Descriptors are looked up in the class dictionaries along the MRO, so inherited adapters count
|
|
41
|
+
# and no adapter gets instantiated just for the check
|
|
42
|
+
return any(isinstance(value, AdapterDescriptor) for klass in cls.__mro__ for value in vars(klass).values())
|
|
48
43
|
|
|
49
44
|
def __init__(self, query: Query) -> None:
|
|
50
45
|
self.query = query
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "ddsql"
|
|
3
|
+
description = "SQL Query Library"
|
|
4
|
+
version = "0.2.0"
|
|
5
|
+
license = "MIT"
|
|
6
|
+
license-files = ["LICENSE"]
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
requires-python = ">=3.11,<3.15"
|
|
9
|
+
keywords = [
|
|
10
|
+
"python",
|
|
11
|
+
"ddsql",
|
|
12
|
+
]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"Programming Language :: Python :: 3.11",
|
|
16
|
+
"Programming Language :: Python :: 3.12",
|
|
17
|
+
"Programming Language :: Python :: 3.13",
|
|
18
|
+
"Programming Language :: Python :: 3.14",
|
|
19
|
+
"Typing :: Typed",
|
|
20
|
+
]
|
|
21
|
+
dependencies = [
|
|
22
|
+
"ddutils>=0.2.0,<0.3.0",
|
|
23
|
+
"jinja2>=3.0.0,<4.0.0",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[[project.authors]]
|
|
27
|
+
name = "davyddd"
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Repository = "https://github.com/davyddd/ddsql"
|
|
31
|
+
|
|
32
|
+
[dependency-groups]
|
|
33
|
+
dev = [
|
|
34
|
+
"ipdb==0.13.13",
|
|
35
|
+
"ipython==9.16.1",
|
|
36
|
+
]
|
|
37
|
+
test = [
|
|
38
|
+
"pytest==9.0.3",
|
|
39
|
+
"pytest-cov==5.0.0",
|
|
40
|
+
"parameterized==0.9.0",
|
|
41
|
+
]
|
|
42
|
+
linter = [
|
|
43
|
+
"ruff==0.15.10",
|
|
44
|
+
"ty==0.0.15",
|
|
45
|
+
"complexipy==8.0.1",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
[tool.uv.build-backend]
|
|
49
|
+
module-root = ""
|
|
50
|
+
|
|
51
|
+
[build-system]
|
|
52
|
+
requires = ["uv_build>=0.12.0,<0.13.0"]
|
|
53
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "ddsql"
|
|
3
|
+
description = "SQL Query Library"
|
|
4
|
+
version = "0.2.0"
|
|
5
|
+
authors = [{ name = "davyddd" }]
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
requires-python = ">=3.11,<3.15"
|
|
10
|
+
keywords = ["python", "ddsql"]
|
|
11
|
+
# uv_build does not derive these from requires-python (poetry did); PyPI and the pyversions badge read them
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Programming Language :: Python :: 3",
|
|
14
|
+
"Programming Language :: Python :: 3.11",
|
|
15
|
+
"Programming Language :: Python :: 3.12",
|
|
16
|
+
"Programming Language :: Python :: 3.13",
|
|
17
|
+
"Programming Language :: Python :: 3.14",
|
|
18
|
+
"Typing :: Typed",
|
|
19
|
+
]
|
|
20
|
+
dependencies = [
|
|
21
|
+
"ddutils>=0.2.0,<0.3.0",
|
|
22
|
+
"jinja2>=3.0.0,<4.0.0",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Repository = "https://github.com/davyddd/ddsql"
|
|
27
|
+
|
|
28
|
+
[dependency-groups]
|
|
29
|
+
dev = [
|
|
30
|
+
"ipdb==0.13.13",
|
|
31
|
+
"ipython==9.16.1",
|
|
32
|
+
]
|
|
33
|
+
test = [
|
|
34
|
+
"pytest==9.0.3",
|
|
35
|
+
"pytest-cov==5.0.0",
|
|
36
|
+
"parameterized==0.9.0",
|
|
37
|
+
]
|
|
38
|
+
linter = [
|
|
39
|
+
"ruff==0.15.10",
|
|
40
|
+
"ty==0.0.15",
|
|
41
|
+
"complexipy==8.0.1",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
[tool.uv.build-backend]
|
|
45
|
+
module-root = ""
|
|
46
|
+
|
|
47
|
+
[build-system]
|
|
48
|
+
requires = ["uv_build>=0.12.0,<0.13.0"]
|
|
49
|
+
build-backend = "uv_build"
|
ddsql-0.0.4/ddsql/__init__.py
DELETED
ddsql-0.0.4/ddsql/adapter.py
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from abc import ABC, abstractmethod
|
|
4
|
-
from typing import TYPE_CHECKING, Any, Dict, Generic, Optional, Sequence, Type, TypeVar
|
|
5
|
-
|
|
6
|
-
from ddutils.annotation_helpers import is_subclass
|
|
7
|
-
|
|
8
|
-
from ddsql.serializers import BaseSerializer
|
|
9
|
-
|
|
10
|
-
if TYPE_CHECKING:
|
|
11
|
-
from ddsql.query import Result
|
|
12
|
-
from ddsql.sqlbase import SQLBase
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class Adapter(ABC):
|
|
16
|
-
serializer: BaseSerializer
|
|
17
|
-
|
|
18
|
-
@classmethod
|
|
19
|
-
def __init_subclass__(cls, **kwargs):
|
|
20
|
-
serializer = getattr(cls, 'serializer', None)
|
|
21
|
-
serializer_class = getattr(serializer, '__class__', None)
|
|
22
|
-
if not is_subclass(serializer_class, BaseSerializer):
|
|
23
|
-
raise NotImplementedError(
|
|
24
|
-
'Subclass of Adapter must define a valid serializer attribute that is a subclass of Serializer'
|
|
25
|
-
)
|
|
26
|
-
|
|
27
|
-
def __init__(self, sql: SQLBase) -> None:
|
|
28
|
-
self.sql = sql
|
|
29
|
-
|
|
30
|
-
async def get_query(self) -> str:
|
|
31
|
-
return await self.sql.query.render_template(
|
|
32
|
-
params=self.sql.params, template_functions=self.serializer.template_functions
|
|
33
|
-
)
|
|
34
|
-
|
|
35
|
-
async def execute(self) -> Result:
|
|
36
|
-
return self.sql.query.build_result(await self._execute())
|
|
37
|
-
|
|
38
|
-
@abstractmethod
|
|
39
|
-
async def _execute(self) -> Sequence[Dict[str, Any]]: # noqa: UP006
|
|
40
|
-
...
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
AdapterT = TypeVar('AdapterT', bound=Adapter)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
class AdapterDescriptor(Generic[AdapterT]):
|
|
47
|
-
adapter_class: Type[AdapterT] # noqa: UP006
|
|
48
|
-
|
|
49
|
-
def __init__(self, adapter_class: Type[AdapterT]): # noqa: UP006
|
|
50
|
-
self.adapter_class = adapter_class
|
|
51
|
-
|
|
52
|
-
def __get__(self, sql: SQLBase, sql_class: Optional[Type[SQLBase]] = None) -> AdapterT: # noqa: UP006, UP007
|
|
53
|
-
return self.adapter_class(sql)
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
__all__ = ('Adapter', 'AdapterDescriptor')
|
ddsql-0.0.4/pyproject.toml
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
[tool.poetry]
|
|
2
|
-
name = "ddsql"
|
|
3
|
-
description = "SQL Query Library"
|
|
4
|
-
version = "0.0.4"
|
|
5
|
-
authors = ["davyddd"]
|
|
6
|
-
license = "MIT"
|
|
7
|
-
repository = "https://github.com/davyddd/ddsql"
|
|
8
|
-
readme = "README.md"
|
|
9
|
-
keywords = ["python", "ddsql"]
|
|
10
|
-
packages = [{include = "ddsql"}]
|
|
11
|
-
|
|
12
|
-
[tool.poetry.dependencies]
|
|
13
|
-
python = ">=3.9,<3.15"
|
|
14
|
-
ddutils = "<0.2.0"
|
|
15
|
-
jinja2 = ">=3.0.0,<4.0.0"
|
|
16
|
-
|
|
17
|
-
[tool.poetry.group.dev.dependencies]
|
|
18
|
-
ipdb = "0.13.9"
|
|
19
|
-
ipython = "8.12.3"
|
|
20
|
-
|
|
21
|
-
[tool.poetry.group.test.dependencies]
|
|
22
|
-
pytest = "8.1.1"
|
|
23
|
-
pytest-cov = "5.0.0"
|
|
24
|
-
parameterized = "0.9.0"
|
|
25
|
-
typing-extensions = "^4.12.2"
|
|
26
|
-
|
|
27
|
-
[tool.poetry.group.linter.dependencies]
|
|
28
|
-
mypy = "1.18.2"
|
|
29
|
-
ruff = "0.14.4"
|
|
30
|
-
|
|
31
|
-
[build-system]
|
|
32
|
-
requires = ["poetry-core>=1.0.0"]
|
|
33
|
-
build-backend = "poetry.core.masonry.api"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|