krons 0.1.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 (101) hide show
  1. kronos/__init__.py +0 -0
  2. kronos/core/__init__.py +145 -0
  3. kronos/core/broadcaster.py +116 -0
  4. kronos/core/element.py +225 -0
  5. kronos/core/event.py +316 -0
  6. kronos/core/eventbus.py +116 -0
  7. kronos/core/flow.py +356 -0
  8. kronos/core/graph.py +442 -0
  9. kronos/core/node.py +982 -0
  10. kronos/core/pile.py +575 -0
  11. kronos/core/processor.py +494 -0
  12. kronos/core/progression.py +296 -0
  13. kronos/enforcement/__init__.py +57 -0
  14. kronos/enforcement/common/__init__.py +34 -0
  15. kronos/enforcement/common/boolean.py +85 -0
  16. kronos/enforcement/common/choice.py +97 -0
  17. kronos/enforcement/common/mapping.py +118 -0
  18. kronos/enforcement/common/model.py +102 -0
  19. kronos/enforcement/common/number.py +98 -0
  20. kronos/enforcement/common/string.py +140 -0
  21. kronos/enforcement/context.py +129 -0
  22. kronos/enforcement/policy.py +80 -0
  23. kronos/enforcement/registry.py +153 -0
  24. kronos/enforcement/rule.py +312 -0
  25. kronos/enforcement/service.py +370 -0
  26. kronos/enforcement/validator.py +198 -0
  27. kronos/errors.py +146 -0
  28. kronos/operations/__init__.py +32 -0
  29. kronos/operations/builder.py +228 -0
  30. kronos/operations/flow.py +398 -0
  31. kronos/operations/node.py +101 -0
  32. kronos/operations/registry.py +92 -0
  33. kronos/protocols.py +414 -0
  34. kronos/py.typed +0 -0
  35. kronos/services/__init__.py +81 -0
  36. kronos/services/backend.py +286 -0
  37. kronos/services/endpoint.py +608 -0
  38. kronos/services/hook.py +471 -0
  39. kronos/services/imodel.py +465 -0
  40. kronos/services/registry.py +115 -0
  41. kronos/services/utilities/__init__.py +36 -0
  42. kronos/services/utilities/header_factory.py +87 -0
  43. kronos/services/utilities/rate_limited_executor.py +271 -0
  44. kronos/services/utilities/rate_limiter.py +180 -0
  45. kronos/services/utilities/resilience.py +414 -0
  46. kronos/session/__init__.py +41 -0
  47. kronos/session/exchange.py +258 -0
  48. kronos/session/message.py +60 -0
  49. kronos/session/session.py +411 -0
  50. kronos/specs/__init__.py +25 -0
  51. kronos/specs/adapters/__init__.py +0 -0
  52. kronos/specs/adapters/_utils.py +45 -0
  53. kronos/specs/adapters/dataclass_field.py +246 -0
  54. kronos/specs/adapters/factory.py +56 -0
  55. kronos/specs/adapters/pydantic_adapter.py +309 -0
  56. kronos/specs/adapters/sql_ddl.py +946 -0
  57. kronos/specs/catalog/__init__.py +36 -0
  58. kronos/specs/catalog/_audit.py +39 -0
  59. kronos/specs/catalog/_common.py +43 -0
  60. kronos/specs/catalog/_content.py +59 -0
  61. kronos/specs/catalog/_enforcement.py +70 -0
  62. kronos/specs/factory.py +120 -0
  63. kronos/specs/operable.py +314 -0
  64. kronos/specs/phrase.py +405 -0
  65. kronos/specs/protocol.py +140 -0
  66. kronos/specs/spec.py +506 -0
  67. kronos/types/__init__.py +60 -0
  68. kronos/types/_sentinel.py +311 -0
  69. kronos/types/base.py +369 -0
  70. kronos/types/db_types.py +260 -0
  71. kronos/types/identity.py +66 -0
  72. kronos/utils/__init__.py +40 -0
  73. kronos/utils/_hash.py +234 -0
  74. kronos/utils/_json_dump.py +392 -0
  75. kronos/utils/_lazy_init.py +63 -0
  76. kronos/utils/_to_list.py +165 -0
  77. kronos/utils/_to_num.py +85 -0
  78. kronos/utils/_utils.py +375 -0
  79. kronos/utils/concurrency/__init__.py +205 -0
  80. kronos/utils/concurrency/_async_call.py +333 -0
  81. kronos/utils/concurrency/_cancel.py +122 -0
  82. kronos/utils/concurrency/_errors.py +96 -0
  83. kronos/utils/concurrency/_patterns.py +363 -0
  84. kronos/utils/concurrency/_primitives.py +328 -0
  85. kronos/utils/concurrency/_priority_queue.py +135 -0
  86. kronos/utils/concurrency/_resource_tracker.py +110 -0
  87. kronos/utils/concurrency/_run_async.py +67 -0
  88. kronos/utils/concurrency/_task.py +95 -0
  89. kronos/utils/concurrency/_utils.py +79 -0
  90. kronos/utils/fuzzy/__init__.py +14 -0
  91. kronos/utils/fuzzy/_extract_json.py +90 -0
  92. kronos/utils/fuzzy/_fuzzy_json.py +288 -0
  93. kronos/utils/fuzzy/_fuzzy_match.py +149 -0
  94. kronos/utils/fuzzy/_string_similarity.py +187 -0
  95. kronos/utils/fuzzy/_to_dict.py +396 -0
  96. kronos/utils/sql/__init__.py +13 -0
  97. kronos/utils/sql/_sql_validation.py +142 -0
  98. krons-0.1.0.dist-info/METADATA +70 -0
  99. krons-0.1.0.dist-info/RECORD +101 -0
  100. krons-0.1.0.dist-info/WHEEL +4 -0
  101. krons-0.1.0.dist-info/licenses/LICENSE +201 -0
kronos/core/flow.py ADDED
@@ -0,0 +1,356 @@
1
+ # Copyright (c) 2025 - 2026, HaiyangLi <quantocean.li at gmail dot com>
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from __future__ import annotations
5
+
6
+ import threading
7
+ from typing import Any, Generic, Literal, TypeVar, cast
8
+ from uuid import UUID
9
+
10
+ from pydantic import Field, PrivateAttr, field_validator, model_validator
11
+
12
+ from kronos.errors import ExistsError, NotFoundError
13
+ from kronos.protocols import Serializable, implements
14
+ from kronos.types import Unset, UnsetType
15
+ from kronos.utils import extract_types, synchronized
16
+
17
+ from .element import Element
18
+ from .pile import Pile
19
+ from .progression import Progression
20
+
21
+ __all__ = ("Flow",)
22
+
23
+ E = TypeVar("E", bound=Element) # Element type for items
24
+ P = TypeVar("P", bound=Progression) # Progression type
25
+
26
+
27
+ @implements(Serializable)
28
+ class Flow(Element, Generic[E, P]):
29
+ """Workflow state container with items and named progressions.
30
+
31
+ Composition: items Pile (storage) + progressions Pile (ordered UUID sequences).
32
+ Progressions reference items by UUID; referential integrity is validated.
33
+
34
+ Thread Safety:
35
+ Flow methods are RLock-synchronized. Direct pile access bypasses lock.
36
+
37
+ Attributes:
38
+ name: Optional flow identifier.
39
+ items: Element storage (Pile[E]).
40
+ progressions: Named UUID sequences (Pile[P]).
41
+
42
+ Generic Parameters:
43
+ E: Element type for items.
44
+ P: Progression type.
45
+
46
+ Example:
47
+ >>> flow = Flow[Node, Progression](item_type=Node)
48
+ >>> flow.add_item(node, progressions="ready")
49
+ """
50
+
51
+ name: str | None = Field(
52
+ default=None,
53
+ description="Optional name for this flow (e.g., 'task_workflow')",
54
+ )
55
+ progressions: Pile[P] = Field(
56
+ default_factory=Pile,
57
+ description="Workflow stages as named progressions",
58
+ )
59
+ items: Pile[E] = Field(
60
+ default_factory=Pile,
61
+ description="Items that progressions reference",
62
+ )
63
+ _progression_names: dict[str, UUID] = PrivateAttr(default_factory=dict)
64
+ _lock: threading.RLock = PrivateAttr(default_factory=threading.RLock)
65
+
66
+ def __init__(
67
+ self,
68
+ items: list[E] | Pile[E] | Element | None = None,
69
+ progressions: list[P] | Pile[P] | None = None,
70
+ name: str | None = None,
71
+ item_type: type[E] | set[type] | list[type] | None = None,
72
+ strict_type: bool = False,
73
+ **data,
74
+ ):
75
+ """Initialize Flow with items, progressions, and type validation.
76
+
77
+ Args:
78
+ items: Initial items (Element, list, Pile, or list[dict]).
79
+ progressions: Initial progressions (list, Pile, or list[dict]).
80
+ name: Flow identifier.
81
+ item_type: Allowed item type(s) for validation.
82
+ strict_type: If True, reject subclasses.
83
+ **data: Additional Element fields.
84
+ """
85
+ item_type = extract_types(item_type) if item_type else None
86
+
87
+ if isinstance(items, Pile):
88
+ data["items"] = items
89
+ elif isinstance(items, dict):
90
+ # Dict from deserialization - let field validator handle it
91
+ data["items"] = items
92
+ elif isinstance(items, list) and items and isinstance(items[0], dict):
93
+ # List of dicts from deserialization - let field validator handle it
94
+ data["items"] = items
95
+ elif items is not None or item_type is not None or strict_type:
96
+ # Normalize to list
97
+ if isinstance(items, Element):
98
+ items = cast(list[E], [items])
99
+
100
+ # Create Pile with items and type validation (item_type/strict_type are frozen)
101
+ # Even if items=None, create Pile if item_type/strict_type specified
102
+ data["items"] = Pile(items=items, item_type=item_type, strict_type=strict_type)
103
+
104
+ # Handle progressions - let field validator convert dict/list to Pile
105
+ if progressions is not None:
106
+ data["progressions"] = progressions
107
+
108
+ if name is not None:
109
+ data["name"] = name
110
+
111
+ super().__init__(**data)
112
+
113
+ @field_validator("items", "progressions", mode="wrap")
114
+ @classmethod
115
+ def _validate_piles(cls, v: Any, handler: Any, info) -> Any:
116
+ """Coerce Pile, dict, or list inputs to Pile."""
117
+ if isinstance(v, Pile):
118
+ return v
119
+ if isinstance(v, dict):
120
+ return Pile.from_dict(v)
121
+ if isinstance(v, list):
122
+ pile: Pile[Any] = Pile()
123
+ for item in v:
124
+ if isinstance(item, dict):
125
+ pile.add(Element.from_dict(item))
126
+ else:
127
+ pile.add(item)
128
+ return pile
129
+ return handler(v)
130
+
131
+ @model_validator(mode="after")
132
+ def _validate_referential_integrity(self) -> Flow:
133
+ """Validate all progression UUIDs exist in items pile."""
134
+ item_ids = set(self.items.keys())
135
+
136
+ for prog in self.progressions:
137
+ missing_ids = set(list(prog)) - item_ids
138
+ if missing_ids:
139
+ raise NotFoundError(
140
+ f"Progression '{prog.name}' contains UUIDs not in items pile: {missing_ids}"
141
+ )
142
+
143
+ return self
144
+
145
+ def model_post_init(self, __context: Any) -> None:
146
+ """Rebuild _progression_names index from progressions."""
147
+ super().model_post_init(__context)
148
+ for progression in self.progressions:
149
+ if progression.name:
150
+ self._progression_names[progression.name] = progression.id
151
+
152
+ def _check_item_exists(self, item_id: UUID) -> E:
153
+ """Get item or raise NotFoundError with flow context."""
154
+ try:
155
+ return self.items[item_id]
156
+ except NotFoundError as e:
157
+ raise NotFoundError(
158
+ f"Item {item_id} not found in flow",
159
+ details=e.details,
160
+ retryable=e.retryable,
161
+ cause=e,
162
+ )
163
+
164
+ def _check_progression_exists(self, progression_id: UUID) -> P:
165
+ """Get progression or raise NotFoundError with flow context."""
166
+ try:
167
+ return self.progressions[progression_id]
168
+ except NotFoundError as e:
169
+ raise NotFoundError(
170
+ f"Progression {progression_id} not found in flow",
171
+ details=e.details,
172
+ retryable=e.retryable,
173
+ cause=e,
174
+ )
175
+
176
+ # ==================== Progression Management ====================
177
+
178
+ @synchronized
179
+ def add_progression(self, progression: P) -> None:
180
+ """Add progression with name uniqueness and referential integrity checks.
181
+
182
+ Args:
183
+ progression: Progression to add.
184
+
185
+ Raises:
186
+ ExistsError: If name already registered.
187
+ NotFoundError: If progression contains UUIDs not in items.
188
+ """
189
+ if progression.name and progression.name in self._progression_names:
190
+ raise ExistsError(
191
+ f"Progression with name '{progression.name}' already exists. Names must be unique."
192
+ )
193
+
194
+ item_ids = set(self.items.keys())
195
+ missing_ids = set(list(progression)) - item_ids
196
+ if missing_ids:
197
+ raise NotFoundError(
198
+ f"Progression '{progression.name or progression.id}' contains UUIDs not in items pile: {missing_ids}"
199
+ )
200
+
201
+ self.progressions.add(progression)
202
+
203
+ if progression.name:
204
+ self._progression_names[progression.name] = progression.id
205
+
206
+ @synchronized
207
+ def remove_progression(self, progression_id: UUID | str | P) -> P:
208
+ """Remove progression by UUID or name.
209
+
210
+ Args:
211
+ progression_id: UUID, name string, or Progression instance.
212
+
213
+ Returns:
214
+ Removed progression.
215
+
216
+ Raises:
217
+ NotFoundError: If progression not found.
218
+ """
219
+ name_to_delete: str | None
220
+ if isinstance(progression_id, str) and progression_id in self._progression_names:
221
+ uid = self._progression_names[progression_id]
222
+ name_to_delete = progression_id
223
+ else:
224
+ uid = self._coerce_id(progression_id)
225
+ prog = self._check_progression_exists(uid)
226
+ name_to_delete = prog.name if prog.name in self._progression_names else None
227
+
228
+ removed = self.progressions.remove(uid)
229
+
230
+ if name_to_delete and name_to_delete in self._progression_names:
231
+ del self._progression_names[name_to_delete]
232
+
233
+ return removed
234
+
235
+ @synchronized
236
+ def get_progression(self, key: UUID | str | P) -> P:
237
+ """Get progression by UUID or name.
238
+
239
+ Args:
240
+ key: UUID, name string, or Progression instance.
241
+
242
+ Returns:
243
+ Matching progression.
244
+
245
+ Raises:
246
+ KeyError: If not found.
247
+ """
248
+ if isinstance(key, str):
249
+ if key in self._progression_names:
250
+ uid = self._progression_names[key]
251
+ return self.progressions[uid]
252
+
253
+ try:
254
+ uid = self._coerce_id(key)
255
+ return self.progressions[uid]
256
+ except (ValueError, TypeError):
257
+ raise KeyError(f"Progression '{key}' not found in flow")
258
+
259
+ uid = key.id if isinstance(key, Progression) else key
260
+ return self.progressions[uid]
261
+
262
+ # ==================== Item Management ====================
263
+
264
+ @synchronized
265
+ def add_item(
266
+ self,
267
+ item: E,
268
+ progressions: list[UUID | str | P] | UUID | str | P | None = None,
269
+ ) -> None:
270
+ """Add item to storage and optionally to progressions.
271
+
272
+ Args:
273
+ item: Element to add.
274
+ progressions: Progression(s) to append item to (by instance, UUID, or name).
275
+
276
+ Raises:
277
+ ExistsError: If item UUID already in pile.
278
+ KeyError: If any progression not found (no side effects on failure).
279
+ """
280
+ resolved_progs: list[P] = []
281
+ if progressions is not None:
282
+ if isinstance(progressions, (str, UUID, Progression)):
283
+ progs = [progressions]
284
+ else:
285
+ progs = list(progressions)
286
+
287
+ for prog in progs:
288
+ if isinstance(prog, Progression):
289
+ resolved_progs.append(prog)
290
+ else:
291
+ resolved_progs.append(self.get_progression(prog))
292
+
293
+ self.items.add(item)
294
+
295
+ for prog in resolved_progs:
296
+ prog.append(item)
297
+
298
+ @synchronized
299
+ def remove_item(self, item_id: UUID | str | Element) -> E:
300
+ """Remove item from storage and all progressions.
301
+
302
+ Args:
303
+ item_id: UUID, UUID string, or Element instance.
304
+
305
+ Returns:
306
+ Removed item.
307
+
308
+ Raises:
309
+ NotFoundError: If item not in pile.
310
+ """
311
+ uid = self._coerce_id(item_id)
312
+
313
+ for progression in self.progressions:
314
+ if uid in progression:
315
+ progression.remove(uid)
316
+
317
+ return self.items.remove(uid)
318
+
319
+ def __repr__(self) -> str:
320
+ name_str = f", name='{self.name}'" if self.name else ""
321
+ return f"Flow(items={len(self.items)}, progressions={len(self.progressions)}{name_str})"
322
+
323
+ def to_dict(
324
+ self,
325
+ mode: Literal["python", "json", "db"] = "python",
326
+ created_at_format: (Literal["datetime", "isoformat", "timestamp"] | UnsetType) = Unset,
327
+ meta_key: str | UnsetType = Unset,
328
+ **kwargs: Any,
329
+ ) -> dict[str, Any]:
330
+ """Serialize Flow including nested Pile contents.
331
+
332
+ Overrides Element.to_dict() to properly serialize items and progressions
333
+ with their contents (not just pile metadata).
334
+ """
335
+ exclude = kwargs.pop("exclude", set())
336
+ if isinstance(exclude, set):
337
+ exclude = exclude | {"items", "progressions"}
338
+ else:
339
+ exclude = set(exclude) | {"items", "progressions"}
340
+
341
+ data = super().to_dict(
342
+ mode=mode,
343
+ created_at_format=created_at_format,
344
+ meta_key=meta_key,
345
+ exclude=exclude,
346
+ **kwargs,
347
+ )
348
+
349
+ data["items"] = self.items.to_dict(
350
+ mode=mode, created_at_format=created_at_format, meta_key=meta_key
351
+ )
352
+ data["progressions"] = self.progressions.to_dict(
353
+ mode=mode, created_at_format=created_at_format, meta_key=meta_key
354
+ )
355
+
356
+ return data