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.
Files changed (109) hide show
  1. corekit/api/__init__.py +18 -3
  2. corekit/api/application.py +275 -0
  3. corekit/api/lifespan.py +233 -0
  4. corekit/api/middleware.py +93 -0
  5. corekit/api/routers.py +109 -1
  6. corekit/concurrency/__init__.py +2 -2
  7. corekit/concurrency/decorators.py +32 -5
  8. corekit/concurrency/thread_local.py +2 -2
  9. corekit/concurrency/worker.py +74 -65
  10. corekit/config/loader.py +42 -5
  11. corekit/config/settings.py +11 -1
  12. corekit/connections/__init__.py +7 -1
  13. corekit/connections/connectable.py +45 -4
  14. corekit/connections/redis/connection.py +53 -10
  15. corekit/connections/sql/__init__.py +33 -4
  16. corekit/connections/sql/connection.py +56 -3
  17. corekit/connections/sql/fields/__init__.py +2 -2
  18. corekit/connections/sql/fields/jsonb.py +13 -6
  19. corekit/connections/sql/migration/__init__.py +9 -5
  20. corekit/connections/sql/migration/base.py +3 -3
  21. corekit/connections/sql/migration/operations.py +135 -44
  22. corekit/connections/sql/migration/registry.py +2 -2
  23. corekit/connections/sql/operations/__init__.py +24 -0
  24. corekit/connections/sql/operations/base.py +111 -0
  25. corekit/connections/sql/operations/statements.py +170 -0
  26. corekit/connections/sql/query.py +4 -62
  27. corekit/connections/sql/table.py +33 -29
  28. corekit/crypto/__init__.py +3 -1
  29. corekit/crypto/constants.py +2 -2
  30. corekit/crypto/hasher.py +9 -4
  31. corekit/data/__init__.py +8 -0
  32. corekit/data/dataset.py +8 -2
  33. corekit/data/expressions/__init__.py +10 -2
  34. corekit/data/expressions/comparison.py +142 -123
  35. corekit/data/expressions/expression.py +71 -98
  36. corekit/data/expressions/operator.py +39 -0
  37. corekit/data/expressions/target.py +21 -0
  38. corekit/data/record.py +147 -147
  39. corekit/data/stats.py +162 -157
  40. corekit/decorators/__init__.py +2 -2
  41. corekit/decorators/exception_handling.py +38 -9
  42. corekit/docker/watchdog.py +50 -31
  43. corekit/etl/__init__.py +2 -1
  44. corekit/etl/connection.py +46 -44
  45. corekit/etl/extract/extractor.py +6 -13
  46. corekit/etl/orchestrator.py +19 -2
  47. corekit/etl/schemas.py +2 -2
  48. corekit/etl/transform/transformer.py +4 -1
  49. corekit/events/publisher.py +1 -1
  50. corekit/events/reader.py +26 -21
  51. corekit/events/sse.py +4 -1
  52. corekit/events/websocket.py +27 -13
  53. corekit/exceptions/__init__.py +33 -0
  54. corekit/exceptions/base.py +139 -10
  55. corekit/exceptions/enum.py +17 -0
  56. corekit/exceptions/types.py +6 -6
  57. corekit/files/__init__.py +2 -4
  58. corekit/files/base.py +15 -2
  59. corekit/files/enum.py +0 -5
  60. corekit/files/json.py +16 -2
  61. corekit/http/__init__.py +51 -0
  62. corekit/http/api.py +24 -0
  63. corekit/http/client.py +100 -73
  64. corekit/http/exceptions.py +140 -0
  65. corekit/http/response.py +50 -1
  66. corekit/http/status.py +89 -0
  67. corekit/jobs/__init__.py +26 -0
  68. corekit/jobs/registry.py +87 -0
  69. corekit/jobs/runner.py +80 -0
  70. corekit/jobs/task.py +173 -0
  71. corekit/log_monitor/models.py +8 -2
  72. corekit/log_monitor/service.py +77 -38
  73. corekit/notifications/base.py +18 -10
  74. corekit/observability/__init__.py +12 -3
  75. corekit/observability/benchmarkable.py +23 -5
  76. corekit/observability/loggable.py +21 -0
  77. corekit/observability/request_context.py +188 -0
  78. corekit/observability/timing/timer.py +4 -2
  79. corekit/registry/__init__.py +12 -7
  80. corekit/registry/ordered.py +86 -0
  81. corekit/registry/registry.py +55 -14
  82. corekit/schemas/__init__.py +10 -0
  83. corekit/schemas/enum.py +70 -49
  84. corekit/schemas/models/arbitrary.py +11 -11
  85. corekit/schemas/pydantic/fields.py +35 -35
  86. corekit/schemas/types.py +45 -40
  87. corekit/serialization/__init__.py +24 -0
  88. corekit/serialization/pickle_file.py +61 -0
  89. corekit/serialization/serializable.py +22 -2
  90. corekit/serialization/serializer.py +10 -3
  91. corekit/utils/__init__.py +59 -5
  92. corekit/utils/coercion.py +118 -0
  93. corekit/utils/collections.py +124 -0
  94. corekit/utils/ids.py +61 -5
  95. corekit/utils/payload.py +112 -0
  96. corekit/utils/raise_exc.py +8 -8
  97. corekit/utils/text.py +56 -0
  98. corekit/utils/time.py +74 -21
  99. corekit/utils/validators.py +15 -15
  100. corekit/utils/void.py +8 -8
  101. {python_corekit-0.1.1.dist-info → python_corekit-0.3.0.dist-info}/METADATA +103 -97
  102. python_corekit-0.3.0.dist-info/RECORD +145 -0
  103. corekit/constants.py +0 -45
  104. corekit/exceptions/http/exceptions.py +0 -37
  105. corekit/files/pickle.py +0 -12
  106. python_corekit-0.1.1.dist-info/RECORD +0 -125
  107. {python_corekit-0.1.1.dist-info → python_corekit-0.3.0.dist-info}/WHEEL +0 -0
  108. {python_corekit-0.1.1.dist-info → python_corekit-0.3.0.dist-info}/licenses/LICENSE +0 -0
  109. {python_corekit-0.1.1.dist-info → python_corekit-0.3.0.dist-info}/top_level.txt +0 -0
@@ -1,68 +1,10 @@
1
1
  """
2
- A chainable query builder over SQLModel's select().
2
+ The read operation, under its original name.
3
3
  """
4
4
 
5
- from typing import Any
6
-
7
- from pydantic import BaseModel, Field
8
- from sqlmodel import desc as _desc
9
- from sqlmodel import select
10
-
11
- from corekit.connections.sql.table import NamedTable
5
+ from corekit.connections.sql.operations.statements import Select
12
6
 
13
7
  __all__ = ["Query"]
14
8
 
15
-
16
- class Query(BaseModel):
17
- """
18
- Accumulates filters, ordering and a limit, then builds a select statement.
19
-
20
- Query(table=User).where(User.age >= 18).order_by(User.name).limit(10)
21
- """
22
-
23
- table: type[NamedTable]
24
- queries: list[Any] = Field(default_factory=list)
25
- order_by_field: Any = None
26
- order_by_desc: bool = False
27
- limit_count: int = -1
28
-
29
- def where(self, query: Any) -> "Query":
30
- """
31
- Add a filter clause.
32
- """
33
- self.queries.append(query)
34
- return self
35
-
36
- def order_by(self, field: Any, desc: bool = False) -> "Query":
37
- """
38
- Order results by a field.
39
- """
40
- self.order_by_field = field
41
- self.order_by_desc = desc
42
- return self
43
-
44
- def limit(self, limit: int) -> "Query":
45
- """
46
- Cap the number of rows returned. A non-positive value means no limit.
47
- """
48
- self.limit_count = limit
49
- return self
50
-
51
- def build(self) -> Any:
52
- """
53
- Produce the select statement.
54
- """
55
- statement = select(self.table)
56
- for query in self.queries:
57
- statement = statement.where(query)
58
-
59
- if self.order_by_field is not None:
60
- order_by_field = self.order_by_field
61
- if self.order_by_desc:
62
- order_by_field = _desc(order_by_field)
63
- statement = statement.order_by(order_by_field)
64
-
65
- if self.limit_count > 0:
66
- statement = statement.limit(self.limit_count)
67
-
68
- return statement
9
+ #: ``Select`` names the statement it builds; ``Query`` is its original name.
10
+ Query = Select
@@ -2,15 +2,25 @@
2
2
  Base table type.
3
3
  """
4
4
 
5
- import json
6
5
  import logging
7
- from typing import Any
6
+ from typing import Any, ClassVar
8
7
 
8
+ from pydantic import BaseModel
9
9
  from sqlmodel import SQLModel
10
10
 
11
11
  from corekit.registry import SmartRegistry
12
12
 
13
- __all__ = ["NamedTable"]
13
+ __all__ = ["NamedTable", "TableExportItem"]
14
+
15
+
16
+ class TableExportItem(BaseModel):
17
+ """
18
+ One exported row, tagged with the table it came from.
19
+ """
20
+
21
+ table_name: str
22
+ data: dict[str, Any]
23
+
14
24
 
15
25
  logger = logging.getLogger(__name__)
16
26
 
@@ -22,10 +32,18 @@ class NamedTable(SQLModel):
22
32
  Subclasses register themselves by name, so a table class can be resolved
23
33
  from a string -- useful for imports, exports and admin tooling that work
24
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.
25
39
  """
26
40
 
27
41
  __registry__: SmartRegistry = SmartRegistry()
28
42
 
43
+ #: A human-readable name for the table. Declared as a ClassVar so pydantic
44
+ #: leaves a subclass's value as a plain string rather than wrapping it.
45
+ _table_display_name: ClassVar[str | None] = None
46
+
29
47
  def __init_subclass__(cls, **kwargs: Any) -> None:
30
48
  """
31
49
  Register every subclass under its class name.
@@ -50,16 +68,21 @@ class NamedTable(SQLModel):
50
68
  @property
51
69
  def table_name(self) -> str:
52
70
  """
53
- The name of the table this record belongs to.
71
+ The database table name for this row (``__tablename__``).
54
72
  """
55
- return type(self).__name__
73
+ return type(self).__tablename__
56
74
 
57
75
  @classmethod
58
76
  def get_display_name(cls) -> str:
59
77
  """
60
- A human-readable name for the table.
78
+ A human-readable name for the table, falling back to the class name.
61
79
  """
62
- return getattr(cls, "__display_name__", None) or cls.__name__
80
+ name = cls._table_display_name
81
+ if name is not None and not isinstance(name, str):
82
+ # A subclass declared outside this module's ClassVar annotation
83
+ # gets its value wrapped, and the wrapper's repr is not a label.
84
+ name = getattr(name, "default", None) or str(name)
85
+ return name or cls.__name__
63
86
 
64
87
  @classmethod
65
88
  def create_default(cls, identifier: str, **kwargs: Any) -> "NamedTable":
@@ -70,27 +93,8 @@ class NamedTable(SQLModel):
70
93
  logger.info(f"Creating default {cls.__name__} with id {kwargs['id']!r}")
71
94
  return cls(**kwargs)
72
95
 
73
- @staticmethod
74
- def _serialize(value: Any) -> str:
75
- return json.dumps(value)
76
-
77
- @staticmethod
78
- def _deserialize(value: str) -> Any:
79
- try:
80
- return json.loads(value)
81
- except (TypeError, ValueError):
82
- logger.error(f"Unable to deserialize value: {value!r}")
83
- raise
84
-
85
- def serialize(self, values: dict[str, Any], key: str) -> dict[str, Any]:
86
- """
87
- Replace ``values[key]`` with its JSON encoding.
88
- """
89
- values[key] = self._serialize(values.get(key))
90
- return values
91
-
92
- def deserialize(self, key: str) -> Any:
96
+ def export(self) -> TableExportItem:
93
97
  """
94
- Decode the JSON stored in the named field.
98
+ This row as a portable record, tagged with its database table name.
95
99
  """
96
- return self._deserialize(getattr(self, key))
100
+ return TableExportItem(table_name=self.table_name, data=self.model_dump())
@@ -1 +1,3 @@
1
- from .hasher import Hasher
1
+ from .hasher import Hasher, MissingSaltError
2
+
3
+ __all__ = ["Hasher", "MissingSaltError"]
@@ -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 -- see `corekit.config.CorekitSettings.crypto_salt` and
7
- # `corekit.crypto.hasher.Hasher`.
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(RuntimeError):
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
- ``COREKIT_CRYPTO_SALT`` / ``crypto_salt`` in a corekit config file. Resolution
22
- is deferred until first use so that importing this module never fails.
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
- "COREKIT_CRYPTO_SALT, or add crypto_salt to your corekit config. "
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/__init__.py CHANGED
@@ -24,6 +24,7 @@ from corekit.data.expressions import (
24
24
  And,
25
25
  Comparison,
26
26
  Contains,
27
+ Dialect,
27
28
  Equals,
28
29
  Expression,
29
30
  FieldExpression,
@@ -34,7 +35,10 @@ from corekit.data.expressions import (
34
35
  LessThanOrEquals,
35
36
  Not,
36
37
  NotEquals,
38
+ Operator,
37
39
  Or,
40
+ Target,
41
+ UnknownFieldError,
38
42
  ValueExpression,
39
43
  )
40
44
  from corekit.data.record import BaseRecord, is_valid_key, resolve_default
@@ -74,6 +78,10 @@ __all__ = [
74
78
  "NotEquals",
75
79
  "NumericFieldStats",
76
80
  "Or",
81
+ "Dialect",
82
+ "Operator",
83
+ "Target",
84
+ "UnknownFieldError",
77
85
  "ValueCounts",
78
86
  "ValueExpression",
79
87
  "is_valid_key",
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
- return self._id_index[record_id]
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,8 +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
- Every node can also emit a backend query via ``to_sql``, ``to_mongo`` or
12
- ``to_elasticsearch``.
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.
13
14
  """
14
15
 
15
16
  from corekit.data.expressions.comparison import (
@@ -23,9 +24,12 @@ from corekit.data.expressions.comparison import (
23
24
  LessThan,
24
25
  LessThanOrEquals,
25
26
  NotEquals,
27
+ UnknownFieldError,
26
28
  ValueExpression,
27
29
  )
28
30
  from corekit.data.expressions.expression import And, Expression, Not, Or
31
+ from corekit.data.expressions.operator import Dialect, Operator
32
+ from corekit.data.expressions.target import Target
29
33
 
30
34
  __all__ = [
31
35
  "And",
@@ -41,6 +45,10 @@ __all__ = [
41
45
  "LessThanOrEquals",
42
46
  "Not",
43
47
  "NotEquals",
48
+ "UnknownFieldError",
44
49
  "Or",
50
+ "Dialect",
51
+ "Operator",
52
+ "Target",
45
53
  "ValueExpression",
46
54
  ]