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.
- kronos/__init__.py +0 -0
- kronos/core/__init__.py +145 -0
- kronos/core/broadcaster.py +116 -0
- kronos/core/element.py +225 -0
- kronos/core/event.py +316 -0
- kronos/core/eventbus.py +116 -0
- kronos/core/flow.py +356 -0
- kronos/core/graph.py +442 -0
- kronos/core/node.py +982 -0
- kronos/core/pile.py +575 -0
- kronos/core/processor.py +494 -0
- kronos/core/progression.py +296 -0
- kronos/enforcement/__init__.py +57 -0
- kronos/enforcement/common/__init__.py +34 -0
- kronos/enforcement/common/boolean.py +85 -0
- kronos/enforcement/common/choice.py +97 -0
- kronos/enforcement/common/mapping.py +118 -0
- kronos/enforcement/common/model.py +102 -0
- kronos/enforcement/common/number.py +98 -0
- kronos/enforcement/common/string.py +140 -0
- kronos/enforcement/context.py +129 -0
- kronos/enforcement/policy.py +80 -0
- kronos/enforcement/registry.py +153 -0
- kronos/enforcement/rule.py +312 -0
- kronos/enforcement/service.py +370 -0
- kronos/enforcement/validator.py +198 -0
- kronos/errors.py +146 -0
- kronos/operations/__init__.py +32 -0
- kronos/operations/builder.py +228 -0
- kronos/operations/flow.py +398 -0
- kronos/operations/node.py +101 -0
- kronos/operations/registry.py +92 -0
- kronos/protocols.py +414 -0
- kronos/py.typed +0 -0
- kronos/services/__init__.py +81 -0
- kronos/services/backend.py +286 -0
- kronos/services/endpoint.py +608 -0
- kronos/services/hook.py +471 -0
- kronos/services/imodel.py +465 -0
- kronos/services/registry.py +115 -0
- kronos/services/utilities/__init__.py +36 -0
- kronos/services/utilities/header_factory.py +87 -0
- kronos/services/utilities/rate_limited_executor.py +271 -0
- kronos/services/utilities/rate_limiter.py +180 -0
- kronos/services/utilities/resilience.py +414 -0
- kronos/session/__init__.py +41 -0
- kronos/session/exchange.py +258 -0
- kronos/session/message.py +60 -0
- kronos/session/session.py +411 -0
- kronos/specs/__init__.py +25 -0
- kronos/specs/adapters/__init__.py +0 -0
- kronos/specs/adapters/_utils.py +45 -0
- kronos/specs/adapters/dataclass_field.py +246 -0
- kronos/specs/adapters/factory.py +56 -0
- kronos/specs/adapters/pydantic_adapter.py +309 -0
- kronos/specs/adapters/sql_ddl.py +946 -0
- kronos/specs/catalog/__init__.py +36 -0
- kronos/specs/catalog/_audit.py +39 -0
- kronos/specs/catalog/_common.py +43 -0
- kronos/specs/catalog/_content.py +59 -0
- kronos/specs/catalog/_enforcement.py +70 -0
- kronos/specs/factory.py +120 -0
- kronos/specs/operable.py +314 -0
- kronos/specs/phrase.py +405 -0
- kronos/specs/protocol.py +140 -0
- kronos/specs/spec.py +506 -0
- kronos/types/__init__.py +60 -0
- kronos/types/_sentinel.py +311 -0
- kronos/types/base.py +369 -0
- kronos/types/db_types.py +260 -0
- kronos/types/identity.py +66 -0
- kronos/utils/__init__.py +40 -0
- kronos/utils/_hash.py +234 -0
- kronos/utils/_json_dump.py +392 -0
- kronos/utils/_lazy_init.py +63 -0
- kronos/utils/_to_list.py +165 -0
- kronos/utils/_to_num.py +85 -0
- kronos/utils/_utils.py +375 -0
- kronos/utils/concurrency/__init__.py +205 -0
- kronos/utils/concurrency/_async_call.py +333 -0
- kronos/utils/concurrency/_cancel.py +122 -0
- kronos/utils/concurrency/_errors.py +96 -0
- kronos/utils/concurrency/_patterns.py +363 -0
- kronos/utils/concurrency/_primitives.py +328 -0
- kronos/utils/concurrency/_priority_queue.py +135 -0
- kronos/utils/concurrency/_resource_tracker.py +110 -0
- kronos/utils/concurrency/_run_async.py +67 -0
- kronos/utils/concurrency/_task.py +95 -0
- kronos/utils/concurrency/_utils.py +79 -0
- kronos/utils/fuzzy/__init__.py +14 -0
- kronos/utils/fuzzy/_extract_json.py +90 -0
- kronos/utils/fuzzy/_fuzzy_json.py +288 -0
- kronos/utils/fuzzy/_fuzzy_match.py +149 -0
- kronos/utils/fuzzy/_string_similarity.py +187 -0
- kronos/utils/fuzzy/_to_dict.py +396 -0
- kronos/utils/sql/__init__.py +13 -0
- kronos/utils/sql/_sql_validation.py +142 -0
- krons-0.1.0.dist-info/METADATA +70 -0
- krons-0.1.0.dist-info/RECORD +101 -0
- krons-0.1.0.dist-info/WHEEL +4 -0
- 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
|