python-corekit 0.1.1__py3-none-any.whl → 0.3.0__py3-none-any.whl
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.
- corekit/api/__init__.py +18 -3
- corekit/api/application.py +275 -0
- corekit/api/lifespan.py +233 -0
- corekit/api/middleware.py +93 -0
- corekit/api/routers.py +109 -1
- corekit/concurrency/__init__.py +2 -2
- corekit/concurrency/decorators.py +32 -5
- corekit/concurrency/thread_local.py +2 -2
- corekit/concurrency/worker.py +74 -65
- corekit/config/loader.py +42 -5
- corekit/config/settings.py +11 -1
- corekit/connections/__init__.py +7 -1
- corekit/connections/connectable.py +45 -4
- corekit/connections/redis/connection.py +53 -10
- corekit/connections/sql/__init__.py +33 -4
- corekit/connections/sql/connection.py +56 -3
- corekit/connections/sql/fields/__init__.py +2 -2
- corekit/connections/sql/fields/jsonb.py +13 -6
- corekit/connections/sql/migration/__init__.py +9 -5
- corekit/connections/sql/migration/base.py +3 -3
- corekit/connections/sql/migration/operations.py +135 -44
- corekit/connections/sql/migration/registry.py +2 -2
- corekit/connections/sql/operations/__init__.py +24 -0
- corekit/connections/sql/operations/base.py +111 -0
- corekit/connections/sql/operations/statements.py +170 -0
- corekit/connections/sql/query.py +4 -62
- corekit/connections/sql/table.py +33 -29
- corekit/crypto/__init__.py +3 -1
- corekit/crypto/constants.py +2 -2
- corekit/crypto/hasher.py +9 -4
- corekit/data/__init__.py +8 -0
- corekit/data/dataset.py +8 -2
- corekit/data/expressions/__init__.py +10 -2
- corekit/data/expressions/comparison.py +142 -123
- corekit/data/expressions/expression.py +71 -98
- corekit/data/expressions/operator.py +39 -0
- corekit/data/expressions/target.py +21 -0
- corekit/data/record.py +147 -147
- corekit/data/stats.py +162 -157
- corekit/decorators/__init__.py +2 -2
- corekit/decorators/exception_handling.py +38 -9
- corekit/docker/watchdog.py +50 -31
- corekit/etl/__init__.py +2 -1
- corekit/etl/connection.py +46 -44
- corekit/etl/extract/extractor.py +6 -13
- corekit/etl/orchestrator.py +19 -2
- corekit/etl/schemas.py +2 -2
- corekit/etl/transform/transformer.py +4 -1
- corekit/events/publisher.py +1 -1
- corekit/events/reader.py +26 -21
- corekit/events/sse.py +4 -1
- corekit/events/websocket.py +27 -13
- corekit/exceptions/__init__.py +33 -0
- corekit/exceptions/base.py +139 -10
- corekit/exceptions/enum.py +17 -0
- corekit/exceptions/types.py +6 -6
- corekit/files/__init__.py +2 -4
- corekit/files/base.py +15 -2
- corekit/files/enum.py +0 -5
- corekit/files/json.py +16 -2
- corekit/http/__init__.py +51 -0
- corekit/http/api.py +24 -0
- corekit/http/client.py +100 -73
- corekit/http/exceptions.py +140 -0
- corekit/http/response.py +50 -1
- corekit/http/status.py +89 -0
- corekit/jobs/__init__.py +26 -0
- corekit/jobs/registry.py +87 -0
- corekit/jobs/runner.py +80 -0
- corekit/jobs/task.py +173 -0
- corekit/log_monitor/models.py +8 -2
- corekit/log_monitor/service.py +77 -38
- corekit/notifications/base.py +18 -10
- corekit/observability/__init__.py +12 -3
- corekit/observability/benchmarkable.py +23 -5
- corekit/observability/loggable.py +21 -0
- corekit/observability/request_context.py +188 -0
- corekit/observability/timing/timer.py +4 -2
- corekit/registry/__init__.py +12 -7
- corekit/registry/ordered.py +86 -0
- corekit/registry/registry.py +55 -14
- corekit/schemas/__init__.py +10 -0
- corekit/schemas/enum.py +70 -49
- corekit/schemas/models/arbitrary.py +11 -11
- corekit/schemas/pydantic/fields.py +35 -35
- corekit/schemas/types.py +45 -40
- corekit/serialization/__init__.py +24 -0
- corekit/serialization/pickle_file.py +61 -0
- corekit/serialization/serializable.py +22 -2
- corekit/serialization/serializer.py +10 -3
- corekit/utils/__init__.py +59 -5
- corekit/utils/coercion.py +118 -0
- corekit/utils/collections.py +124 -0
- corekit/utils/ids.py +61 -5
- corekit/utils/payload.py +112 -0
- corekit/utils/raise_exc.py +8 -8
- corekit/utils/text.py +56 -0
- corekit/utils/time.py +74 -21
- corekit/utils/validators.py +15 -15
- corekit/utils/void.py +8 -8
- {python_corekit-0.1.1.dist-info → python_corekit-0.3.0.dist-info}/METADATA +103 -97
- python_corekit-0.3.0.dist-info/RECORD +145 -0
- corekit/constants.py +0 -45
- corekit/exceptions/http/exceptions.py +0 -37
- corekit/files/pickle.py +0 -12
- python_corekit-0.1.1.dist-info/RECORD +0 -125
- {python_corekit-0.1.1.dist-info → python_corekit-0.3.0.dist-info}/WHEEL +0 -0
- {python_corekit-0.1.1.dist-info → python_corekit-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {python_corekit-0.1.1.dist-info → python_corekit-0.3.0.dist-info}/top_level.txt +0 -0
|
@@ -23,6 +23,7 @@ from redis import asyncio as aioredis
|
|
|
23
23
|
|
|
24
24
|
from corekit.config import get_settings
|
|
25
25
|
from corekit.connections import Connectable
|
|
26
|
+
from corekit.exceptions import InternalCoreException, Retryability
|
|
26
27
|
|
|
27
28
|
__all__ = ["RedisConnection", "RedisNotConnectedError"]
|
|
28
29
|
|
|
@@ -31,11 +32,14 @@ logger = logging.getLogger(__name__)
|
|
|
31
32
|
DEFAULT_URL = "redis://localhost:6379/0"
|
|
32
33
|
|
|
33
34
|
|
|
34
|
-
class RedisNotConnectedError(
|
|
35
|
+
class RedisNotConnectedError(InternalCoreException):
|
|
35
36
|
"""
|
|
36
37
|
Raised when a client is used before its connection is opened.
|
|
37
38
|
"""
|
|
38
39
|
|
|
40
|
+
def __init__(self, message: str, *, error: str | None = None) -> None:
|
|
41
|
+
super().__init__(message, retryable=Retryability.NON_RETRYABLE, error=error)
|
|
42
|
+
|
|
39
43
|
|
|
40
44
|
def _encode(value: Any) -> str:
|
|
41
45
|
"""
|
|
@@ -65,14 +69,15 @@ class RedisConnection(Connectable):
|
|
|
65
69
|
"""
|
|
66
70
|
A Connectable wrapper over a Redis client.
|
|
67
71
|
|
|
68
|
-
The sync client is shared per URL for the life of
|
|
72
|
+
The sync client is shared per URL for the life of its holders, because a
|
|
69
73
|
Redis client is a connection pool and building one per caller defeats it.
|
|
70
|
-
``
|
|
71
|
-
pool
|
|
72
|
-
bound to a running event loop.
|
|
74
|
+
Each ``connect`` takes a hold; ``disconnect`` releases it and closes the
|
|
75
|
+
pool only when no holder remains. Async clients are per-instance, since they
|
|
76
|
+
are bound to a running event loop.
|
|
73
77
|
"""
|
|
74
78
|
|
|
75
79
|
_shared_clients: dict[str, Any] = {}
|
|
80
|
+
_shared_refs: dict[str, int] = {}
|
|
76
81
|
|
|
77
82
|
def __init__(self, url: str | None = None, safe: bool = False, **kwargs: Any) -> None:
|
|
78
83
|
"""
|
|
@@ -83,6 +88,7 @@ class RedisConnection(Connectable):
|
|
|
83
88
|
self._url = url or get_settings().redis.url or DEFAULT_URL
|
|
84
89
|
self._safe = safe
|
|
85
90
|
self._async_client: Any = None
|
|
91
|
+
self._holds_shared = False
|
|
86
92
|
|
|
87
93
|
@property
|
|
88
94
|
def url(self) -> str:
|
|
@@ -93,7 +99,7 @@ class RedisConnection(Connectable):
|
|
|
93
99
|
|
|
94
100
|
@property
|
|
95
101
|
def is_connected(self) -> bool:
|
|
96
|
-
return
|
|
102
|
+
return self._holds_shared
|
|
97
103
|
|
|
98
104
|
@property
|
|
99
105
|
def is_async_connected(self) -> bool:
|
|
@@ -102,8 +108,10 @@ class RedisConnection(Connectable):
|
|
|
102
108
|
@property
|
|
103
109
|
def client(self) -> Any:
|
|
104
110
|
"""
|
|
105
|
-
The sync client, raising if
|
|
111
|
+
The sync client, raising if this instance has not connected.
|
|
106
112
|
"""
|
|
113
|
+
if not self._holds_shared:
|
|
114
|
+
raise RedisNotConnectedError("RedisConnection is not connected. Call connect() or use a with block.")
|
|
107
115
|
existing = RedisConnection._shared_clients.get(self._url)
|
|
108
116
|
if existing is None:
|
|
109
117
|
raise RedisNotConnectedError("RedisConnection is not connected. Call connect() or use a with block.")
|
|
@@ -135,11 +143,29 @@ class RedisConnection(Connectable):
|
|
|
135
143
|
if RedisConnection._shared_clients.get(self._url) is None:
|
|
136
144
|
self.info(f"Creating shared Redis client for {self._url}")
|
|
137
145
|
RedisConnection._shared_clients[self._url] = self._build(is_async=False)
|
|
146
|
+
RedisConnection._shared_refs[self._url] = 0
|
|
147
|
+
if not self._holds_shared:
|
|
148
|
+
RedisConnection._shared_refs[self._url] = RedisConnection._shared_refs.get(self._url, 0) + 1
|
|
149
|
+
self._holds_shared = True
|
|
138
150
|
|
|
139
151
|
def _disconnect(self) -> None:
|
|
140
152
|
"""
|
|
141
|
-
|
|
153
|
+
Release this instance's hold on the shared pool; close it when last.
|
|
142
154
|
"""
|
|
155
|
+
if not self._holds_shared:
|
|
156
|
+
return
|
|
157
|
+
self._holds_shared = False
|
|
158
|
+
refs = RedisConnection._shared_refs.get(self._url, 1) - 1
|
|
159
|
+
if refs <= 0:
|
|
160
|
+
client = RedisConnection._shared_clients.pop(self._url, None)
|
|
161
|
+
RedisConnection._shared_refs.pop(self._url, None)
|
|
162
|
+
if client is not None:
|
|
163
|
+
try:
|
|
164
|
+
client.close()
|
|
165
|
+
except Exception as exc:
|
|
166
|
+
self.debug(f"Error closing Redis client for {self._url}: {exc}")
|
|
167
|
+
else:
|
|
168
|
+
RedisConnection._shared_refs[self._url] = refs
|
|
143
169
|
|
|
144
170
|
async def _async_connect(self) -> None:
|
|
145
171
|
self._async_client = self._build(is_async=True)
|
|
@@ -166,6 +192,7 @@ class RedisConnection(Connectable):
|
|
|
166
192
|
except Exception as exc:
|
|
167
193
|
logger.debug("Error closing Redis client for %s: %s", url, exc)
|
|
168
194
|
del cls._shared_clients[url]
|
|
195
|
+
cls._shared_refs.clear()
|
|
169
196
|
|
|
170
197
|
# ------------------------------------------------------------------
|
|
171
198
|
# Sync API
|
|
@@ -175,9 +202,11 @@ class RedisConnection(Connectable):
|
|
|
175
202
|
return _decode(self.client.get(key))
|
|
176
203
|
|
|
177
204
|
def set(self, key: str, value: Any, **kwargs: Any) -> None:
|
|
205
|
+
self.require_writable()
|
|
178
206
|
self.client.set(key, _encode(value), **kwargs)
|
|
179
207
|
|
|
180
208
|
def delete(self, *keys: str) -> None:
|
|
209
|
+
self.require_writable()
|
|
181
210
|
self.client.delete(*keys)
|
|
182
211
|
|
|
183
212
|
def ping(self) -> bool:
|
|
@@ -187,15 +216,18 @@ class RedisConnection(Connectable):
|
|
|
187
216
|
return bool(self.client.exists(key))
|
|
188
217
|
|
|
189
218
|
def expire(self, key: str, seconds: int) -> None:
|
|
219
|
+
self.require_writable()
|
|
190
220
|
self.client.expire(key, seconds)
|
|
191
221
|
|
|
192
222
|
def ttl(self, key: str) -> int:
|
|
193
223
|
return self.client.ttl(key)
|
|
194
224
|
|
|
195
225
|
def incr(self, key: str, amount: int = 1) -> int:
|
|
226
|
+
self.require_writable()
|
|
196
227
|
return self.client.incr(key, amount)
|
|
197
228
|
|
|
198
229
|
def decr(self, key: str, amount: int = 1) -> int:
|
|
230
|
+
self.require_writable()
|
|
199
231
|
return self.client.decr(key, amount)
|
|
200
232
|
|
|
201
233
|
def keys(self, pattern: str = "*") -> list[Any]:
|
|
@@ -205,13 +237,19 @@ class RedisConnection(Connectable):
|
|
|
205
237
|
return _decode(self.client.hget(key, field))
|
|
206
238
|
|
|
207
239
|
def hset(self, key: str, field: str, value: Any) -> None:
|
|
240
|
+
self.require_writable()
|
|
208
241
|
self.client.hset(key, field, _encode(value))
|
|
209
242
|
|
|
210
243
|
def hdel(self, key: str, *fields: str) -> None:
|
|
244
|
+
self.require_writable()
|
|
211
245
|
self.client.hdel(key, *fields)
|
|
212
246
|
|
|
213
247
|
def publish(self, channel: str, message: Any) -> int:
|
|
214
|
-
|
|
248
|
+
self.require_writable()
|
|
249
|
+
# Pre-serialized pub/sub payloads (JSON strings, bytes) pass through;
|
|
250
|
+
# structured values are encoded the same way as cache writes.
|
|
251
|
+
payload = message if isinstance(message, (str, bytes, bytearray)) else _encode(message)
|
|
252
|
+
return self.client.publish(channel, payload)
|
|
215
253
|
|
|
216
254
|
def pubsub(self) -> Any:
|
|
217
255
|
return self.client.pubsub()
|
|
@@ -224,16 +262,21 @@ class RedisConnection(Connectable):
|
|
|
224
262
|
return _decode(await self.async_client.get(key))
|
|
225
263
|
|
|
226
264
|
async def aset(self, key: str, value: Any, **kwargs: Any) -> None:
|
|
265
|
+
self.require_writable()
|
|
227
266
|
await self.async_client.set(key, _encode(value), **kwargs)
|
|
228
267
|
|
|
229
268
|
async def asetex(self, key: str, seconds: int, value: Any) -> None:
|
|
269
|
+
self.require_writable()
|
|
230
270
|
await self.async_client.setex(key, seconds, _encode(value))
|
|
231
271
|
|
|
232
272
|
async def adelete(self, *keys: str) -> None:
|
|
273
|
+
self.require_writable()
|
|
233
274
|
await self.async_client.delete(*keys)
|
|
234
275
|
|
|
235
276
|
async def akeys(self, pattern: str = "*") -> list[Any]:
|
|
236
277
|
return await self.async_client.keys(pattern)
|
|
237
278
|
|
|
238
279
|
async def apublish(self, channel: str, message: Any) -> int:
|
|
239
|
-
|
|
280
|
+
self.require_writable()
|
|
281
|
+
payload = message if isinstance(message, (str, bytes, bytearray)) else _encode(message)
|
|
282
|
+
return await self.async_client.publish(channel, payload)
|
|
@@ -1,10 +1,39 @@
|
|
|
1
1
|
"""
|
|
2
|
-
SQL building blocks: a Connectable session,
|
|
2
|
+
SQL building blocks: a Connectable session, statement primitives and a base table.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
from corekit.connections.sql.connection import PaginatedResult, SQLConnection
|
|
6
|
-
from corekit.connections.sql.fields import JSONBField, PydanticJSON
|
|
6
|
+
from corekit.connections.sql.fields import JSONBField, JsonModelField, PydanticJSON
|
|
7
|
+
from corekit.connections.sql.operations import (
|
|
8
|
+
Conditional,
|
|
9
|
+
DdlOperation,
|
|
10
|
+
Delete,
|
|
11
|
+
DmlOperation,
|
|
12
|
+
DqlOperation,
|
|
13
|
+
Insert,
|
|
14
|
+
Operation,
|
|
15
|
+
Select,
|
|
16
|
+
Update,
|
|
17
|
+
)
|
|
7
18
|
from corekit.connections.sql.query import Query
|
|
8
|
-
from corekit.connections.sql.table import NamedTable
|
|
19
|
+
from corekit.connections.sql.table import NamedTable, TableExportItem
|
|
9
20
|
|
|
10
|
-
__all__ = [
|
|
21
|
+
__all__ = [
|
|
22
|
+
"Conditional",
|
|
23
|
+
"DdlOperation",
|
|
24
|
+
"Delete",
|
|
25
|
+
"DmlOperation",
|
|
26
|
+
"DqlOperation",
|
|
27
|
+
"Insert",
|
|
28
|
+
"JsonModelField",
|
|
29
|
+
"JSONBField",
|
|
30
|
+
"NamedTable",
|
|
31
|
+
"Operation",
|
|
32
|
+
"PaginatedResult",
|
|
33
|
+
"PydanticJSON",
|
|
34
|
+
"Query",
|
|
35
|
+
"Select",
|
|
36
|
+
"SQLConnection",
|
|
37
|
+
"TableExportItem",
|
|
38
|
+
"Update",
|
|
39
|
+
]
|
|
@@ -24,6 +24,7 @@ from sqlmodel import Session, SQLModel, create_engine, select
|
|
|
24
24
|
|
|
25
25
|
from corekit.config import get_settings
|
|
26
26
|
from corekit.connections import Connectable
|
|
27
|
+
from corekit.connections.sql.operations import DqlOperation, Operation, Select
|
|
27
28
|
from corekit.connections.sql.query import Query
|
|
28
29
|
from corekit.connections.sql.table import NamedTable
|
|
29
30
|
|
|
@@ -203,12 +204,14 @@ class SQLConnection(Connectable):
|
|
|
203
204
|
"""
|
|
204
205
|
Create any tables present in SQLModel metadata but missing from the database.
|
|
205
206
|
"""
|
|
207
|
+
self.require_writable()
|
|
206
208
|
SQLModel.metadata.create_all(_get_engine(self._connection_url))
|
|
207
209
|
|
|
208
210
|
def exec_ddl(self, sql: str) -> None:
|
|
209
211
|
"""
|
|
210
212
|
Run a raw DDL statement. Intended for migrations, not application code.
|
|
211
213
|
"""
|
|
214
|
+
self.require_writable()
|
|
212
215
|
self._start_session()
|
|
213
216
|
self.session.exec(text(sql))
|
|
214
217
|
self.session.commit()
|
|
@@ -221,6 +224,7 @@ class SQLConnection(Connectable):
|
|
|
221
224
|
"""
|
|
222
225
|
Insert new rows.
|
|
223
226
|
"""
|
|
227
|
+
self.require_writable()
|
|
224
228
|
self._start_session()
|
|
225
229
|
self.session.add_all(items)
|
|
226
230
|
self.session.commit()
|
|
@@ -229,6 +233,7 @@ class SQLConnection(Connectable):
|
|
|
229
233
|
"""
|
|
230
234
|
Insert or update rows. Slower than insert, but tolerates existing ids.
|
|
231
235
|
"""
|
|
236
|
+
self.require_writable()
|
|
232
237
|
self._start_session()
|
|
233
238
|
for item in items:
|
|
234
239
|
self.session.merge(item)
|
|
@@ -238,6 +243,7 @@ class SQLConnection(Connectable):
|
|
|
238
243
|
"""
|
|
239
244
|
Delete a row by id.
|
|
240
245
|
"""
|
|
246
|
+
self.require_writable()
|
|
241
247
|
item = self.fetch_one_by_id(table, identifier)
|
|
242
248
|
if item is None:
|
|
243
249
|
return
|
|
@@ -295,8 +301,12 @@ class SQLConnection(Connectable):
|
|
|
295
301
|
def fetch_one_by_condition(self, table: type[NamedTable], key: Any, value: Any) -> NamedTable | None:
|
|
296
302
|
"""
|
|
297
303
|
The first row where a column equals a value.
|
|
304
|
+
|
|
305
|
+
``key`` may be a column attribute (``Person.city``) or a column name
|
|
306
|
+
string resolved against ``table``.
|
|
298
307
|
"""
|
|
299
|
-
|
|
308
|
+
column = getattr(table, key) if isinstance(key, str) else key
|
|
309
|
+
return self.first(select(table).where(column == value))
|
|
300
310
|
|
|
301
311
|
def exists(self, table: type[NamedTable], identifier: str) -> bool:
|
|
302
312
|
"""
|
|
@@ -320,6 +330,36 @@ class SQLConnection(Connectable):
|
|
|
320
330
|
"""
|
|
321
331
|
return list(self._exec(query.build()))
|
|
322
332
|
|
|
333
|
+
def delete_many_by_query(self, query: Query) -> None:
|
|
334
|
+
"""
|
|
335
|
+
Delete every row matching a Query.
|
|
336
|
+
"""
|
|
337
|
+
self.require_writable()
|
|
338
|
+
self._start_session()
|
|
339
|
+
for item in self.fetch_many_by_query(query):
|
|
340
|
+
self.session.delete(item)
|
|
341
|
+
self.session.commit()
|
|
342
|
+
|
|
343
|
+
def execute(self, operation: Operation) -> Any:
|
|
344
|
+
"""
|
|
345
|
+
Run an operation, committing if it writes.
|
|
346
|
+
|
|
347
|
+
A read returns its rows; a write returns the result, whose
|
|
348
|
+
``rowcount`` reports how many rows it touched.
|
|
349
|
+
"""
|
|
350
|
+
if operation.writes:
|
|
351
|
+
self.require_writable()
|
|
352
|
+
result = self._exec(operation.build())
|
|
353
|
+
if operation.writes:
|
|
354
|
+
self.session.commit()
|
|
355
|
+
return result
|
|
356
|
+
|
|
357
|
+
def fetch(self, operation: DqlOperation) -> list[NamedTable]:
|
|
358
|
+
"""
|
|
359
|
+
Every row an operation selects.
|
|
360
|
+
"""
|
|
361
|
+
return list(self.execute(operation))
|
|
362
|
+
|
|
323
363
|
def fetch_one_by_query(self, query: Query) -> NamedTable | None:
|
|
324
364
|
"""
|
|
325
365
|
The first row matching a Query.
|
|
@@ -333,10 +373,23 @@ class SQLConnection(Connectable):
|
|
|
333
373
|
self._start_session()
|
|
334
374
|
return self.session.exec(select(func.count()).select_from(table)).one()
|
|
335
375
|
|
|
336
|
-
def paginate(
|
|
376
|
+
def paginate(
|
|
377
|
+
self,
|
|
378
|
+
table: type[NamedTable],
|
|
379
|
+
offset: int = 0,
|
|
380
|
+
limit: int = 50,
|
|
381
|
+
*,
|
|
382
|
+
order_by: Any = None,
|
|
383
|
+
desc: bool = False,
|
|
384
|
+
) -> PaginatedResult:
|
|
337
385
|
"""
|
|
338
386
|
A page of rows, together with the total row count.
|
|
387
|
+
|
|
388
|
+
Pages are ordered: pass ``order_by`` for a column, or omit it to order
|
|
389
|
+
by ``table.id`` so offsets are stable.
|
|
339
390
|
"""
|
|
340
391
|
total = self.count(table)
|
|
341
|
-
|
|
392
|
+
query = Select(table=table).offset(offset).limit(limit)
|
|
393
|
+
query.order_by(order_by if order_by is not None else table.id, desc=desc)
|
|
394
|
+
rows = self.fetch(query)
|
|
342
395
|
return PaginatedResult(rows=rows, total=total)
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
Custom SQL column types.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
-
from corekit.connections.sql.fields.jsonb import JSONBField, PydanticJSON
|
|
5
|
+
from corekit.connections.sql.fields.jsonb import JSONBField, JsonModelField, PydanticJSON
|
|
6
6
|
|
|
7
|
-
__all__ = ["JSONBField", "PydanticJSON"]
|
|
7
|
+
__all__ = ["JsonModelField", "JSONBField", "PydanticJSON"]
|
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Columns that store a Pydantic model as JSON.
|
|
3
3
|
|
|
4
|
-
``
|
|
5
|
-
structured data without a separate table and without hand-written encoding
|
|
6
|
-
every call site::
|
|
4
|
+
``JsonModelField`` round-trips a model through a JSON column, so a table can
|
|
5
|
+
hold structured data without a separate table and without hand-written encoding
|
|
6
|
+
at every call site::
|
|
7
7
|
|
|
8
8
|
class Profile(BaseModel):
|
|
9
9
|
theme: str = "dark"
|
|
10
10
|
|
|
11
11
|
class User(NamedTable, table=True):
|
|
12
12
|
id: str = Field(primary_key=True)
|
|
13
|
-
profile: Profile =
|
|
13
|
+
profile: Profile = JsonModelField(Profile)
|
|
14
14
|
|
|
15
15
|
Reading ``user.profile`` gives a ``Profile``, not a dict.
|
|
16
|
+
|
|
17
|
+
The SQLAlchemy ``impl`` is ``JSON``, not PostgreSQL ``JSONB``. The historical
|
|
18
|
+
``JSONBField`` name is kept as an alias.
|
|
16
19
|
"""
|
|
17
20
|
|
|
18
21
|
from typing import Any
|
|
@@ -22,7 +25,7 @@ from sqlalchemy import Column
|
|
|
22
25
|
from sqlalchemy.types import JSON, TypeDecorator
|
|
23
26
|
from sqlmodel import Field
|
|
24
27
|
|
|
25
|
-
__all__ = ["JSONBField", "PydanticJSON"]
|
|
28
|
+
__all__ = ["JsonModelField", "JSONBField", "PydanticJSON"]
|
|
26
29
|
|
|
27
30
|
|
|
28
31
|
class PydanticJSON(TypeDecorator):
|
|
@@ -60,8 +63,12 @@ class PydanticJSON(TypeDecorator):
|
|
|
60
63
|
return self.pydantic_model(**value)
|
|
61
64
|
|
|
62
65
|
|
|
63
|
-
def
|
|
66
|
+
def JsonModelField(pydantic_model: type[BaseModel], **kwargs: Any) -> Any: # noqa: N802 - reads as a field constructor
|
|
64
67
|
"""
|
|
65
68
|
Declare a column holding ``pydantic_model`` as JSON.
|
|
66
69
|
"""
|
|
67
70
|
return Field(sa_column=Column(PydanticJSON(pydantic_model)), **kwargs)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
#: Historical name. The column type is SQLAlchemy ``JSON``, not PostgreSQL JSONB.
|
|
74
|
+
JSONBField = JsonModelField
|
|
@@ -8,7 +8,7 @@ Define a migration by subclassing ``Migration`` and listing its operations::
|
|
|
8
8
|
name = "user_is_active"
|
|
9
9
|
description = "Adds the is_active flag to users."
|
|
10
10
|
|
|
11
|
-
def operations(self) -> list[
|
|
11
|
+
def operations(self) -> list[MigrationOperation]:
|
|
12
12
|
return [AddColumn(table="user", column="is_active", dtype="BOOLEAN", default="true")]
|
|
13
13
|
|
|
14
14
|
Collect them from a package and run them::
|
|
@@ -24,13 +24,15 @@ from corekit.connections.sql.migration.base import Migration
|
|
|
24
24
|
from corekit.connections.sql.migration.operations import (
|
|
25
25
|
AddColumn,
|
|
26
26
|
AlterColumnType,
|
|
27
|
+
ColumnSpec,
|
|
27
28
|
CreateIndex,
|
|
28
|
-
|
|
29
|
+
CreateTable,
|
|
30
|
+
DataMigrationOperation,
|
|
29
31
|
DropColumn,
|
|
30
32
|
DropColumnDefault,
|
|
31
33
|
DropIndex,
|
|
32
34
|
DropTable,
|
|
33
|
-
|
|
35
|
+
MigrationOperation,
|
|
34
36
|
RenameColumn,
|
|
35
37
|
RenameTable,
|
|
36
38
|
SetColumnDefault,
|
|
@@ -41,8 +43,10 @@ from corekit.connections.sql.migration.table import SchemaMigration
|
|
|
41
43
|
__all__ = [
|
|
42
44
|
"AddColumn",
|
|
43
45
|
"AlterColumnType",
|
|
46
|
+
"ColumnSpec",
|
|
44
47
|
"CreateIndex",
|
|
45
|
-
"
|
|
48
|
+
"CreateTable",
|
|
49
|
+
"DataMigrationOperation",
|
|
46
50
|
"DropColumn",
|
|
47
51
|
"DropColumnDefault",
|
|
48
52
|
"DropIndex",
|
|
@@ -50,7 +54,7 @@ __all__ = [
|
|
|
50
54
|
"Migration",
|
|
51
55
|
"MigrationChecksumError",
|
|
52
56
|
"MigrationRegistry",
|
|
53
|
-
"
|
|
57
|
+
"MigrationOperation",
|
|
54
58
|
"RenameColumn",
|
|
55
59
|
"RenameTable",
|
|
56
60
|
"SchemaMigration",
|
|
@@ -5,7 +5,7 @@ A Migration is defined by:
|
|
|
5
5
|
- version: unique positive integer, determines execution order
|
|
6
6
|
- name: short human-readable label (used in logs and the DB table)
|
|
7
7
|
- description: longer explanation of what and why
|
|
8
|
-
- operations: ordered list of
|
|
8
|
+
- operations: ordered list of MigrationOperation instances to execute
|
|
9
9
|
|
|
10
10
|
The checksum is computed at class definition time from the canonical
|
|
11
11
|
representations of all operations. It is stored in the schema_migration
|
|
@@ -17,7 +17,7 @@ and will cause a hard crash.
|
|
|
17
17
|
import hashlib
|
|
18
18
|
from abc import ABC, abstractmethod
|
|
19
19
|
|
|
20
|
-
from corekit.connections.sql.migration.operations import
|
|
20
|
+
from corekit.connections.sql.migration.operations import MigrationOperation
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
class Migration(ABC):
|
|
@@ -26,7 +26,7 @@ class Migration(ABC):
|
|
|
26
26
|
description: str
|
|
27
27
|
|
|
28
28
|
@abstractmethod
|
|
29
|
-
def operations(self) -> list[
|
|
29
|
+
def operations(self) -> list[MigrationOperation]:
|
|
30
30
|
raise NotImplementedError
|
|
31
31
|
|
|
32
32
|
@property
|