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
@@ -0,0 +1,471 @@
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
+ from collections.abc import Awaitable, Callable
7
+ from typing import Any, ClassVar, TypeVar
8
+
9
+ from pydantic import Field, PrivateAttr, field_validator
10
+ from typing_extensions import TypedDict
11
+
12
+ from kronos.core import Broadcaster, Event, EventStatus
13
+ from kronos.types import Enum, Undefined
14
+ from kronos.utils import concurrency
15
+
16
+ SC = TypeVar("SC")
17
+ StreamHandlers = dict[str | type, Callable[[SC], Awaitable[None]]]
18
+ E = TypeVar("E", bound=Event)
19
+
20
+
21
+ class HookPhase(Enum):
22
+ """Event lifecycle phases for hook registration.
23
+
24
+ Hooks execute at specific points in Event lifecycle:
25
+ - PreEventCreate: Before Event instantiation (receives event type)
26
+ - PreInvocation: After Event created, before invoke() (receives event instance)
27
+ - PostInvocation: After invoke() completes (receives event with result)
28
+ - ErrorHandling: On exception during invocation
29
+ """
30
+
31
+ PreEventCreate = "pre_event_create"
32
+ PreInvocation = "pre_invocation"
33
+ PostInvocation = "post_invocation"
34
+ ErrorHandling = "error_handling"
35
+
36
+
37
+ class AssociatedEventInfo(TypedDict, total=False):
38
+ """Information about the event associated with the hook."""
39
+
40
+ kron_class: str
41
+ """Full qualified name of the event class."""
42
+
43
+ event_id: str
44
+ """ID of the event."""
45
+
46
+ event_created_at: str
47
+ """Creation timestamp of the event (ISO format string)."""
48
+
49
+
50
+ class HookEvent(Event):
51
+ """Hook execution event that delegates to HookRegistry.
52
+
53
+ Extends kron.Event with hook-specific execution logic.
54
+ Parent Event.invoke() handles lifecycle, this implements _invoke().
55
+ """
56
+
57
+ registry: HookRegistry = Field(..., exclude=True)
58
+ hook_phase: HookPhase
59
+ exit: bool = Field(False, exclude=True)
60
+ params: dict[str, Any] = Field(default_factory=dict, exclude=True)
61
+ event_like: Event | type[Event] = Field(..., exclude=True)
62
+ _should_exit: bool = PrivateAttr(False)
63
+ _exit_cause: BaseException | None = PrivateAttr(None)
64
+
65
+ associated_event_info: AssociatedEventInfo | None = None
66
+
67
+ @field_validator("exit", mode="before")
68
+ def _validate_exit(cls, v: Any) -> bool: # noqa: N805
69
+ if v is None:
70
+ return False
71
+ return v
72
+
73
+ async def _invoke(self) -> Any:
74
+ """Execute hook via registry (called by parent Event.invoke()).
75
+
76
+ Parent Event.invoke() handles status/timing/errors automatically.
77
+ Just execute hook logic and let exceptions propagate naturally.
78
+ """
79
+ result = await self.registry.call(
80
+ self.event_like,
81
+ hook_phase=self.hook_phase,
82
+ exit=self.exit,
83
+ **self.params,
84
+ )
85
+
86
+ # Unpack the result - hook_phase returns tuple of (inner_tuple, meta)
87
+ if isinstance(result, tuple) and len(result) == 2 and isinstance(result[1], dict):
88
+ inner_tuple, meta = result
89
+ res, se, _ = inner_tuple
90
+ else:
91
+ # Streaming chunk returns a simpler tuple
92
+ res, se, _ = result
93
+ meta = {}
94
+
95
+ # Build associated event info from meta dict
96
+ event_info: AssociatedEventInfo = {"kron_class": str(meta.get("kron_class", ""))}
97
+ if "event_id" in meta:
98
+ event_info["event_id"] = str(meta["event_id"])
99
+ if "event_created_at" in meta:
100
+ event_info["event_created_at"] = str(meta["event_created_at"])
101
+ self.associated_event_info = event_info
102
+ self._should_exit = se
103
+
104
+ # Handle error results - raise them so parent Event catches and sets FAILED status
105
+ if isinstance(res, tuple) and len(res) == 2:
106
+ # Tuple (Undefined, exception) from cancelled hook
107
+ self._exit_cause = res[1]
108
+ raise res[1]
109
+
110
+ if isinstance(res, Exception):
111
+ # Exception result from failed hook
112
+ self._exit_cause = res
113
+ raise res
114
+
115
+ # Success - return result (parent sets COMPLETED status)
116
+ return res
117
+
118
+
119
+ K = TypeVar("K")
120
+
121
+
122
+ def get_handler(
123
+ d_: dict[K, Any], k: K, get: bool = False, /
124
+ ) -> Callable[..., Awaitable[Any]] | None:
125
+ """Retrieve async handler from dict, wrapping sync functions if needed.
126
+
127
+ Args:
128
+ d_: Handler dictionary (HookPhase->handler or chunk_type->handler).
129
+ k: Key to look up.
130
+ get: If True, return default passthrough handler when key missing.
131
+
132
+ Returns:
133
+ Async handler function, or None if key missing and get=False.
134
+ """
135
+ handler = d_.get(k)
136
+ if handler is None and not get:
137
+ return None
138
+
139
+ if handler is not None:
140
+ if not concurrency.is_coro_func(handler):
141
+
142
+ async def _wrapper(*args: Any, **kwargs: Any) -> Any:
143
+ await concurrency.sleep(0)
144
+ return handler(*args, **kwargs)
145
+
146
+ return _wrapper
147
+ return handler
148
+
149
+ async def _default_handler(*args: Any, **_kwargs: Any) -> Any:
150
+ await concurrency.sleep(0)
151
+ return args[0] if args else None
152
+
153
+ return _default_handler
154
+
155
+
156
+ def validate_hooks(kw: dict[Any, Any]) -> None:
157
+ """Validate hook dict: keys must be HookPhase, values must be callable.
158
+
159
+ Raises:
160
+ ValueError: If dict structure or types are invalid.
161
+ """
162
+ if not isinstance(kw, dict):
163
+ raise ValueError("Hooks must be a dictionary of callable functions")
164
+
165
+ for k, v in kw.items():
166
+ if not isinstance(k, HookPhase) or k not in HookPhase.allowed():
167
+ raise ValueError(f"Hook key must be one of {HookPhase.allowed()}, got {k}")
168
+ if not callable(v):
169
+ raise ValueError(f"Hook for {k} must be callable, got {type(v)}")
170
+
171
+
172
+ def validate_stream_handlers(kw: dict[Any, Any]) -> None:
173
+ """Validate stream handler dict: keys must be str|type, values callable.
174
+
175
+ Raises:
176
+ ValueError: If dict structure or types are invalid.
177
+ """
178
+ if not isinstance(kw, dict):
179
+ raise ValueError("Stream handlers must be a dictionary of callable functions")
180
+
181
+ for k, v in kw.items():
182
+ if not isinstance(k, str | type):
183
+ raise ValueError(f"Stream handler key must be a string or type, got {type(k)}")
184
+
185
+ if not callable(v):
186
+ raise ValueError(f"Stream handler for {k} must be callable, got {type(v)}")
187
+
188
+
189
+ class HookRegistry:
190
+ """Registry for hook callbacks at Event lifecycle phases.
191
+
192
+ Manages two handler types:
193
+ - Phase hooks: Execute at PreEventCreate/PreInvocation/PostInvocation/ErrorHandling
194
+ - Stream handlers: Process chunks during streaming (keyed by type name or class)
195
+
196
+ Handler semantics:
197
+ - Return value: Passed through to caller
198
+ - Raise exception: Cancels/aborts operation (status depends on phase)
199
+ - Exit flag: Determines whether exception should halt further processing
200
+ """
201
+
202
+ _hooks: dict[HookPhase, Callable[..., Any]]
203
+ _stream_handlers: dict[str | type, Callable[..., Any]]
204
+
205
+ def __init__(
206
+ self,
207
+ hooks: dict[HookPhase, Callable[..., Any]] | None = None,
208
+ stream_handlers: StreamHandlers[Any] | None = None,
209
+ ):
210
+ """Initialize registry with optional hooks and stream handlers.
211
+
212
+ Args:
213
+ hooks: Mapping of HookPhase to handler callables.
214
+ stream_handlers: Mapping of chunk type (str|type) to handler callables.
215
+ """
216
+ _hooks: dict[HookPhase, Callable[..., Any]] = {}
217
+ _stream_handlers: dict[str | type, Callable[..., Any]] = {}
218
+
219
+ if hooks is not None:
220
+ validate_hooks(hooks)
221
+ _hooks.update(hooks)
222
+
223
+ if stream_handlers is not None:
224
+ validate_stream_handlers(stream_handlers)
225
+ _stream_handlers.update(stream_handlers)
226
+
227
+ self._hooks = _hooks
228
+ self._stream_handlers = _stream_handlers
229
+
230
+ async def _call(
231
+ self,
232
+ hp_: HookPhase | None,
233
+ ct_: str | type | None,
234
+ ch_: Any,
235
+ ev_: E | type[E],
236
+ /,
237
+ **kw: Any,
238
+ ) -> tuple[Any | Exception, bool]:
239
+ """Internal dispatch to hook or stream handler."""
240
+ if hp_ is None and ct_ is None:
241
+ raise RuntimeError("Either hook_type or chunk_type must be provided")
242
+ if hp_ and (self._hooks.get(hp_)):
243
+ validate_hooks({hp_: self._hooks[hp_]})
244
+ h = get_handler(self._hooks, hp_, True)
245
+ if h is not None:
246
+ return await h(ev_, **kw)
247
+ raise RuntimeError(f"No handler found for hook phase: {hp_}")
248
+ elif not ct_:
249
+ raise RuntimeError("Hook type is required when chunk_type is not provided")
250
+ else:
251
+ validate_stream_handlers({ct_: self._stream_handlers.get(ct_)})
252
+ h = get_handler(self._stream_handlers, ct_, True)
253
+ if h is not None:
254
+ return await h(ev_, ct_, ch_, **kw)
255
+ raise RuntimeError(f"No handler found for chunk type: {ct_}")
256
+
257
+ async def _call_stream_handler(
258
+ self,
259
+ ct_: str | type,
260
+ ch_: Any,
261
+ ev_: Any,
262
+ /,
263
+ **kw: Any,
264
+ ) -> Any:
265
+ """Internal dispatch to stream handler by chunk type."""
266
+ validate_stream_handlers({ct_: self._stream_handlers.get(ct_)})
267
+ handler = get_handler(self._stream_handlers, ct_, True)
268
+ if handler is not None:
269
+ return await handler(ev_, ct_, ch_, **kw)
270
+ raise RuntimeError(f"No stream handler found for chunk type: {ct_}")
271
+
272
+ async def pre_event_create(
273
+ self, event_type: type[E], /, exit: bool = False, **kw: Any
274
+ ) -> tuple[Any, bool, EventStatus]:
275
+ """Execute PreEventCreate hook before Event instantiation.
276
+
277
+ Args:
278
+ event_type: Event class being created.
279
+ exit: If True and hook raises, signal caller to halt.
280
+ **kw: Passed to hook handler.
281
+
282
+ Returns:
283
+ Tuple of (result|exception, should_exit, status).
284
+ """
285
+ try:
286
+ res = await self._call(
287
+ HookPhase.PreEventCreate,
288
+ None,
289
+ None,
290
+ event_type,
291
+ exit=exit,
292
+ **kw,
293
+ )
294
+ return (res, False, EventStatus.COMPLETED)
295
+ except concurrency.get_cancelled_exc_class() as e:
296
+ return ((Undefined, e), True, EventStatus.CANCELLED)
297
+ except Exception as e:
298
+ return (e, exit, EventStatus.CANCELLED)
299
+
300
+ async def pre_invocation(
301
+ self, event: E, /, exit: bool = False, **kw: Any
302
+ ) -> tuple[Any, bool, EventStatus]:
303
+ """Execute PreInvocation hook before Event.invoke().
304
+
305
+ Args:
306
+ event: Event instance about to be invoked.
307
+ exit: If True and hook raises, signal caller to halt.
308
+ **kw: Passed to hook handler.
309
+
310
+ Returns:
311
+ Tuple of (result|exception, should_exit, status).
312
+ """
313
+ try:
314
+ res = await self._call(
315
+ HookPhase.PreInvocation,
316
+ None,
317
+ None,
318
+ event,
319
+ exit=exit,
320
+ **kw,
321
+ )
322
+ return (res, False, EventStatus.COMPLETED)
323
+ except concurrency.get_cancelled_exc_class() as e:
324
+ return ((Undefined, e), True, EventStatus.CANCELLED)
325
+ except Exception as e:
326
+ return (e, exit, EventStatus.CANCELLED)
327
+
328
+ async def post_invocation(
329
+ self, event: E, /, exit: bool = False, **kw: Any
330
+ ) -> tuple[Any, bool, EventStatus]:
331
+ """Execute PostInvocation hook after Event.invoke() completes.
332
+
333
+ Args:
334
+ event: Event instance with execution results populated.
335
+ exit: If True and hook raises, signal caller to halt.
336
+ **kw: Passed to hook handler.
337
+
338
+ Returns:
339
+ Tuple of (result|exception, should_exit, status). Status is ABORTED on error.
340
+ """
341
+ try:
342
+ res = await self._call(
343
+ HookPhase.PostInvocation,
344
+ None,
345
+ None,
346
+ event,
347
+ exit=exit,
348
+ **kw,
349
+ )
350
+ return (res, False, EventStatus.COMPLETED)
351
+ except concurrency.get_cancelled_exc_class() as e:
352
+ return ((Undefined, e), True, EventStatus.CANCELLED)
353
+ except Exception as e:
354
+ return (e, exit, EventStatus.ABORTED)
355
+
356
+ async def handle_streaming_chunk(
357
+ self,
358
+ chunk_type: str | type | None,
359
+ chunk: Any,
360
+ /,
361
+ exit: bool = False,
362
+ **kw: Any,
363
+ ) -> tuple[Any, bool, EventStatus | None]:
364
+ """Process a streaming chunk via registered handler.
365
+
366
+ Args:
367
+ chunk_type: Type identifier for handler lookup (str name or class).
368
+ chunk: The chunk data to process.
369
+ exit: If True and handler raises, signal caller to halt.
370
+ **kw: Passed to handler.
371
+
372
+ Returns:
373
+ Tuple of (result|exception, should_exit, status|None).
374
+
375
+ Raises:
376
+ ValueError: If chunk_type is None.
377
+ """
378
+ if chunk_type is None:
379
+ raise ValueError("chunk_type cannot be None for streaming chunks")
380
+ try:
381
+ res = await self._call_stream_handler(
382
+ chunk_type,
383
+ chunk,
384
+ None,
385
+ exit=exit,
386
+ **kw,
387
+ )
388
+ return (res, False, None)
389
+ except concurrency.get_cancelled_exc_class() as e:
390
+ return ((Undefined, e), True, EventStatus.CANCELLED)
391
+ except Exception as e:
392
+ return (e, exit, EventStatus.ABORTED)
393
+
394
+ async def call(
395
+ self,
396
+ event_like: Event | type[Event],
397
+ /,
398
+ *,
399
+ hook_phase: HookPhase | None = None,
400
+ chunk_type: str | type | None = None,
401
+ chunk: Any = None,
402
+ exit: bool = False,
403
+ **kw: Any,
404
+ ) -> (
405
+ tuple[tuple[Any, bool, EventStatus], dict[str, Any]] | tuple[Any, bool, EventStatus | None]
406
+ ):
407
+ """Call a hook or stream handler.
408
+
409
+ If method is provided, it will call the corresponding hook.
410
+ If chunk_type is provided, it will call the corresponding stream handler.
411
+ If both are provided, method will be used.
412
+ """
413
+ if hook_phase is None and chunk_type is None:
414
+ raise ValueError("Either method or chunk_type must be provided")
415
+
416
+ if hook_phase:
417
+ meta: dict[str, Any] = {"kron_class": event_like.class_name(full=True)}
418
+ match hook_phase:
419
+ case HookPhase.PreEventCreate | HookPhase.PreEventCreate.value:
420
+ # For pre_event_create, event_like should be a type
421
+ if isinstance(event_like, type):
422
+ return (
423
+ await self.pre_event_create(event_like, exit=exit, **kw),
424
+ meta,
425
+ )
426
+ # Fall through to treat as event instance
427
+ return (
428
+ await self.pre_event_create(type(event_like), exit=exit, **kw),
429
+ meta,
430
+ )
431
+ case HookPhase.PreInvocation | HookPhase.PreInvocation.value:
432
+ # For pre_invocation, event_like should be an instance
433
+ if isinstance(event_like, Event):
434
+ meta["event_id"] = str(event_like.id)
435
+ meta["event_created_at"] = event_like.created_at.isoformat()
436
+ return (
437
+ await self.pre_invocation(event_like, exit=exit, **kw),
438
+ meta,
439
+ )
440
+ raise TypeError("PreInvocation requires an Event instance, not a type")
441
+ case HookPhase.PostInvocation | HookPhase.PostInvocation.value:
442
+ # For post_invocation, event_like should be an instance
443
+ if isinstance(event_like, Event):
444
+ meta["event_id"] = str(event_like.id)
445
+ meta["event_created_at"] = event_like.created_at.isoformat()
446
+ return (
447
+ await self.post_invocation(event_like, exit=exit, **kw),
448
+ meta,
449
+ )
450
+ raise TypeError("PostInvocation requires an Event instance, not a type")
451
+ return await self.handle_streaming_chunk(chunk_type, chunk, exit=exit, **kw)
452
+
453
+ def _can_handle(
454
+ self,
455
+ /,
456
+ *,
457
+ hp_: HookPhase | None = None,
458
+ ct_=None,
459
+ ) -> bool:
460
+ """Check if the registry can handle the given event or chunk type."""
461
+ if hp_:
462
+ return hp_ in self._hooks
463
+ if ct_:
464
+ return ct_ in self._stream_handlers
465
+ return False
466
+
467
+
468
+ class HookBroadcaster(Broadcaster):
469
+ """Broadcaster specialized for HookEvent distribution."""
470
+
471
+ _event_type: ClassVar[type[HookEvent]] = HookEvent