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/services/hook.py
ADDED
|
@@ -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
|