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,11 +1,17 @@
1
+ import logging
1
2
  import re
2
3
  from typing import Any, Iterator
3
4
 
5
+ from corekit.config import get_settings
6
+
4
7
  REPLACEMENT_CHAR = "-"
5
8
  NORMALIZATION_PATTERN = re.compile(r"[\s_-]+")
6
9
  # Split CamelCase into words: "HTTPServerError" -> "HTTP-Server-Error".
7
10
  CAMEL_BOUNDARY_PATTERN = re.compile(r"(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])")
8
11
 
12
+ _logger = logging.getLogger("SmartRegistry")
13
+ _MISSING = object()
14
+
9
15
 
10
16
  class SmartRegistry:
11
17
  """
@@ -31,19 +37,33 @@ class SmartRegistry:
31
37
  """
32
38
  Retrieve an item from the registry using a normalized key.
33
39
  """
34
- return self.__registry__[self.__normalize_key__(key)]
40
+ return self.__registry__[normalize_key(key)]
35
41
 
36
42
  def __setitem__(self, key: str, value: Any) -> None:
37
43
  """
38
44
  Store an item in the registry with a normalized key.
39
- """
40
- self.__registry__[self.__normalize_key__(key)] = value
45
+
46
+ A second write to the same normalized key used to replace the first
47
+ with no signal. That drops a handler that spelled the same name a
48
+ different way. The replacement is still allowed so an intentional
49
+ update works; it is warned, and refused when ``strict_mode`` is on.
50
+ """
51
+ normalized = normalize_key(key)
52
+ existing = self.__registry__.get(normalized, _MISSING)
53
+ if existing is not _MISSING and existing is not value:
54
+ if _strict_mode():
55
+ raise ValueError(
56
+ f"Registry already contains {normalized!r}. "
57
+ f"strict_mode refuses a second registration under the same key."
58
+ )
59
+ _logger.warning(f"Overwriting registry key {normalized!r}")
60
+ self.__registry__[normalized] = value
41
61
 
42
62
  def __delitem__(self, key: str) -> None:
43
63
  """
44
64
  Remove an item from the registry.
45
65
  """
46
- del self.__registry__[self.__normalize_key__(key)]
66
+ del self.__registry__[normalize_key(key)]
47
67
 
48
68
  def __iter__(self) -> Iterator[str]:
49
69
  """
@@ -73,7 +93,7 @@ class SmartRegistry:
73
93
  """
74
94
  Check if a normalized key exists in the registry.
75
95
  """
76
- return self.__normalize_key__(key) in self.__registry__
96
+ return normalize_key(key) in self.__registry__
77
97
 
78
98
  def __str__(self) -> str:
79
99
  """
@@ -114,21 +134,42 @@ class SmartRegistry:
114
134
  self.__registry__ = state
115
135
 
116
136
  @staticmethod
117
- def __normalize_key__(key: str) -> str:
137
+ def normalize_key(key: str) -> str:
118
138
  """
119
139
  Reduce a key to a canonical hyphenated form.
120
140
 
121
- Word boundaries are taken from CamelCase as well as from whitespace,
122
- underscores and hyphens, so ``"GreetingHandler"``, ``"greeting_handler"``
123
- and ``"Greeting Handler"`` all normalize to ``"greeting-handler"``.
124
- Without the CamelCase step a class registered under its ``__name__``
125
- could not be found by the snake_case name a caller would naturally type.
141
+ Public so callers do not have to reach for the old private name.
142
+ ``__normalize_key__`` remains as an alias.
126
143
  """
127
- spaced = CAMEL_BOUNDARY_PATTERN.sub(REPLACEMENT_CHAR, key.strip())
128
- return NORMALIZATION_PATTERN.sub(REPLACEMENT_CHAR, spaced.lower()).strip(REPLACEMENT_CHAR)
144
+ return normalize_key(key)
145
+
146
+ # Kept so existing callers, including ThreadLocalRegistry and tests, keep
147
+ # working. New code should call ``normalize_key``.
148
+ __normalize_key__ = staticmethod(normalize_key)
129
149
 
130
150
  def get(self, key: str, fallback: Any = None) -> Any:
131
151
  """
132
152
  Safely retrieve an item from the registry with an optional fallback.
133
153
  """
134
- return self.__registry__.get(self.__normalize_key__(key), fallback)
154
+ return self.__registry__.get(normalize_key(key), fallback)
155
+
156
+
157
+ def _strict_mode() -> bool:
158
+ """
159
+ Whether a colliding key should be refused.
160
+ """
161
+ return get_settings().standards.strict_mode
162
+
163
+
164
+ def normalize_key(key: str) -> str:
165
+ """
166
+ Reduce a key to a canonical hyphenated form.
167
+
168
+ Word boundaries are taken from CamelCase as well as from whitespace,
169
+ underscores and hyphens, so ``"GreetingHandler"``, ``"greeting_handler"``
170
+ and ``"Greeting Handler"`` all normalize to ``"greeting-handler"``.
171
+ Without the CamelCase step a class registered under its ``__name__``
172
+ could not be found by the snake_case name a caller would naturally type.
173
+ """
174
+ spaced = CAMEL_BOUNDARY_PATTERN.sub(REPLACEMENT_CHAR, key.strip())
175
+ return NORMALIZATION_PATTERN.sub(REPLACEMENT_CHAR, spaced.lower()).strip(REPLACEMENT_CHAR)
@@ -0,0 +1,10 @@
1
+ """
2
+ Shared types.
3
+
4
+ The enum bases validate their members on construction, so an unknown value
5
+ raises where it is introduced rather than somewhere further along.
6
+ """
7
+
8
+ from corekit.schemas.enum import IntegerEnum, StringEnum, ValidatingEnum
9
+
10
+ __all__ = ["IntegerEnum", "StringEnum", "ValidatingEnum"]
corekit/schemas/enum.py CHANGED
@@ -1,49 +1,70 @@
1
- from enum import Enum
2
- from typing import Any
3
-
4
-
5
- class ValidatingEnum(Enum):
6
- """
7
- A base class for enums that validates values against a set of allowed values.
8
- """
9
-
10
- @classmethod
11
- def is_member(cls, value: Any) -> bool:
12
- """
13
- Check if the value is a member of the enum.
14
- """
15
- return value in cls._value2member_map_
16
-
17
- @classmethod
18
- def validate_and_create(cls, value: Any) -> "ValidatingEnum":
19
- if cls.is_member(value):
20
- return cls(value)
21
- raise ValueError(f"{cls.__name__} does not contain {cls}")
22
-
23
- @classmethod
24
- def get_all_members(cls) -> list["ValidatingEnum"]:
25
- """
26
- Get all members of the enum.
27
- """
28
- return list(cls._value2member_map_.values()) # type: ignore[return-value]
29
-
30
- def display_name(self) -> str:
31
- return self.name.replace("_", " ").title()
32
-
33
-
34
- class StringEnum(str, ValidatingEnum):
35
- @classmethod
36
- def from_string(cls, value: str) -> "StringEnum":
37
- """
38
- Convert a string to the corresponding enum member.
39
- """
40
- return cls.validate_and_create(value)
41
-
42
-
43
- class IntegerEnum(int, ValidatingEnum):
44
- @classmethod
45
- def from_int(cls, value: int) -> "IntegerEnum":
46
- """
47
- Convert an integer to the corresponding enum member.
48
- """
49
- return cls.validate_and_create(value)
1
+ from enum import Enum
2
+ from typing import Any
3
+
4
+
5
+ class ValidatingEnum(Enum):
6
+ """
7
+ A base class for enums that validates values against a set of allowed values.
8
+ """
9
+
10
+ @classmethod
11
+ def is_member(cls, value: Any) -> bool:
12
+ """
13
+ Check if the value is a member of the enum.
14
+ """
15
+ return value in cls._value2member_map_
16
+
17
+ @classmethod
18
+ def validate_and_create(cls, value: Any) -> "ValidatingEnum":
19
+ if cls.is_member(value):
20
+ return cls(value)
21
+ raise ValueError(f"{cls.__name__} does not contain {value!r}")
22
+
23
+ @classmethod
24
+ def get_all_members(cls) -> list["ValidatingEnum"]:
25
+ """
26
+ Get all members of the enum.
27
+ """
28
+ return list(cls._value2member_map_.values()) # type: ignore[return-value]
29
+
30
+ def display_name(self) -> str:
31
+ return self.name.replace("_", " ").title()
32
+
33
+
34
+ class StringEnum(str, ValidatingEnum):
35
+ """
36
+ String-valued enum that stringifies to its value.
37
+
38
+ Plain ``(str, Enum)`` members compare equal to their value but ``str()`` /
39
+ f-strings still render as ``ClassName.MEMBER``. Override that so DB defaults,
40
+ status comparisons, and log lines get ``"pending"`` rather than
41
+ ``"AccountRequestStatus.PENDING"``.
42
+ """
43
+
44
+ @classmethod
45
+ def from_string(cls, value: str) -> "StringEnum":
46
+ """
47
+ Convert a string to the corresponding enum member.
48
+ """
49
+ return cls.validate_and_create(value)
50
+
51
+ def __str__(self) -> str:
52
+ return str(self.value)
53
+
54
+ def __format__(self, format_spec: str) -> str:
55
+ return self.value.__format__(format_spec)
56
+
57
+
58
+ class IntegerEnum(int, ValidatingEnum):
59
+ @classmethod
60
+ def from_int(cls, value: int) -> "IntegerEnum":
61
+ """
62
+ Convert an integer to the corresponding enum member.
63
+ """
64
+ return cls.validate_and_create(value)
65
+
66
+ def __str__(self) -> str:
67
+ return str(self.value)
68
+
69
+ def __format__(self, format_spec: str) -> str:
70
+ return self.value.__format__(format_spec)
@@ -1,11 +1,11 @@
1
- from pydantic import BaseModel
2
-
3
-
4
- class ArbitraryModel(BaseModel):
5
- """
6
- Base model for arbitrary data. This model is used to take unstructured data and convert it into
7
- a structured format that can be easily mapped to a model. This model is capable of parsing any type of data
8
- and converting it into a structured format that can be easily accessed and manipulated.
9
- """
10
-
11
- pass
1
+ from pydantic import BaseModel
2
+
3
+
4
+ class ArbitraryModel(BaseModel):
5
+ """
6
+ Base model for arbitrary data. This model is used to take unstructured data and convert it into
7
+ a structured format that can be easily mapped to a model. This model is capable of parsing any type of data
8
+ and converting it into a structured format that can be easily accessed and manipulated.
9
+ """
10
+
11
+ pass
@@ -1,35 +1,35 @@
1
- from typing import Any
2
-
3
- from pydantic import Field
4
-
5
-
6
- def DefaultStringField(*args: Any, default: Any = "", **kwargs: Any) -> Field:
7
- return Field(default=default, *args, **kwargs)
8
-
9
-
10
- def DefaultIntField(*args: Any, default: Any = 0, **kwargs: Any) -> Field:
11
- return Field(default=default, *args, **kwargs)
12
-
13
-
14
- def DefaultFloatField(*args: Any, default: Any = 0.0, **kwargs: Any) -> Field:
15
- return Field(default=default, *args, **kwargs)
16
-
17
-
18
- def DefaultBoolField(*args: Any, default: Any = False, **kwargs: Any) -> Field:
19
- return Field(default=default, *args, **kwargs)
20
-
21
-
22
- def DefaultEmptyField(*args: Any, default: Any = None, **kwargs: Any) -> Field:
23
- return Field(default=default, *args, **kwargs)
24
-
25
-
26
- def DefaultListField(*args: Any, default_factory: Any = list, **kwargs: Any) -> Field:
27
- return Field(default_factory=default_factory, *args, **kwargs)
28
-
29
-
30
- def DefaultSetField(*args: Any, default_factory: Any = set, **kwargs: Any) -> Field:
31
- return Field(default_factory=default_factory, *args, **kwargs)
32
-
33
-
34
- def DefaultDictField(*args: Any, default_factory: Any = dict, **kwargs: Any) -> Field:
35
- return Field(default_factory=default_factory, *args, **kwargs)
1
+ from typing import Any
2
+
3
+ from pydantic import Field
4
+
5
+
6
+ def DefaultStringField(*args: Any, default: Any = "", **kwargs: Any) -> Field:
7
+ return Field(default=default, *args, **kwargs)
8
+
9
+
10
+ def DefaultIntField(*args: Any, default: Any = 0, **kwargs: Any) -> Field:
11
+ return Field(default=default, *args, **kwargs)
12
+
13
+
14
+ def DefaultFloatField(*args: Any, default: Any = 0.0, **kwargs: Any) -> Field:
15
+ return Field(default=default, *args, **kwargs)
16
+
17
+
18
+ def DefaultBoolField(*args: Any, default: Any = False, **kwargs: Any) -> Field:
19
+ return Field(default=default, *args, **kwargs)
20
+
21
+
22
+ def DefaultEmptyField(*args: Any, default: Any = None, **kwargs: Any) -> Field:
23
+ return Field(default=default, *args, **kwargs)
24
+
25
+
26
+ def DefaultListField(*args: Any, default_factory: Any = list, **kwargs: Any) -> Field:
27
+ return Field(default_factory=default_factory, *args, **kwargs)
28
+
29
+
30
+ def DefaultSetField(*args: Any, default_factory: Any = set, **kwargs: Any) -> Field:
31
+ return Field(default_factory=default_factory, *args, **kwargs)
32
+
33
+
34
+ def DefaultDictField(*args: Any, default_factory: Any = dict, **kwargs: Any) -> Field:
35
+ return Field(default_factory=default_factory, *args, **kwargs)
corekit/schemas/types.py CHANGED
@@ -1,40 +1,45 @@
1
- from datetime import date, datetime
2
- from typing import Any, Sequence
3
-
4
- # ========== Primitive Types ==========
5
- Number = int | float
6
- AnyPrimitive = str | bool | Number
7
-
8
- # ========== Dict Types ==========
9
- StringDict = dict[str, str]
10
- CounterDict = dict[str, int]
11
- StatisticDict = dict[str, Number]
12
- AnyDict = dict[str, Any]
13
- UnknownDict = dict[Any, Any]
14
-
15
- # ========== Sequence Types ==========
16
- # Using the more broad "Sequence" type instead of "List" to allow for other sequence types like "tuple"
17
- StringSequence = Sequence[str]
18
- IntegerSequence = Sequence[int]
19
- FloatSequence = Sequence[float]
20
- NumberSequence = Sequence[Number]
21
- BooleanSequence = Sequence[bool]
22
- UnknownSequence = Sequence[Any]
23
-
24
- # Using the more specific "list" and "set types allows for more rigid type checking
25
- StringList = list[str]
26
- IntegerList = list[int]
27
- FloatList = list[float]
28
- NumberList = list[Number]
29
- BooleanList = list[bool]
30
- UnknownList = list[Any]
31
-
32
- StringSet = set[str]
33
- IntegerSet = set[int]
34
- FloatSet = set[float]
35
- NumberSet = set[Number]
36
- BooleanSet = set[bool]
37
- UnknownSet = set[Any]
38
-
39
- # ========== Date Types ==========
40
- ArbitraryDate = date | datetime
1
+ from datetime import date, datetime
2
+ from typing import Any, Awaitable, Callable, Coroutine, Sequence
3
+
4
+ # ========== Primitive Types ==========
5
+ Number = int | float
6
+ AnyPrimitive = str | bool | Number
7
+
8
+ # ========== Dict Types ==========
9
+ StringDict = dict[str, str]
10
+ CounterDict = dict[str, int]
11
+ StatisticDict = dict[str, Number]
12
+ AnyDict = dict[str, Any]
13
+ UnknownDict = dict[Any, Any]
14
+
15
+ # ========== Sequence Types ==========
16
+ # Using the more broad "Sequence" type instead of "List" to allow for other sequence types like "tuple"
17
+ StringSequence = Sequence[str]
18
+ IntegerSequence = Sequence[int]
19
+ FloatSequence = Sequence[float]
20
+ NumberSequence = Sequence[Number]
21
+ BooleanSequence = Sequence[bool]
22
+ UnknownSequence = Sequence[Any]
23
+
24
+ # Using the more specific "list" and "set types allows for more rigid type checking
25
+ StringList = list[str]
26
+ IntegerList = list[int]
27
+ FloatList = list[float]
28
+ NumberList = list[Number]
29
+ BooleanList = list[bool]
30
+ UnknownList = list[Any]
31
+
32
+ StringSet = set[str]
33
+ IntegerSet = set[int]
34
+ FloatSet = set[float]
35
+ NumberSet = set[Number]
36
+ BooleanSet = set[bool]
37
+ UnknownSet = set[Any]
38
+
39
+ # ========== Date Types ==========
40
+ ArbitraryDate = date | datetime
41
+
42
+ # ========== Function Types ==========
43
+ AsyncFunction = Callable[..., Awaitable[Any] | Coroutine[Any, Any, Any]]
44
+ SyncFunction = Callable[..., Any]
45
+ ArbitraryFunction = AsyncFunction | SyncFunction
@@ -0,0 +1,24 @@
1
+ """
2
+ Serialization with authenticated payloads.
3
+
4
+ ``pickle`` and ``dill`` execute code while loading, so a payload must be proven
5
+ to come from a holder of the shared key *before* it is decoded, and the decoder
6
+ is chosen by the receiver rather than read out of the payload. JSON is the
7
+ default because it cannot execute code at all.
8
+
9
+ Set a key with ``COREKIT_SERIALIZATION__KEY`` to use an executing engine.
10
+ """
11
+
12
+ from corekit.serialization.enum import SerializerEngine
13
+ from corekit.serialization.pickle_file import PickleFileManager
14
+ from corekit.serialization.serializable import Serializable
15
+ from corekit.serialization.serializer import Serializer, SignatureError, UnsafeEngineError
16
+
17
+ __all__ = [
18
+ "PickleFileManager",
19
+ "Serializable",
20
+ "Serializer",
21
+ "SerializerEngine",
22
+ "SignatureError",
23
+ "UnsafeEngineError",
24
+ ]
@@ -0,0 +1,61 @@
1
+ """
2
+ Reading and writing signed pickle files.
3
+
4
+ Pickle executes code on load. ``PickleFileManager`` therefore requires a key
5
+ and stores authenticated payloads via ``Serializer``, never bare ``pickle.dumps``.
6
+ Partial reads and line streaming are refused: a pickle document is not
7
+ line-oriented and a fragment is not a valid payload.
8
+
9
+ Lives under ``serialization`` rather than ``files`` so ``corekit.config`` can
10
+ import ``TomlFileManager`` without forming a files ↔ serialization cycle.
11
+ """
12
+
13
+ from typing import Any, Iterator
14
+
15
+ from corekit.files.base import FileContent, FileError, FileManager
16
+ from corekit.files.enum import FileMode
17
+ from corekit.serialization.enum import SerializerEngine
18
+ from corekit.serialization.serializer import Serializer
19
+
20
+ __all__ = ["PickleFileManager"]
21
+
22
+
23
+ class PickleFileManager(FileManager):
24
+ """
25
+ A FileManager that round-trips values through a signed pickle ``Serializer``.
26
+ """
27
+
28
+ def __init__(
29
+ self,
30
+ file_path: str,
31
+ mode: FileMode | None = None,
32
+ *,
33
+ key: bytes | str,
34
+ binary: bool = True,
35
+ ) -> None:
36
+ if not binary:
37
+ raise FileError("PickleFileManager requires binary mode; pickle payloads are bytes.")
38
+ super().__init__(file_path, mode=mode or FileMode.get_default(binary=True), binary=True)
39
+ self._serializer = Serializer(SerializerEngine.PICKLE, key=key)
40
+
41
+ def _serialize(self, content: Any) -> FileContent:
42
+ return self._serializer.serialize(content)
43
+
44
+ def _deserialize(self, content: FileContent) -> Any:
45
+ if isinstance(content, str):
46
+ content = content.encode("utf-8")
47
+ return self._serializer.deserialize(content)
48
+
49
+ def read(self, size: int = -1) -> Any:
50
+ """
51
+ Read and verify the whole file. Partial reads are refused.
52
+ """
53
+ if size != -1:
54
+ raise FileError("PickleFileManager refuses partial reads; read the whole file with size=-1.")
55
+ return super().read(size)
56
+
57
+ def readline(self) -> Any:
58
+ raise FileError("PickleFileManager cannot readline; pickle payloads are not line-oriented.")
59
+
60
+ def stream(self) -> Iterator[Any]:
61
+ raise FileError("PickleFileManager cannot stream; pickle payloads are not line-oriented.")
@@ -15,11 +15,28 @@ class Serializable:
15
15
 
16
16
  Serializing an arbitrary object needs pickle or dill, both of which execute
17
17
  code on load, so a key is required -- see ``Serializer``.
18
+
19
+ The HMAC key on ``_serializer`` is never written into the payload. Signing
20
+ authenticates provenance; it is not encryption.
18
21
  """
19
22
 
20
23
  def __init__(self, **kwargs: Any) -> None:
21
24
  self._serializer = Serializer(**kwargs)
22
25
 
26
+ def __getstate__(self) -> dict[str, Any]:
27
+ """
28
+ Drop ``_serializer`` so a pickle/dill payload cannot carry the HMAC key.
29
+ """
30
+ state = self.__dict__.copy()
31
+ state.pop("_serializer", None)
32
+ return state
33
+
34
+ def __setstate__(self, state: dict[str, Any]) -> None:
35
+ """
36
+ Restore instance state. The serializer is reattached by ``from_serialized``.
37
+ """
38
+ self.__dict__.update(state)
39
+
23
40
  @classmethod
24
41
  def from_serialized(cls, serialized: bytes, **kwargs: Any) -> Any:
25
42
  """
@@ -28,11 +45,14 @@ class Serializable:
28
45
  The engine and key are supplied by the caller rather than read from the
29
46
  payload, so the receiver decides how the bytes are decoded.
30
47
  """
31
- return Serializer(**kwargs).deserialize(serialized)
48
+ obj = Serializer(**kwargs).deserialize(serialized)
49
+ if isinstance(obj, Serializable):
50
+ obj._serializer = Serializer(**kwargs)
51
+ return obj
32
52
 
33
53
  def serialize(self) -> bytes:
34
54
  """
35
- Encode this object.
55
+ Encode this object without embedding the HMAC key.
36
56
  """
37
57
  if not self._serializer.is_valid_for_class():
38
58
  raise TypeError(
@@ -24,6 +24,7 @@ from typing import Any
24
24
  import dill
25
25
 
26
26
  from corekit.config import get_settings
27
+ from corekit.exceptions import InternalCoreException, Retryability
27
28
  from corekit.serialization.enum import SerializerEngine
28
29
 
29
30
  __all__ = ["Serializer", "SignatureError", "UnsafeEngineError"]
@@ -42,17 +43,23 @@ _ENGINE_MODULES = {
42
43
  }
43
44
 
44
45
 
45
- class SignatureError(Exception):
46
+ class SignatureError(InternalCoreException):
46
47
  """
47
48
  Raised when a payload's signature is missing or does not verify.
48
49
  """
49
50
 
51
+ def __init__(self, message: str, *, error: str | None = None) -> None:
52
+ super().__init__(message, retryable=Retryability.NON_RETRYABLE, error=error)
50
53
 
51
- class UnsafeEngineError(Exception):
54
+
55
+ class UnsafeEngineError(InternalCoreException):
52
56
  """
53
57
  Raised when a code-executing engine is requested without a key.
54
58
  """
55
59
 
60
+ def __init__(self, message: str, *, error: str | None = None) -> None:
61
+ super().__init__(message, retryable=Retryability.NON_RETRYABLE, error=error)
62
+
56
63
 
57
64
  class Serializer:
58
65
  """
@@ -69,7 +76,7 @@ class Serializer:
69
76
  if self._engine in EXECUTING_ENGINES and self._key is None:
70
77
  raise UnsafeEngineError(
71
78
  f"{self._engine.value} executes code when loading, so it needs a key to authenticate "
72
- f"payloads. Pass key=..., set COREKIT_SERIALIZATION_KEY, or use SerializerEngine.JSON."
79
+ f"payloads. Pass key=..., set COREKIT_SERIALIZATION__KEY, or use SerializerEngine.JSON."
73
80
  )
74
81
 
75
82
  @staticmethod
corekit/utils/__init__.py CHANGED
@@ -1,5 +1,59 @@
1
- from .ids import generate_uuid
2
- from .raise_exc import raise_exc
3
- from .time import time_now, timedelta_now
4
- from .validators import false_validator, true_validator
5
- from .void import void
1
+ """
2
+ Small, dependency-free helpers.
3
+
4
+ Four groups: total conversions that never raise (``safe_*``), string assembly
5
+ for human-readable output, identifier generation, and nested-dictionary lookup.
6
+ Plus timezone-aware time helpers and the placeholder callbacks (``void``,
7
+ ``raise_exc``, ``true_validator``, ``false_validator``) that callbacks-by-
8
+ configuration need.
9
+ """
10
+
11
+ from corekit.utils.coercion import safe_dict, safe_float, safe_int, safe_list, safe_string, safe_tuple
12
+ from corekit.utils.collections import UNSET, MultiMatch, keygetter, repeated_get, split_list
13
+ from corekit.utils.ids import (
14
+ SHORTCODE_CHARS,
15
+ generate_session_token,
16
+ generate_shortcode,
17
+ generate_shortcodes,
18
+ generate_uuid,
19
+ )
20
+ from corekit.utils.payload import Payload, decode_payload, encode_payload
21
+ from corekit.utils.raise_exc import raise_exc
22
+ from corekit.utils.text import join_lines, list_to_english, sanitize_filename
23
+ from corekit.utils.time import isoformat_now, parse_docker_timestamp, time_now, timedelta_now, timestamp_now
24
+ from corekit.utils.validators import false_validator, true_validator
25
+ from corekit.utils.void import void
26
+
27
+ __all__ = [
28
+ "SHORTCODE_CHARS",
29
+ "UNSET",
30
+ "MultiMatch",
31
+ "Payload",
32
+ "decode_payload",
33
+ "encode_payload",
34
+ "false_validator",
35
+ "generate_session_token",
36
+ "generate_shortcode",
37
+ "generate_shortcodes",
38
+ "generate_uuid",
39
+ "isoformat_now",
40
+ "join_lines",
41
+ "keygetter",
42
+ "list_to_english",
43
+ "parse_docker_timestamp",
44
+ "raise_exc",
45
+ "repeated_get",
46
+ "safe_dict",
47
+ "safe_float",
48
+ "safe_int",
49
+ "safe_list",
50
+ "safe_string",
51
+ "safe_tuple",
52
+ "sanitize_filename",
53
+ "split_list",
54
+ "time_now",
55
+ "timedelta_now",
56
+ "timestamp_now",
57
+ "true_validator",
58
+ "void",
59
+ ]