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/protocols.py
ADDED
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
# Copyright (c) 2025 - 2026, HaiyangLi <quantocean.li at gmail dot com>
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""Runtime-checkable protocols and @implements decorator.
|
|
5
|
+
|
|
6
|
+
Protocols define structural interfaces (duck typing) with isinstance() support.
|
|
7
|
+
Use @implements(Protocol) to declare and validate protocol implementations.
|
|
8
|
+
|
|
9
|
+
Example:
|
|
10
|
+
@implements(Serializable, Invocable)
|
|
11
|
+
class MyClass:
|
|
12
|
+
def to_dict(self, **kwargs): ...
|
|
13
|
+
async def invoke(self): ...
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import inspect
|
|
17
|
+
import warnings
|
|
18
|
+
from typing import Any, Literal, Protocol, runtime_checkable
|
|
19
|
+
from uuid import UUID
|
|
20
|
+
|
|
21
|
+
__all__ = (
|
|
22
|
+
"Allowable",
|
|
23
|
+
"Communicatable",
|
|
24
|
+
"Containable",
|
|
25
|
+
"Deserializable",
|
|
26
|
+
"Hashable",
|
|
27
|
+
"Invocable",
|
|
28
|
+
"Observable",
|
|
29
|
+
"Serializable",
|
|
30
|
+
"SignatureMismatchError",
|
|
31
|
+
"implements",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class SignatureMismatchError(TypeError):
|
|
36
|
+
"""@implements detected incompatible method signature."""
|
|
37
|
+
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@runtime_checkable
|
|
42
|
+
class ObservableProto(Protocol):
|
|
43
|
+
"""Has unique UUID identity. Check: isinstance(obj, Observable)."""
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def id(self) -> UUID:
|
|
47
|
+
"""Unique identifier for this instance."""
|
|
48
|
+
...
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@runtime_checkable
|
|
52
|
+
class Serializable(Protocol):
|
|
53
|
+
"""Can serialize to dict. Implement to_dict(**kwargs) -> dict."""
|
|
54
|
+
|
|
55
|
+
def to_dict(self, **kwargs: Any) -> dict[str, Any]:
|
|
56
|
+
"""Serialize to dict. kwargs: mode, format, etc."""
|
|
57
|
+
...
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@runtime_checkable
|
|
61
|
+
class Deserializable(Protocol):
|
|
62
|
+
"""Can deserialize from dict. Implement classmethod from_dict(data, **kwargs)."""
|
|
63
|
+
|
|
64
|
+
@classmethod
|
|
65
|
+
def from_dict(cls, data: dict[str, Any], **kwargs: Any) -> Any:
|
|
66
|
+
"""Create instance from dict."""
|
|
67
|
+
...
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@runtime_checkable
|
|
71
|
+
class Containable(Protocol):
|
|
72
|
+
"""Supports 'in' operator. Implement __contains__(item) -> bool."""
|
|
73
|
+
|
|
74
|
+
def __contains__(self, item: Any) -> bool:
|
|
75
|
+
"""Check membership (by UUID or instance)."""
|
|
76
|
+
...
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@runtime_checkable
|
|
80
|
+
class Invocable(Protocol):
|
|
81
|
+
"""Async executable. Implement async invoke() -> Any."""
|
|
82
|
+
|
|
83
|
+
async def invoke(self) -> Any:
|
|
84
|
+
"""Execute and return result."""
|
|
85
|
+
...
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@runtime_checkable
|
|
89
|
+
class Hashable(Protocol):
|
|
90
|
+
"""Hashable for sets/dicts. Implement __hash__() -> int."""
|
|
91
|
+
|
|
92
|
+
def __hash__(self) -> int:
|
|
93
|
+
"""Hash value (typically based on immutable id)."""
|
|
94
|
+
...
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@runtime_checkable
|
|
98
|
+
class Allowable(Protocol):
|
|
99
|
+
"""Has defined allowed values. Implement allowed() -> set[str]."""
|
|
100
|
+
|
|
101
|
+
def allowed(self) -> set[str]:
|
|
102
|
+
"""Set of allowed keys/values."""
|
|
103
|
+
...
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@runtime_checkable
|
|
107
|
+
class Communicatable(Protocol):
|
|
108
|
+
"""Entity with mailbox for message exchange.
|
|
109
|
+
|
|
110
|
+
Enables multi-agent communication. Higher layers define Agent, Branch, etc.
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def id(self) -> UUID:
|
|
115
|
+
"""Entity identifier for routing."""
|
|
116
|
+
...
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def mailbox(self) -> Any:
|
|
120
|
+
"""Mailbox for send/receive operations."""
|
|
121
|
+
...
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
Observable = ObservableProto
|
|
125
|
+
"""Alias: Observable = ObservableProto."""
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _get_signature_params(func: Any) -> dict[str, inspect.Parameter] | None:
|
|
129
|
+
"""Extract params from callable, excluding self/cls. Returns None if not introspectable."""
|
|
130
|
+
if isinstance(func, (classmethod, staticmethod)):
|
|
131
|
+
func = func.__func__
|
|
132
|
+
|
|
133
|
+
if isinstance(func, property):
|
|
134
|
+
return None
|
|
135
|
+
|
|
136
|
+
if not callable(func):
|
|
137
|
+
return None
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
sig = inspect.signature(func)
|
|
141
|
+
except (ValueError, TypeError):
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
params = {}
|
|
145
|
+
for name, param in sig.parameters.items():
|
|
146
|
+
if name in ("self", "cls"):
|
|
147
|
+
continue
|
|
148
|
+
params[name] = param
|
|
149
|
+
|
|
150
|
+
return params
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _check_signature_compatibility(
|
|
154
|
+
protocol_params: dict[str, inspect.Parameter],
|
|
155
|
+
impl_params: dict[str, inspect.Parameter],
|
|
156
|
+
) -> list[str]:
|
|
157
|
+
"""Check impl signature compatibility with protocol. Returns error messages.
|
|
158
|
+
|
|
159
|
+
Rules: impl must accept required protocol params; may have extra optionals;
|
|
160
|
+
*args/**kwargs can satisfy protocol params; if protocol has **kwargs, impl must too.
|
|
161
|
+
"""
|
|
162
|
+
errors = []
|
|
163
|
+
|
|
164
|
+
impl_has_var_positional = any(
|
|
165
|
+
p.kind == inspect.Parameter.VAR_POSITIONAL for p in impl_params.values()
|
|
166
|
+
)
|
|
167
|
+
impl_has_var_keyword = any(
|
|
168
|
+
p.kind == inspect.Parameter.VAR_KEYWORD for p in impl_params.values()
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
# Check if protocol has *args or **kwargs
|
|
172
|
+
proto_has_var_positional = any(
|
|
173
|
+
p.kind == inspect.Parameter.VAR_POSITIONAL for p in protocol_params.values()
|
|
174
|
+
)
|
|
175
|
+
proto_has_var_keyword = any(
|
|
176
|
+
p.kind == inspect.Parameter.VAR_KEYWORD for p in protocol_params.values()
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
# If protocol accepts **kwargs, implementation must also accept them
|
|
180
|
+
# Otherwise callers passing kwargs (allowed by protocol) will fail
|
|
181
|
+
if proto_has_var_keyword and not impl_has_var_keyword:
|
|
182
|
+
errors.append(" - 'kwargs': protocol accepts **kwargs but implementation doesn't")
|
|
183
|
+
|
|
184
|
+
# If protocol accepts *args, implementation must also accept them
|
|
185
|
+
if proto_has_var_positional and not impl_has_var_positional:
|
|
186
|
+
errors.append(" - 'args': protocol accepts *args but implementation doesn't")
|
|
187
|
+
|
|
188
|
+
# For each protocol parameter, verify implementation can accept it
|
|
189
|
+
for param_name, proto_param in protocol_params.items():
|
|
190
|
+
# Skip VAR_POSITIONAL and VAR_KEYWORD in protocol (handled above)
|
|
191
|
+
if proto_param.kind in (
|
|
192
|
+
inspect.Parameter.VAR_POSITIONAL,
|
|
193
|
+
inspect.Parameter.VAR_KEYWORD,
|
|
194
|
+
):
|
|
195
|
+
continue
|
|
196
|
+
|
|
197
|
+
# Check if implementation has this parameter
|
|
198
|
+
if param_name in impl_params:
|
|
199
|
+
impl_param = impl_params[param_name]
|
|
200
|
+
|
|
201
|
+
# Check parameter kind compatibility
|
|
202
|
+
# Implementation can be more flexible (e.g., POSITIONAL_OR_KEYWORD
|
|
203
|
+
# can accept POSITIONAL_ONLY)
|
|
204
|
+
proto_kind = proto_param.kind
|
|
205
|
+
impl_kind = impl_param.kind
|
|
206
|
+
|
|
207
|
+
# VAR_POSITIONAL/VAR_KEYWORD in impl can accept anything
|
|
208
|
+
if impl_kind in (
|
|
209
|
+
inspect.Parameter.VAR_POSITIONAL,
|
|
210
|
+
inspect.Parameter.VAR_KEYWORD,
|
|
211
|
+
):
|
|
212
|
+
continue
|
|
213
|
+
|
|
214
|
+
# Required in protocol but has default in impl is OK
|
|
215
|
+
# (implementation is more lenient)
|
|
216
|
+
|
|
217
|
+
# If protocol param is required, impl param should not require
|
|
218
|
+
# something the protocol doesn't provide
|
|
219
|
+
if proto_param.default is inspect.Parameter.empty:
|
|
220
|
+
# Protocol requires this param
|
|
221
|
+
# Implementation can either:
|
|
222
|
+
# 1. Also require it (empty default)
|
|
223
|
+
# 2. Make it optional (has default) - this is fine
|
|
224
|
+
|
|
225
|
+
# Check if impl param is keyword-only but protocol is positional
|
|
226
|
+
if impl_kind == inspect.Parameter.KEYWORD_ONLY and proto_kind in (
|
|
227
|
+
inspect.Parameter.POSITIONAL_ONLY,
|
|
228
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
229
|
+
):
|
|
230
|
+
errors.append(
|
|
231
|
+
f" - '{param_name}': protocol allows positional, "
|
|
232
|
+
f"but implementation requires keyword-only"
|
|
233
|
+
)
|
|
234
|
+
else:
|
|
235
|
+
# Protocol param is optional (has default)
|
|
236
|
+
# Implementation should NOT make it required (tightening contract)
|
|
237
|
+
if impl_param.default is inspect.Parameter.empty:
|
|
238
|
+
errors.append(
|
|
239
|
+
f" - '{param_name}': protocol makes this optional, "
|
|
240
|
+
f"but implementation requires it"
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
else:
|
|
244
|
+
# Parameter not in implementation by name
|
|
245
|
+
# Check if it can be satisfied by *args or **kwargs
|
|
246
|
+
proto_kind = proto_param.kind
|
|
247
|
+
|
|
248
|
+
if proto_kind in (
|
|
249
|
+
inspect.Parameter.POSITIONAL_ONLY,
|
|
250
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
251
|
+
):
|
|
252
|
+
# Can be satisfied by *args or **kwargs
|
|
253
|
+
if not (impl_has_var_positional or impl_has_var_keyword):
|
|
254
|
+
errors.append(
|
|
255
|
+
f" - '{param_name}': required by protocol but not in implementation"
|
|
256
|
+
)
|
|
257
|
+
elif proto_kind == inspect.Parameter.KEYWORD_ONLY: # noqa: SIM102
|
|
258
|
+
# Can only be satisfied by **kwargs
|
|
259
|
+
if not impl_has_var_keyword:
|
|
260
|
+
errors.append(
|
|
261
|
+
f" - '{param_name}': keyword-only param required by protocol "
|
|
262
|
+
f"but not in implementation"
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
# Check if implementation has required parameters that protocol doesn't provide
|
|
266
|
+
for param_name, impl_param in impl_params.items():
|
|
267
|
+
# Skip *args and **kwargs
|
|
268
|
+
if impl_param.kind in (
|
|
269
|
+
inspect.Parameter.VAR_POSITIONAL,
|
|
270
|
+
inspect.Parameter.VAR_KEYWORD,
|
|
271
|
+
):
|
|
272
|
+
continue
|
|
273
|
+
|
|
274
|
+
# If implementation requires a parameter (no default)
|
|
275
|
+
if impl_param.default is inspect.Parameter.empty: # noqa: SIM102
|
|
276
|
+
# Protocol must also have this parameter
|
|
277
|
+
if param_name not in protocol_params:
|
|
278
|
+
# Check if protocol has *args or **kwargs that could provide it
|
|
279
|
+
proto_has_var_positional = any(
|
|
280
|
+
p.kind == inspect.Parameter.VAR_POSITIONAL for p in protocol_params.values()
|
|
281
|
+
)
|
|
282
|
+
proto_has_var_keyword = any(
|
|
283
|
+
p.kind == inspect.Parameter.VAR_KEYWORD for p in protocol_params.values()
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
can_satisfy = False
|
|
287
|
+
if impl_param.kind in (
|
|
288
|
+
inspect.Parameter.POSITIONAL_ONLY,
|
|
289
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
290
|
+
):
|
|
291
|
+
can_satisfy = proto_has_var_positional or proto_has_var_keyword
|
|
292
|
+
elif impl_param.kind == inspect.Parameter.KEYWORD_ONLY:
|
|
293
|
+
can_satisfy = proto_has_var_keyword
|
|
294
|
+
|
|
295
|
+
if not can_satisfy:
|
|
296
|
+
errors.append(
|
|
297
|
+
f" - '{param_name}': implementation requires this param "
|
|
298
|
+
f"but protocol doesn't provide it"
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
return errors
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def implements(
|
|
305
|
+
*protocols: type,
|
|
306
|
+
signature_check: Literal["error", "warn", "skip"] = "warn",
|
|
307
|
+
allow_inherited: bool = False,
|
|
308
|
+
):
|
|
309
|
+
"""Decorator to declare and validate protocol implementations.
|
|
310
|
+
|
|
311
|
+
Validates members exist (in class body by default) and optionally checks
|
|
312
|
+
signature compatibility. Stores validated protocols in cls.__protocols__.
|
|
313
|
+
|
|
314
|
+
Args:
|
|
315
|
+
*protocols: Protocol classes to implement.
|
|
316
|
+
signature_check: "error"=raise, "warn"=warning, "skip"=no check.
|
|
317
|
+
allow_inherited: Accept inherited implementations (default: require in class body).
|
|
318
|
+
|
|
319
|
+
Raises:
|
|
320
|
+
TypeError: Required member missing.
|
|
321
|
+
SignatureMismatchError: If signature_check="error" and mismatch.
|
|
322
|
+
|
|
323
|
+
Example:
|
|
324
|
+
@implements(Serializable, signature_check="error")
|
|
325
|
+
class MyModel:
|
|
326
|
+
def to_dict(self, **kwargs): return {"id": self.id}
|
|
327
|
+
"""
|
|
328
|
+
|
|
329
|
+
def decorator(cls):
|
|
330
|
+
all_signature_errors = []
|
|
331
|
+
|
|
332
|
+
# Validate that all protocol members are defined in class body
|
|
333
|
+
for protocol in protocols:
|
|
334
|
+
# Get protocol members from protocol class annotations
|
|
335
|
+
protocol_members = {}
|
|
336
|
+
for name, obj in inspect.getmembers(protocol):
|
|
337
|
+
if name.startswith("_"):
|
|
338
|
+
continue
|
|
339
|
+
# Include methods, properties, classmethods
|
|
340
|
+
if callable(obj) or isinstance(obj, (property, classmethod)):
|
|
341
|
+
protocol_members[name] = obj
|
|
342
|
+
|
|
343
|
+
# Check each required member exists
|
|
344
|
+
for member_name, protocol_member in protocol_members.items():
|
|
345
|
+
# Check if member is in class body or inherited (based on allow_inherited)
|
|
346
|
+
# For Pydantic models, also check __annotations__ for fields
|
|
347
|
+
in_class_body = member_name in cls.__dict__
|
|
348
|
+
|
|
349
|
+
# For Pydantic models, check if it's a field annotation
|
|
350
|
+
if not in_class_body and hasattr(cls, "__annotations__"):
|
|
351
|
+
in_class_body = member_name in cls.__annotations__
|
|
352
|
+
|
|
353
|
+
# Check if member exists anywhere (including inherited)
|
|
354
|
+
has_member = hasattr(cls, member_name)
|
|
355
|
+
|
|
356
|
+
if allow_inherited:
|
|
357
|
+
# When inheritance is allowed, just check the member exists
|
|
358
|
+
if not has_member:
|
|
359
|
+
protocol_name = protocol.__name__
|
|
360
|
+
raise TypeError(
|
|
361
|
+
f"{cls.__name__} declares @implements({protocol_name}) but "
|
|
362
|
+
f"'{member_name}' is not defined or inherited"
|
|
363
|
+
)
|
|
364
|
+
else:
|
|
365
|
+
# Strict mode: require member in class body
|
|
366
|
+
if not in_class_body:
|
|
367
|
+
protocol_name = protocol.__name__
|
|
368
|
+
raise TypeError(
|
|
369
|
+
f"{cls.__name__} declares @implements({protocol_name}) but does not "
|
|
370
|
+
f"define '{member_name}' in its class body. "
|
|
371
|
+
f"Use allow_inherited=True to accept inherited implementations."
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
# Signature checking (if enabled and member exists)
|
|
375
|
+
if signature_check != "skip":
|
|
376
|
+
# Get the actual implementation (from class body or inherited)
|
|
377
|
+
if in_class_body:
|
|
378
|
+
impl_member = cls.__dict__.get(member_name)
|
|
379
|
+
else:
|
|
380
|
+
impl_member = getattr(cls, member_name, None)
|
|
381
|
+
|
|
382
|
+
if impl_member is None and hasattr(cls, "__annotations__"):
|
|
383
|
+
# Pydantic field - skip signature check (it's a field, not method)
|
|
384
|
+
continue
|
|
385
|
+
|
|
386
|
+
# Get signatures for comparison
|
|
387
|
+
proto_params = _get_signature_params(protocol_member)
|
|
388
|
+
impl_params = _get_signature_params(impl_member)
|
|
389
|
+
|
|
390
|
+
# Only check if both have extractable signatures
|
|
391
|
+
if proto_params is not None and impl_params is not None:
|
|
392
|
+
errors = _check_signature_compatibility(
|
|
393
|
+
proto_params,
|
|
394
|
+
impl_params,
|
|
395
|
+
)
|
|
396
|
+
if errors:
|
|
397
|
+
error_msg = (
|
|
398
|
+
f"{cls.__name__}.{member_name} signature incompatible "
|
|
399
|
+
f"with {protocol.__name__}.{member_name}:\n" + "\n".join(errors)
|
|
400
|
+
)
|
|
401
|
+
all_signature_errors.append(error_msg)
|
|
402
|
+
|
|
403
|
+
# Handle signature errors based on signature_check mode
|
|
404
|
+
if all_signature_errors:
|
|
405
|
+
full_message = "\n\n".join(all_signature_errors)
|
|
406
|
+
if signature_check == "error":
|
|
407
|
+
raise SignatureMismatchError(full_message)
|
|
408
|
+
elif signature_check == "warn":
|
|
409
|
+
warnings.warn(full_message, stacklevel=2)
|
|
410
|
+
|
|
411
|
+
cls.__protocols__ = protocols
|
|
412
|
+
return cls
|
|
413
|
+
|
|
414
|
+
return decorator
|
kronos/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Copyright (c) 2025 - 2026, HaiyangLi <quantocean.li at gmail dot com>
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""Services module: iModel, ServiceBackend, hooks, and registry.
|
|
5
|
+
|
|
6
|
+
Core exports:
|
|
7
|
+
- iModel: Unified service interface with rate limiting and hooks
|
|
8
|
+
- ServiceBackend/Endpoint: Backend abstractions for API calls
|
|
9
|
+
- HookRegistry/HookEvent/HookPhase: Lifecycle hook system
|
|
10
|
+
- ServiceRegistry: O(1) name-based service lookup
|
|
11
|
+
|
|
12
|
+
Uses lazy loading for fast import.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from typing import TYPE_CHECKING
|
|
18
|
+
|
|
19
|
+
# Lazy import mapping
|
|
20
|
+
_LAZY_IMPORTS: dict[str, tuple[str, str]] = {
|
|
21
|
+
"Calling": ("kronos.services.backend", "Calling"),
|
|
22
|
+
"NormalizedResponse": ("kronos.services.backend", "NormalizedResponse"),
|
|
23
|
+
"ServiceBackend": ("kronos.services.backend", "ServiceBackend"),
|
|
24
|
+
"ServiceConfig": ("kronos.services.backend", "ServiceConfig"),
|
|
25
|
+
"ServiceRegistry": ("kronos.services.registry", "ServiceRegistry"),
|
|
26
|
+
"iModel": ("kronos.services.imodel", "iModel"),
|
|
27
|
+
"Endpoint": ("kronos.services.endpoint", "Endpoint"),
|
|
28
|
+
"EndpointConfig": ("kronos.services.endpoint", "EndpointConfig"),
|
|
29
|
+
"APICalling": ("kronos.services.endpoint", "APICalling"),
|
|
30
|
+
"HookRegistry": ("kronos.services.hook", "HookRegistry"),
|
|
31
|
+
"HookEvent": ("kronos.services.hook", "HookEvent"),
|
|
32
|
+
"HookPhase": ("kronos.services.hook", "HookPhase"),
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
_LOADED: dict[str, object] = {}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def __getattr__(name: str) -> object:
|
|
39
|
+
"""Lazy import attributes on first access."""
|
|
40
|
+
if name in _LOADED:
|
|
41
|
+
return _LOADED[name]
|
|
42
|
+
|
|
43
|
+
if name in _LAZY_IMPORTS:
|
|
44
|
+
from importlib import import_module
|
|
45
|
+
|
|
46
|
+
module_name, attr_name = _LAZY_IMPORTS[name]
|
|
47
|
+
module = import_module(module_name)
|
|
48
|
+
value = getattr(module, attr_name)
|
|
49
|
+
_LOADED[name] = value
|
|
50
|
+
return value
|
|
51
|
+
|
|
52
|
+
raise AttributeError(f"module 'kronos.services' has no attribute {name!r}")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def __dir__() -> list[str]:
|
|
56
|
+
"""Return all available attributes for autocomplete."""
|
|
57
|
+
return list(__all__)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# TYPE_CHECKING block for static analysis
|
|
61
|
+
if TYPE_CHECKING:
|
|
62
|
+
from .backend import Calling, NormalizedResponse, ServiceBackend, ServiceConfig
|
|
63
|
+
from .endpoint import APICalling, Endpoint, EndpointConfig
|
|
64
|
+
from .hook import HookEvent, HookPhase, HookRegistry
|
|
65
|
+
from .imodel import iModel
|
|
66
|
+
from .registry import ServiceRegistry
|
|
67
|
+
|
|
68
|
+
__all__ = (
|
|
69
|
+
"APICalling",
|
|
70
|
+
"Calling",
|
|
71
|
+
"Endpoint",
|
|
72
|
+
"EndpointConfig",
|
|
73
|
+
"HookEvent",
|
|
74
|
+
"HookPhase",
|
|
75
|
+
"HookRegistry",
|
|
76
|
+
"NormalizedResponse",
|
|
77
|
+
"ServiceBackend",
|
|
78
|
+
"ServiceConfig",
|
|
79
|
+
"ServiceRegistry",
|
|
80
|
+
"iModel",
|
|
81
|
+
)
|