ddsql 0.2.0__tar.gz → 0.3.1__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ddsql
3
- Version: 0.2.0
3
+ Version: 0.3.1
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.2.0,<0.3.0
15
+ Requires-Dist: ddutils>=0.3.0,<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
 
@@ -141,7 +146,7 @@ per task. The connections module adds two things on top:
141
146
 
142
147
  - `ConnectionManagerFactory(registries, default, connection_manager_class=ConnectionManager)` resolves a
143
148
  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.
149
+ chosen registry; `registry(alias)` returns the registry itself.
145
150
  - `ConnectionManager(registry)` is the default connection manager: an async context manager that hands out
146
151
  the connection of the registry's current scope and releases nothing on exit. Enough for clients that are
147
152
  pools themselves, such as ClickHouse.
@@ -177,7 +182,7 @@ async with clickhouse() as client:
177
182
  atomic = ConnectionManagerFactory(session_registries, default=PostgresDB.PRIMARY, connection_manager_class=Atomic)
178
183
 
179
184
 
180
- async with atomic(using=PostgresDB.REPLICA) as session:
185
+ async with atomic(alias=PostgresDB.REPLICA) as session:
181
186
  ...
182
187
  ```
183
188
 
@@ -202,7 +207,7 @@ class PostgresAdapter(Adapter):
202
207
  serializer = PostgresSerializer()
203
208
 
204
209
  async def _execute(self) -> Sequence[dict[str, Any]]:
205
- async with atomic(using=self.db) as session:
210
+ async with atomic(alias=self.alias) as session:
206
211
  query = await self.get_query() # the rendered SQL query
207
212
  result = await session.execute(text(query))
208
213
  return [dict(zip(result.keys(), row)) for row in result.fetchall()]
@@ -213,8 +218,8 @@ adapters are validated.
213
218
 
214
219
  ### Choosing the database
215
220
 
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:
221
+ `Adapter.using(alias)` picks the connection alias right before executing, like Django's `using`. `_execute`
222
+ reads `self.alias` (`None` means the default) and passes it to whatever opens the connection, as in the example above:
218
223
 
219
224
  ```python
220
225
  await SQL(query).postgres.execute() # default connection
@@ -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
 
@@ -121,7 +126,7 @@ per task. The connections module adds two things on top:
121
126
 
122
127
  - `ConnectionManagerFactory(registries, default, connection_manager_class=ConnectionManager)` resolves a
123
128
  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.
129
+ chosen registry; `registry(alias)` returns the registry itself.
125
130
  - `ConnectionManager(registry)` is the default connection manager: an async context manager that hands out
126
131
  the connection of the registry's current scope and releases nothing on exit. Enough for clients that are
127
132
  pools themselves, such as ClickHouse.
@@ -157,7 +162,7 @@ async with clickhouse() as client:
157
162
  atomic = ConnectionManagerFactory(session_registries, default=PostgresDB.PRIMARY, connection_manager_class=Atomic)
158
163
 
159
164
 
160
- async with atomic(using=PostgresDB.REPLICA) as session:
165
+ async with atomic(alias=PostgresDB.REPLICA) as session:
161
166
  ...
162
167
  ```
163
168
 
@@ -182,7 +187,7 @@ class PostgresAdapter(Adapter):
182
187
  serializer = PostgresSerializer()
183
188
 
184
189
  async def _execute(self) -> Sequence[dict[str, Any]]:
185
- async with atomic(using=self.db) as session:
190
+ async with atomic(alias=self.alias) as session:
186
191
  query = await self.get_query() # the rendered SQL query
187
192
  result = await session.execute(text(query))
188
193
  return [dict(zip(result.keys(), row)) for row in result.fetchall()]
@@ -193,8 +198,8 @@ adapters are validated.
193
198
 
194
199
  ### Choosing the database
195
200
 
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:
201
+ `Adapter.using(alias)` picks the connection alias right before executing, like Django's `using`. `_execute`
202
+ reads `self.alias` (`None` means the default) and passes it to whatever opens the connection, as in the example above:
198
203
 
199
204
  ```python
200
205
  await SQL(query).postgres.execute() # default connection
@@ -31,17 +31,17 @@ class Adapter(ABC):
31
31
 
32
32
  def __init__(self, sql: SQLBase) -> None:
33
33
  self.sql = sql
34
- self.db: Hashable | None = None
34
+ self.alias: Hashable | None = None
35
35
 
36
- def using(self, db: Hashable) -> Self:
36
+ def using(self, alias: Hashable) -> Self:
37
37
  """
38
- Picks the database for this execution, like Django's `using`; `_execute` reads `self.db`
38
+ Picks the connection alias for this execution, like Django's `using`; `_execute` reads `self.alias`
39
39
  (`None` = default) and passes it to whatever opens the connection:
40
40
 
41
41
  await SQL(query).postgres.execute() # default connection
42
42
  await SQL(query).postgres.using(PostgresDB.REPLICA).execute()
43
43
  """
44
- self.db = db
44
+ self.alias = alias
45
45
  return self
46
46
 
47
47
  async def get_query(self) -> str:
@@ -41,7 +41,7 @@ class ConnectionManagerFactory(Generic[ConnectionAliasT, ConnectionT, Connection
41
41
  atomic = ConnectionManagerFactory(session_registries, default=PostgresDB.PRIMARY, connection_manager_class=Atomic)
42
42
 
43
43
  async with clickhouse() as client: ...
44
- async with atomic(using=PostgresDB.REPLICA) as session: ...
44
+ async with atomic(alias=PostgresDB.REPLICA) as session: ...
45
45
  """
46
46
 
47
47
  def __init__(
@@ -56,15 +56,15 @@ class ConnectionManagerFactory(Generic[ConnectionAliasT, ConnectionT, Connection
56
56
  self.default = default
57
57
  self.connection_manager_class = connection_manager_class
58
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
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
62
  if alias not in self.registries:
63
63
  raise KeyError(f'Unknown connection {alias!r}; expected one of {list(self.registries)!r}')
64
64
  return self.registries[alias] # type: ignore[index]
65
65
 
66
- def __call__(self, using: Hashable | None = None) -> ConnectionManagerT:
67
- return self.connection_manager_class(self.registry(using))
66
+ def __call__(self, alias: Hashable | None = None) -> ConnectionManagerT:
67
+ return self.connection_manager_class(self.registry(alias))
68
68
 
69
69
 
70
70
  __all__ = ('ConnectionManager', 'ConnectionManagerFactory')
@@ -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.2.0"
4
+ version = "0.3.1"
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.2.0,<0.3.0",
22
+ "ddutils>=0.3.0,<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.2.0"
4
+ version = "0.3.1"
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.2.0,<0.3.0",
21
+ "ddutils>=0.3.0,<0.4.0",
22
22
  "jinja2>=3.0.0,<4.0.0",
23
23
  ]
24
24
 
File without changes
File without changes
File without changes
File without changes
File without changes