python-corekit 0.2.0__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/application.py +47 -9
- corekit/api/lifespan.py +26 -3
- corekit/concurrency/__init__.py +2 -2
- corekit/concurrency/decorators.py +32 -5
- corekit/concurrency/thread_local.py +2 -2
- corekit/concurrency/worker.py +9 -0
- 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 +2 -1
- corekit/connections/sql/connection.py +39 -5
- corekit/connections/sql/fields/__init__.py +2 -2
- corekit/connections/sql/fields/jsonb.py +13 -6
- corekit/connections/sql/migration/__init__.py +4 -0
- corekit/connections/sql/migration/operations.py +69 -2
- corekit/connections/sql/operations/base.py +11 -2
- corekit/connections/sql/operations/statements.py +25 -5
- corekit/connections/sql/table.py +7 -29
- corekit/crypto/__init__.py +3 -1
- corekit/crypto/constants.py +2 -2
- corekit/crypto/hasher.py +9 -4
- corekit/data/dataset.py +8 -2
- corekit/data/expressions/__init__.py +3 -3
- corekit/data/expressions/comparison.py +19 -80
- corekit/data/expressions/expression.py +0 -32
- corekit/data/expressions/operator.py +13 -28
- corekit/data/stats.py +3 -0
- corekit/decorators/exception_handling.py +36 -8
- corekit/docker/watchdog.py +50 -31
- corekit/etl/__init__.py +2 -1
- corekit/etl/connection.py +14 -12
- 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 +24 -11
- corekit/exceptions/__init__.py +24 -9
- 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 +43 -5
- 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/runner.py +12 -1
- corekit/jobs/task.py +23 -2
- corekit/log_monitor/models.py +8 -2
- corekit/log_monitor/service.py +77 -38
- corekit/notifications/base.py +18 -10
- corekit/observability/__init__.py +9 -2
- corekit/observability/benchmarkable.py +23 -5
- corekit/observability/loggable.py +21 -0
- corekit/observability/request_context.py +55 -2
- corekit/observability/timing/timer.py +4 -2
- corekit/registry/__init__.py +2 -2
- corekit/registry/registry.py +55 -14
- corekit/schemas/enum.py +22 -1
- corekit/schemas/types.py +6 -1
- corekit/serialization/__init__.py +2 -0
- corekit/serialization/pickle_file.py +61 -0
- corekit/serialization/serializable.py +22 -2
- corekit/serialization/serializer.py +9 -2
- corekit/utils/collections.py +22 -13
- corekit/utils/payload.py +12 -0
- {python_corekit-0.2.0.dist-info → python_corekit-0.3.0.dist-info}/METADATA +7 -7
- 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.2.0.dist-info/RECORD +0 -143
- {python_corekit-0.2.0.dist-info → python_corekit-0.3.0.dist-info}/WHEEL +0 -0
- {python_corekit-0.2.0.dist-info → python_corekit-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {python_corekit-0.2.0.dist-info → python_corekit-0.3.0.dist-info}/top_level.txt +0 -0
|
@@ -3,7 +3,7 @@ SQL building blocks: a Connectable session, statement primitives and a base tabl
|
|
|
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
7
|
from corekit.connections.sql.operations import (
|
|
8
8
|
Conditional,
|
|
9
9
|
DdlOperation,
|
|
@@ -25,6 +25,7 @@ __all__ = [
|
|
|
25
25
|
"DmlOperation",
|
|
26
26
|
"DqlOperation",
|
|
27
27
|
"Insert",
|
|
28
|
+
"JsonModelField",
|
|
28
29
|
"JSONBField",
|
|
29
30
|
"NamedTable",
|
|
30
31
|
"Operation",
|
|
@@ -24,7 +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
|
|
27
|
+
from corekit.connections.sql.operations import DqlOperation, Operation, Select
|
|
28
28
|
from corekit.connections.sql.query import Query
|
|
29
29
|
from corekit.connections.sql.table import NamedTable
|
|
30
30
|
|
|
@@ -204,12 +204,14 @@ class SQLConnection(Connectable):
|
|
|
204
204
|
"""
|
|
205
205
|
Create any tables present in SQLModel metadata but missing from the database.
|
|
206
206
|
"""
|
|
207
|
+
self.require_writable()
|
|
207
208
|
SQLModel.metadata.create_all(_get_engine(self._connection_url))
|
|
208
209
|
|
|
209
210
|
def exec_ddl(self, sql: str) -> None:
|
|
210
211
|
"""
|
|
211
212
|
Run a raw DDL statement. Intended for migrations, not application code.
|
|
212
213
|
"""
|
|
214
|
+
self.require_writable()
|
|
213
215
|
self._start_session()
|
|
214
216
|
self.session.exec(text(sql))
|
|
215
217
|
self.session.commit()
|
|
@@ -222,6 +224,7 @@ class SQLConnection(Connectable):
|
|
|
222
224
|
"""
|
|
223
225
|
Insert new rows.
|
|
224
226
|
"""
|
|
227
|
+
self.require_writable()
|
|
225
228
|
self._start_session()
|
|
226
229
|
self.session.add_all(items)
|
|
227
230
|
self.session.commit()
|
|
@@ -230,6 +233,7 @@ class SQLConnection(Connectable):
|
|
|
230
233
|
"""
|
|
231
234
|
Insert or update rows. Slower than insert, but tolerates existing ids.
|
|
232
235
|
"""
|
|
236
|
+
self.require_writable()
|
|
233
237
|
self._start_session()
|
|
234
238
|
for item in items:
|
|
235
239
|
self.session.merge(item)
|
|
@@ -239,6 +243,7 @@ class SQLConnection(Connectable):
|
|
|
239
243
|
"""
|
|
240
244
|
Delete a row by id.
|
|
241
245
|
"""
|
|
246
|
+
self.require_writable()
|
|
242
247
|
item = self.fetch_one_by_id(table, identifier)
|
|
243
248
|
if item is None:
|
|
244
249
|
return
|
|
@@ -296,8 +301,12 @@ class SQLConnection(Connectable):
|
|
|
296
301
|
def fetch_one_by_condition(self, table: type[NamedTable], key: Any, value: Any) -> NamedTable | None:
|
|
297
302
|
"""
|
|
298
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``.
|
|
299
307
|
"""
|
|
300
|
-
|
|
308
|
+
column = getattr(table, key) if isinstance(key, str) else key
|
|
309
|
+
return self.first(select(table).where(column == value))
|
|
301
310
|
|
|
302
311
|
def exists(self, table: type[NamedTable], identifier: str) -> bool:
|
|
303
312
|
"""
|
|
@@ -321,6 +330,16 @@ class SQLConnection(Connectable):
|
|
|
321
330
|
"""
|
|
322
331
|
return list(self._exec(query.build()))
|
|
323
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
|
+
|
|
324
343
|
def execute(self, operation: Operation) -> Any:
|
|
325
344
|
"""
|
|
326
345
|
Run an operation, committing if it writes.
|
|
@@ -328,8 +347,10 @@ class SQLConnection(Connectable):
|
|
|
328
347
|
A read returns its rows; a write returns the result, whose
|
|
329
348
|
``rowcount`` reports how many rows it touched.
|
|
330
349
|
"""
|
|
350
|
+
if operation.writes:
|
|
351
|
+
self.require_writable()
|
|
331
352
|
result = self._exec(operation.build())
|
|
332
|
-
if
|
|
353
|
+
if operation.writes:
|
|
333
354
|
self.session.commit()
|
|
334
355
|
return result
|
|
335
356
|
|
|
@@ -352,10 +373,23 @@ class SQLConnection(Connectable):
|
|
|
352
373
|
self._start_session()
|
|
353
374
|
return self.session.exec(select(func.count()).select_from(table)).one()
|
|
354
375
|
|
|
355
|
-
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:
|
|
356
385
|
"""
|
|
357
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.
|
|
358
390
|
"""
|
|
359
391
|
total = self.count(table)
|
|
360
|
-
|
|
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)
|
|
361
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
|
|
@@ -24,7 +24,9 @@ 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,
|
|
29
|
+
CreateTable,
|
|
28
30
|
DataMigrationOperation,
|
|
29
31
|
DropColumn,
|
|
30
32
|
DropColumnDefault,
|
|
@@ -41,7 +43,9 @@ from corekit.connections.sql.migration.table import SchemaMigration
|
|
|
41
43
|
__all__ = [
|
|
42
44
|
"AddColumn",
|
|
43
45
|
"AlterColumnType",
|
|
46
|
+
"ColumnSpec",
|
|
44
47
|
"CreateIndex",
|
|
48
|
+
"CreateTable",
|
|
45
49
|
"DataMigrationOperation",
|
|
46
50
|
"DropColumn",
|
|
47
51
|
"DropColumnDefault",
|
|
@@ -360,6 +360,72 @@ class DropIndex(MigrationOperation):
|
|
|
360
360
|
# ---------------------------------------------------------------------------
|
|
361
361
|
|
|
362
362
|
|
|
363
|
+
@dataclass(frozen=True)
|
|
364
|
+
class ColumnSpec:
|
|
365
|
+
"""
|
|
366
|
+
One column in a ``CreateTable`` statement.
|
|
367
|
+
|
|
368
|
+
Types and defaults are SQL text, as on ``AddColumn``, not Python values.
|
|
369
|
+
Write ``default="FALSE"`` or ``default="'red'"``, not ``False`` or
|
|
370
|
+
``"red"``.
|
|
371
|
+
"""
|
|
372
|
+
|
|
373
|
+
name: str
|
|
374
|
+
dtype: str
|
|
375
|
+
nullable: bool = True
|
|
376
|
+
primary_key: bool = False
|
|
377
|
+
default: str | None = None
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
@dataclass
|
|
381
|
+
class CreateTable(MigrationOperation):
|
|
382
|
+
"""
|
|
383
|
+
Create a table from an explicit column list.
|
|
384
|
+
|
|
385
|
+
This is a new operation. It does not change the SQL or checksum of
|
|
386
|
+
``AddColumn`` and the other existing operations. Listing the columns here,
|
|
387
|
+
rather than reading a model at run time, is what keeps an applied
|
|
388
|
+
migration's checksum stable when that model later gains a field.
|
|
389
|
+
|
|
390
|
+
``if_not_exists`` defaults to True. Both SQLite and Postgres accept
|
|
391
|
+
``CREATE TABLE IF NOT EXISTS``.
|
|
392
|
+
"""
|
|
393
|
+
|
|
394
|
+
table: type[NamedTable] | str
|
|
395
|
+
columns: list[ColumnSpec]
|
|
396
|
+
if_not_exists: bool = True
|
|
397
|
+
|
|
398
|
+
def to_sql(self) -> str:
|
|
399
|
+
"""
|
|
400
|
+
Example: CREATE TABLE [IF NOT EXISTS] <table_name> (<column> <dtype> ...)
|
|
401
|
+
"""
|
|
402
|
+
if not self.columns:
|
|
403
|
+
raise ValueError("CreateTable requires at least one column")
|
|
404
|
+
|
|
405
|
+
definitions = ", ".join(self._column_sql(column) for column in self.columns)
|
|
406
|
+
exists = "IF NOT EXISTS " if self.if_not_exists else ""
|
|
407
|
+
return f"CREATE TABLE {exists}{self.table_name} ({definitions})"
|
|
408
|
+
|
|
409
|
+
@staticmethod
|
|
410
|
+
def _column_sql(column: ColumnSpec) -> str:
|
|
411
|
+
parts = [column.name, column.dtype]
|
|
412
|
+
if column.primary_key:
|
|
413
|
+
parts.append("PRIMARY KEY")
|
|
414
|
+
if not column.nullable:
|
|
415
|
+
parts.append("NOT NULL")
|
|
416
|
+
if column.default is not None:
|
|
417
|
+
parts.append(f"DEFAULT {column.default}")
|
|
418
|
+
return " ".join(parts)
|
|
419
|
+
|
|
420
|
+
def canonical(self) -> str:
|
|
421
|
+
rendered = ", ".join(
|
|
422
|
+
f"{column.name}:{column.dtype}:nullable={column.nullable}:"
|
|
423
|
+
f"primary_key={column.primary_key}:default={column.default}"
|
|
424
|
+
for column in self.columns
|
|
425
|
+
)
|
|
426
|
+
return f"CreateTable(table={self.table_name}, columns=[{rendered}], if_not_exists={self.if_not_exists})"
|
|
427
|
+
|
|
428
|
+
|
|
363
429
|
@dataclass
|
|
364
430
|
class DropTable(MigrationOperation):
|
|
365
431
|
table: type[NamedTable] | str
|
|
@@ -409,8 +475,9 @@ class DataMigrationOperation(MigrationOperation):
|
|
|
409
475
|
Executes a Python function for data migrations (seeding, transformations).
|
|
410
476
|
|
|
411
477
|
The function receives a SQLConnection and should use ORM methods to
|
|
412
|
-
read/write data. The
|
|
413
|
-
|
|
478
|
+
read/write data. The checksum is the ``name`` only, not the function.
|
|
479
|
+
Editing the callable without a new name is invisible to history, so a
|
|
480
|
+
changed function requires a new name. Choose a stable, descriptive one.
|
|
414
481
|
|
|
415
482
|
Example:
|
|
416
483
|
DataMigrationOperation(
|
|
@@ -3,7 +3,7 @@ Statement primitives: the shapes a database operation can take.
|
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
from abc import ABC, abstractmethod
|
|
6
|
-
from typing import Any
|
|
6
|
+
from typing import Any, ClassVar
|
|
7
7
|
|
|
8
8
|
from pydantic import BaseModel, Field
|
|
9
9
|
|
|
@@ -24,6 +24,10 @@ class Operation(BaseModel, ABC):
|
|
|
24
24
|
|
|
25
25
|
model_config = {"arbitrary_types_allowed": True}
|
|
26
26
|
|
|
27
|
+
#: True when executing this operation changes rows and must be committed.
|
|
28
|
+
#: Conditional writes (update, delete) and insert both set this; reads do not.
|
|
29
|
+
writes: ClassVar[bool] = False
|
|
30
|
+
|
|
27
31
|
table: type[NamedTable]
|
|
28
32
|
|
|
29
33
|
@abstractmethod
|
|
@@ -79,9 +83,14 @@ class DqlOperation(Conditional, ABC):
|
|
|
79
83
|
|
|
80
84
|
class DmlOperation(Conditional, ABC):
|
|
81
85
|
"""
|
|
82
|
-
A write against rows:
|
|
86
|
+
A conditional write against rows: update or delete.
|
|
87
|
+
|
|
88
|
+
Insert is also a write, but it has no ``where``, so it is not this class.
|
|
89
|
+
Both set ``writes`` so a connection commits them.
|
|
83
90
|
"""
|
|
84
91
|
|
|
92
|
+
writes: ClassVar[bool] = True
|
|
93
|
+
|
|
85
94
|
|
|
86
95
|
class DdlOperation(Operation, ABC):
|
|
87
96
|
"""
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
The statements an application runs: select, insert, update and delete.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
-
from typing import Any
|
|
5
|
+
from typing import Any, ClassVar
|
|
6
6
|
|
|
7
7
|
from pydantic import Field
|
|
8
8
|
from sqlmodel import delete as _delete
|
|
@@ -18,7 +18,7 @@ __all__ = ["Delete", "Insert", "Select", "Update"]
|
|
|
18
18
|
|
|
19
19
|
class Select(DqlOperation):
|
|
20
20
|
"""
|
|
21
|
-
Read rows, optionally ordered and capped.
|
|
21
|
+
Read rows, optionally ordered, offset and capped.
|
|
22
22
|
|
|
23
23
|
Select(table=User).where(User.age >= 18).order_by(User.name).limit(10)
|
|
24
24
|
|
|
@@ -26,11 +26,14 @@ class Select(DqlOperation):
|
|
|
26
26
|
predicate language ``Dataset`` filters with::
|
|
27
27
|
|
|
28
28
|
Select(table=User).where(Field("age") >= 18)
|
|
29
|
+
|
|
30
|
+
``limit(-1)`` means no cap. ``limit(0)`` returns no rows.
|
|
29
31
|
"""
|
|
30
32
|
|
|
31
33
|
order_by_field: Any = None
|
|
32
34
|
order_by_desc: bool = False
|
|
33
35
|
limit_count: int = -1
|
|
36
|
+
offset_count: int = 0
|
|
34
37
|
|
|
35
38
|
def order_by(self, field: Any, desc: bool = False) -> "Select":
|
|
36
39
|
"""
|
|
@@ -42,11 +45,20 @@ class Select(DqlOperation):
|
|
|
42
45
|
|
|
43
46
|
def limit(self, limit: int) -> "Select":
|
|
44
47
|
"""
|
|
45
|
-
Cap the number of rows returned.
|
|
48
|
+
Cap the number of rows returned.
|
|
49
|
+
|
|
50
|
+
``-1`` means no limit. ``0`` means zero rows.
|
|
46
51
|
"""
|
|
47
52
|
self.limit_count = limit
|
|
48
53
|
return self
|
|
49
54
|
|
|
55
|
+
def offset(self, offset: int) -> "Select":
|
|
56
|
+
"""
|
|
57
|
+
Skip the first ``offset`` rows.
|
|
58
|
+
"""
|
|
59
|
+
self.offset_count = offset
|
|
60
|
+
return self
|
|
61
|
+
|
|
50
62
|
def build(self) -> Any:
|
|
51
63
|
"""
|
|
52
64
|
Produce the select statement.
|
|
@@ -59,7 +71,10 @@ class Select(DqlOperation):
|
|
|
59
71
|
order_by_field = _desc(order_by_field)
|
|
60
72
|
statement = statement.order_by(order_by_field)
|
|
61
73
|
|
|
62
|
-
if self.
|
|
74
|
+
if self.offset_count > 0:
|
|
75
|
+
statement = statement.offset(self.offset_count)
|
|
76
|
+
|
|
77
|
+
if self.limit_count >= 0:
|
|
63
78
|
statement = statement.limit(self.limit_count)
|
|
64
79
|
|
|
65
80
|
return statement
|
|
@@ -121,7 +136,10 @@ class Update(DmlOperation):
|
|
|
121
136
|
|
|
122
137
|
class Insert(Operation):
|
|
123
138
|
"""
|
|
124
|
-
Add rows.
|
|
139
|
+
Add rows. A write, so ``SQLConnection.execute`` commits it.
|
|
140
|
+
|
|
141
|
+
Not a ``DmlOperation``: an insert has no rows to narrow, so it does not
|
|
142
|
+
take ``where``.
|
|
125
143
|
|
|
126
144
|
Insert(table=User, rows=[{"name": "Ada"}, {"name": "Bob"}])
|
|
127
145
|
|
|
@@ -129,6 +147,8 @@ class Insert(Operation):
|
|
|
129
147
|
statement. To add model instances, use ``SQLConnection.insert``.
|
|
130
148
|
"""
|
|
131
149
|
|
|
150
|
+
writes: ClassVar[bool] = True
|
|
151
|
+
|
|
132
152
|
rows: list[dict[str, Any]] = Field(default_factory=list)
|
|
133
153
|
|
|
134
154
|
def add(self, **values: Any) -> "Insert":
|
corekit/connections/sql/table.py
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
Base table type.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
-
import json
|
|
6
5
|
import logging
|
|
7
6
|
from typing import Any, ClassVar
|
|
8
7
|
|
|
@@ -33,6 +32,10 @@ class NamedTable(SQLModel):
|
|
|
33
32
|
Subclasses register themselves by name, so a table class can be resolved
|
|
34
33
|
from a string -- useful for imports, exports and admin tooling that work
|
|
35
34
|
with table names rather than imported classes.
|
|
35
|
+
|
|
36
|
+
``table_name`` is the database table name (``__tablename__``). Use
|
|
37
|
+
``get_display_name`` for a human-readable label. Structured JSON columns
|
|
38
|
+
belong on ``JsonModelField`` / ``PydanticJSON``, not ad-hoc encode helpers.
|
|
36
39
|
"""
|
|
37
40
|
|
|
38
41
|
__registry__: SmartRegistry = SmartRegistry()
|
|
@@ -65,9 +68,9 @@ class NamedTable(SQLModel):
|
|
|
65
68
|
@property
|
|
66
69
|
def table_name(self) -> str:
|
|
67
70
|
"""
|
|
68
|
-
The
|
|
71
|
+
The database table name for this row (``__tablename__``).
|
|
69
72
|
"""
|
|
70
|
-
return type(self).
|
|
73
|
+
return type(self).__tablename__
|
|
71
74
|
|
|
72
75
|
@classmethod
|
|
73
76
|
def get_display_name(cls) -> str:
|
|
@@ -92,31 +95,6 @@ class NamedTable(SQLModel):
|
|
|
92
95
|
|
|
93
96
|
def export(self) -> TableExportItem:
|
|
94
97
|
"""
|
|
95
|
-
This row as a portable record, tagged with its table.
|
|
98
|
+
This row as a portable record, tagged with its database table name.
|
|
96
99
|
"""
|
|
97
100
|
return TableExportItem(table_name=self.table_name, data=self.model_dump())
|
|
98
|
-
|
|
99
|
-
@staticmethod
|
|
100
|
-
def _serialize(value: Any) -> str:
|
|
101
|
-
return json.dumps(value)
|
|
102
|
-
|
|
103
|
-
@staticmethod
|
|
104
|
-
def _deserialize(value: str) -> Any:
|
|
105
|
-
try:
|
|
106
|
-
return json.loads(value)
|
|
107
|
-
except (TypeError, ValueError):
|
|
108
|
-
logger.error(f"Unable to deserialize value: {value!r}")
|
|
109
|
-
raise
|
|
110
|
-
|
|
111
|
-
def serialize(self, values: dict[str, Any], key: str) -> dict[str, Any]:
|
|
112
|
-
"""
|
|
113
|
-
Replace ``values[key]`` with its JSON encoding.
|
|
114
|
-
"""
|
|
115
|
-
values[key] = self._serialize(values.get(key))
|
|
116
|
-
return values
|
|
117
|
-
|
|
118
|
-
def deserialize(self, key: str) -> Any:
|
|
119
|
-
"""
|
|
120
|
-
Decode the JSON stored in the named field.
|
|
121
|
-
"""
|
|
122
|
-
return self._deserialize(getattr(self, key))
|
corekit/crypto/__init__.py
CHANGED
corekit/crypto/constants.py
CHANGED
|
@@ -3,5 +3,5 @@ EMPTY_HASH = ""
|
|
|
3
3
|
|
|
4
4
|
# There is deliberately no default salt. A salt shipped with a library is shared
|
|
5
5
|
# by every install, which defeats the purpose of salting. Supply one through
|
|
6
|
-
# configuration --
|
|
7
|
-
#
|
|
6
|
+
# configuration -- COREKIT_CRYPTO__SALT, or [crypto] salt in a config file --
|
|
7
|
+
# or pass salt= to Hasher.
|
corekit/crypto/hasher.py
CHANGED
|
@@ -4,22 +4,27 @@ from typing import Any
|
|
|
4
4
|
from corekit.config import get_settings
|
|
5
5
|
from corekit.crypto.constants import EMPTY_HASH, HASH_JOINER
|
|
6
6
|
from corekit.crypto.enum import SaltMethod
|
|
7
|
+
from corekit.exceptions import InternalCoreException, Retryability
|
|
7
8
|
from corekit.observability.loggable import Loggable
|
|
8
9
|
|
|
9
10
|
|
|
10
|
-
class MissingSaltError(
|
|
11
|
+
class MissingSaltError(InternalCoreException):
|
|
11
12
|
"""
|
|
12
13
|
Raised when hashing is attempted without a configured salt.
|
|
13
14
|
"""
|
|
14
15
|
|
|
16
|
+
def __init__(self, message: str, *, error: str | None = None) -> None:
|
|
17
|
+
super().__init__(message, retryable=Retryability.NON_RETRYABLE, error=error)
|
|
18
|
+
|
|
15
19
|
|
|
16
20
|
class Hasher(Loggable):
|
|
17
21
|
"""
|
|
18
22
|
Salted SHA-256 hashing.
|
|
19
23
|
|
|
20
24
|
The salt has no default. Supply it explicitly, or configure it via
|
|
21
|
-
``
|
|
22
|
-
is deferred until first use so that importing this module
|
|
25
|
+
``COREKIT_CRYPTO__SALT`` or ``[crypto] salt`` in a corekit config file.
|
|
26
|
+
Resolution is deferred until first use so that importing this module
|
|
27
|
+
never fails.
|
|
23
28
|
"""
|
|
24
29
|
|
|
25
30
|
def __init__(
|
|
@@ -41,7 +46,7 @@ class Hasher(Loggable):
|
|
|
41
46
|
if not salt:
|
|
42
47
|
raise MissingSaltError(
|
|
43
48
|
"No crypto salt configured. Pass salt=... to Hasher, set "
|
|
44
|
-
"
|
|
49
|
+
"COREKIT_CRYPTO__SALT, or add salt under [crypto] in your corekit config. "
|
|
45
50
|
'Generate one with: python -c "import secrets; print(secrets.token_hex(32))"'
|
|
46
51
|
)
|
|
47
52
|
return salt
|
corekit/data/dataset.py
CHANGED
|
@@ -268,8 +268,14 @@ class Dataset:
|
|
|
268
268
|
def at(self, idx: int) -> Any:
|
|
269
269
|
return self._records[idx]
|
|
270
270
|
|
|
271
|
-
def get_record(self, record_id: Any) -> Any:
|
|
272
|
-
|
|
271
|
+
def get_record(self, record_id: Any) -> Any | None:
|
|
272
|
+
"""
|
|
273
|
+
Look up a record by id, or ``None`` if it is missing.
|
|
274
|
+
|
|
275
|
+
Matches SQL ``fetch_one_by_id``: identity lookup returns absence as
|
|
276
|
+
``None`` rather than raising.
|
|
277
|
+
"""
|
|
278
|
+
return self._id_index.get(record_id)
|
|
273
279
|
|
|
274
280
|
# -- removing whole records ----------------------------------------------
|
|
275
281
|
|
|
@@ -8,9 +8,9 @@ values or against each other::
|
|
|
8
8
|
Field("first_name") == Field("nickname")
|
|
9
9
|
(Field("age") > 20) & ~Field("name").contains("test")
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
A node evaluates against a record in Python. ``to_sqlalchemy`` is the other
|
|
12
|
+
implementation of the same predicate. Operators SQL cannot mean the same way
|
|
13
|
+
are refused there rather than compiled into a different query.
|
|
14
14
|
"""
|
|
15
15
|
|
|
16
16
|
from corekit.data.expressions.comparison import (
|