ddsql 0.3.0__tar.gz → 0.4.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.3.0 → ddsql-0.4.0}/PKG-INFO +11 -47
- {ddsql-0.3.0 → ddsql-0.4.0}/README.md +9 -45
- ddsql-0.4.0/ddsql/__init__.py +3 -0
- {ddsql-0.3.0 → ddsql-0.4.0}/ddsql/serializers/base.py +8 -1
- {ddsql-0.3.0 → ddsql-0.4.0}/pyproject.toml +2 -2
- {ddsql-0.3.0 → ddsql-0.4.0}/pyproject.toml.orig +2 -2
- ddsql-0.3.0/ddsql/__init__.py +0 -3
- ddsql-0.3.0/ddsql/connections.py +0 -70
- {ddsql-0.3.0 → ddsql-0.4.0}/LICENSE +0 -0
- {ddsql-0.3.0 → ddsql-0.4.0}/ddsql/adapter.py +0 -0
- {ddsql-0.3.0 → ddsql-0.4.0}/ddsql/py.typed +0 -0
- {ddsql-0.3.0 → ddsql-0.4.0}/ddsql/query.py +0 -0
- {ddsql-0.3.0 → ddsql-0.4.0}/ddsql/serializers/__init__.py +0 -0
- {ddsql-0.3.0 → ddsql-0.4.0}/ddsql/serializers/clickhouse.py +0 -0
- {ddsql-0.3.0 → ddsql-0.4.0}/ddsql/serializers/postgres.py +0 -0
- {ddsql-0.3.0 → ddsql-0.4.0}/ddsql/sqlbase.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ddsql
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: SQL Query Library
|
|
5
5
|
Keywords: python,ddsql
|
|
6
6
|
Author: davyddd
|
|
@@ -12,7 +12,7 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.13
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.14
|
|
14
14
|
Classifier: Typing :: Typed
|
|
15
|
-
Requires-Dist: ddutils>=0.
|
|
15
|
+
Requires-Dist: ddutils>=0.3.1,<0.4.0
|
|
16
16
|
Requires-Dist: jinja2>=3.0.0,<4.0.0
|
|
17
17
|
Requires-Python: >=3.11, <3.15
|
|
18
18
|
Project-URL: Repository, https://github.com/davyddd/ddsql
|
|
@@ -71,6 +71,11 @@ or create your own by inheriting from `BaseSerializer`.
|
|
|
71
71
|
| `date` | `'2025-01-01'` | `'2025-01-01'::date` | `toDate('2025-01-01')` |
|
|
72
72
|
| `list`/`tuple`/`set` | `(item1, item2, ...)` | `(item1, item2, ...)` | `(item1, item2, ...)` |
|
|
73
73
|
|
|
74
|
+
An `Enum` member is serialized by its value: `Kind.ADMIN` of a `class Kind(str, Enum)` renders as `'admin'`,
|
|
75
|
+
a plain `Enum` holding a `datetime` as that `datetime`. A subclass is matched as its base type
|
|
76
|
+
(a `ProfileId(UUID)` as a `UUID`, a `pendulum.DateTime` as a `datetime`). `bytes` and `bytearray` are not
|
|
77
|
+
serialized: like any other type missing from the table they go to `serialize_other_object`.
|
|
78
|
+
|
|
74
79
|
String values are escaped according to the dialect rules, so quotes, backslashes
|
|
75
80
|
and control characters cannot break the query:
|
|
76
81
|
|
|
@@ -135,51 +140,10 @@ class CustomSerializer(BaseSerializer):
|
|
|
135
140
|
|
|
136
141
|
## Connections
|
|
137
142
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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(alias)` 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(alias=PostgresDB.REPLICA) as session:
|
|
181
|
-
...
|
|
182
|
-
```
|
|
143
|
+
Connection routing (`ConnectionManagerFactory`, `ConnectionManager`) lives in
|
|
144
|
+
[ddutils](https://github.com/davyddd/ddutils#connections) since it is not specific to SQL: a `ScopedRegistry` per
|
|
145
|
+
alias, a factory that opens a connection manager on the chosen registry, like Django's `using`. An adapter runs its
|
|
146
|
+
query inside `factory(alias=self.alias)`; see "Choosing the database" below.
|
|
183
147
|
|
|
184
148
|
## Adapter
|
|
185
149
|
|
|
@@ -51,6 +51,11 @@ or create your own by inheriting from `BaseSerializer`.
|
|
|
51
51
|
| `date` | `'2025-01-01'` | `'2025-01-01'::date` | `toDate('2025-01-01')` |
|
|
52
52
|
| `list`/`tuple`/`set` | `(item1, item2, ...)` | `(item1, item2, ...)` | `(item1, item2, ...)` |
|
|
53
53
|
|
|
54
|
+
An `Enum` member is serialized by its value: `Kind.ADMIN` of a `class Kind(str, Enum)` renders as `'admin'`,
|
|
55
|
+
a plain `Enum` holding a `datetime` as that `datetime`. A subclass is matched as its base type
|
|
56
|
+
(a `ProfileId(UUID)` as a `UUID`, a `pendulum.DateTime` as a `datetime`). `bytes` and `bytearray` are not
|
|
57
|
+
serialized: like any other type missing from the table they go to `serialize_other_object`.
|
|
58
|
+
|
|
54
59
|
String values are escaped according to the dialect rules, so quotes, backslashes
|
|
55
60
|
and control characters cannot break the query:
|
|
56
61
|
|
|
@@ -115,51 +120,10 @@ class CustomSerializer(BaseSerializer):
|
|
|
115
120
|
|
|
116
121
|
## Connections
|
|
117
122
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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(alias)` 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(alias=PostgresDB.REPLICA) as session:
|
|
161
|
-
...
|
|
162
|
-
```
|
|
123
|
+
Connection routing (`ConnectionManagerFactory`, `ConnectionManager`) lives in
|
|
124
|
+
[ddutils](https://github.com/davyddd/ddutils#connections) since it is not specific to SQL: a `ScopedRegistry` per
|
|
125
|
+
alias, a factory that opens a connection manager on the chosen registry, like Django's `using`. An adapter runs its
|
|
126
|
+
query inside `factory(alias=self.alias)`; see "Choosing the database" below.
|
|
163
127
|
|
|
164
128
|
## Adapter
|
|
165
129
|
|
|
@@ -4,6 +4,9 @@ from decimal import Decimal
|
|
|
4
4
|
from typing import Any
|
|
5
5
|
from uuid import UUID
|
|
6
6
|
|
|
7
|
+
from ddutils.annotation_helpers import NON_COMPLEX_SEQUENCE_TYPES
|
|
8
|
+
from ddutils.value_helpers import unwrap_enum
|
|
9
|
+
|
|
7
10
|
|
|
8
11
|
class BaseSerializer:
|
|
9
12
|
@property
|
|
@@ -11,6 +14,9 @@ class BaseSerializer:
|
|
|
11
14
|
return {'serialize_value': self.serialize_value}
|
|
12
15
|
|
|
13
16
|
def serialize_value(self, value: Any) -> str:
|
|
17
|
+
# an `Enum` member is serialized by its value: `f'{Kind.A}'` of a `class Kind(int, Enum)` is 'Kind.A', not '1'
|
|
18
|
+
value = unwrap_enum(value)
|
|
19
|
+
|
|
14
20
|
if value is None:
|
|
15
21
|
return self.serialize_none(value)
|
|
16
22
|
elif isinstance(value, bool):
|
|
@@ -27,7 +33,8 @@ class BaseSerializer:
|
|
|
27
33
|
# the check for date must come after datetime,
|
|
28
34
|
# because a datetime instance can also be identified as a date
|
|
29
35
|
return self.serialize_date(value)
|
|
30
|
-
elif isinstance(value, Collection) and not isinstance(value, Mapping):
|
|
36
|
+
elif isinstance(value, Collection) and not isinstance(value, (Mapping, *NON_COMPLEX_SEQUENCE_TYPES)):
|
|
37
|
+
# `bytes` is a collection of ints, but has no SQL representation here
|
|
31
38
|
return self.serialize_collection(value)
|
|
32
39
|
else:
|
|
33
40
|
return self.serialize_other_object(value)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "ddsql"
|
|
3
3
|
description = "SQL Query Library"
|
|
4
|
-
version = "0.
|
|
4
|
+
version = "0.4.0"
|
|
5
5
|
license = "MIT"
|
|
6
6
|
license-files = ["LICENSE"]
|
|
7
7
|
readme = "README.md"
|
|
@@ -19,7 +19,7 @@ classifiers = [
|
|
|
19
19
|
"Typing :: Typed",
|
|
20
20
|
]
|
|
21
21
|
dependencies = [
|
|
22
|
-
"ddutils>=0.
|
|
22
|
+
"ddutils>=0.3.1,<0.4.0",
|
|
23
23
|
"jinja2>=3.0.0,<4.0.0",
|
|
24
24
|
]
|
|
25
25
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "ddsql"
|
|
3
3
|
description = "SQL Query Library"
|
|
4
|
-
version = "0.
|
|
4
|
+
version = "0.4.0"
|
|
5
5
|
authors = [{ name = "davyddd" }]
|
|
6
6
|
license = "MIT"
|
|
7
7
|
license-files = ["LICENSE"]
|
|
@@ -18,7 +18,7 @@ classifiers = [
|
|
|
18
18
|
"Typing :: Typed",
|
|
19
19
|
]
|
|
20
20
|
dependencies = [
|
|
21
|
-
"ddutils>=0.
|
|
21
|
+
"ddutils>=0.3.1,<0.4.0",
|
|
22
22
|
"jinja2>=3.0.0,<4.0.0",
|
|
23
23
|
]
|
|
24
24
|
|
ddsql-0.3.0/ddsql/__init__.py
DELETED
ddsql-0.3.0/ddsql/connections.py
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
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(alias=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, alias: Hashable | None = None) -> ScopedRegistry[ConnectionT]:
|
|
60
|
-
"""The registry of `alias`, or of the default connection; `Hashable` because `Adapter.alias` is untyped."""
|
|
61
|
-
alias = self.default if alias is None else alias
|
|
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, alias: Hashable | None = None) -> ConnectionManagerT:
|
|
67
|
-
return self.connection_manager_class(self.registry(alias))
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
__all__ = ('ConnectionManager', 'ConnectionManagerFactory')
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|