krons 0.1.1__py3-none-any.whl → 0.2.1__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.
- krons/__init__.py +49 -0
- krons/agent/__init__.py +144 -0
- krons/agent/mcps/__init__.py +14 -0
- krons/agent/mcps/loader.py +287 -0
- krons/agent/mcps/wrapper.py +799 -0
- krons/agent/message/__init__.py +20 -0
- krons/agent/message/action.py +69 -0
- krons/agent/message/assistant.py +52 -0
- krons/agent/message/common.py +49 -0
- krons/agent/message/instruction.py +130 -0
- krons/agent/message/prepare_msg.py +187 -0
- krons/agent/message/role.py +53 -0
- krons/agent/message/system.py +53 -0
- krons/agent/operations/__init__.py +82 -0
- krons/agent/operations/act.py +100 -0
- krons/agent/operations/generate.py +145 -0
- krons/agent/operations/llm_reparse.py +89 -0
- krons/agent/operations/operate.py +247 -0
- krons/agent/operations/parse.py +243 -0
- krons/agent/operations/react.py +286 -0
- krons/agent/operations/specs.py +235 -0
- krons/agent/operations/structure.py +151 -0
- krons/agent/operations/utils.py +79 -0
- krons/agent/providers/__init__.py +17 -0
- krons/agent/providers/anthropic_messages.py +146 -0
- krons/agent/providers/claude_code.py +276 -0
- krons/agent/providers/gemini.py +268 -0
- krons/agent/providers/match.py +75 -0
- krons/agent/providers/oai_chat.py +174 -0
- krons/agent/third_party/__init__.py +2 -0
- krons/agent/third_party/anthropic_models.py +154 -0
- krons/agent/third_party/claude_code.py +682 -0
- krons/agent/third_party/gemini_models.py +508 -0
- krons/agent/third_party/openai_models.py +295 -0
- krons/agent/tool.py +291 -0
- krons/core/__init__.py +56 -74
- krons/core/base/__init__.py +121 -0
- krons/core/{broadcaster.py → base/broadcaster.py} +7 -3
- krons/core/{element.py → base/element.py} +13 -5
- krons/core/{event.py → base/event.py} +39 -6
- krons/core/{eventbus.py → base/eventbus.py} +3 -1
- krons/core/{flow.py → base/flow.py} +11 -4
- krons/core/{graph.py → base/graph.py} +24 -8
- krons/core/{node.py → base/node.py} +44 -19
- krons/core/{pile.py → base/pile.py} +22 -8
- krons/core/{processor.py → base/processor.py} +21 -7
- krons/core/{progression.py → base/progression.py} +3 -1
- krons/{specs → core/specs}/__init__.py +0 -5
- krons/{specs → core/specs}/adapters/dataclass_field.py +16 -8
- krons/{specs → core/specs}/adapters/pydantic_adapter.py +11 -5
- krons/{specs → core/specs}/adapters/sql_ddl.py +14 -8
- krons/{specs → core/specs}/catalog/__init__.py +2 -2
- krons/{specs → core/specs}/catalog/_audit.py +2 -2
- krons/{specs → core/specs}/catalog/_common.py +2 -2
- krons/{specs → core/specs}/catalog/_content.py +4 -4
- krons/{specs → core/specs}/catalog/_enforcement.py +3 -3
- krons/{specs → core/specs}/factory.py +5 -5
- krons/{specs → core/specs}/operable.py +8 -2
- krons/{specs → core/specs}/protocol.py +4 -2
- krons/{specs → core/specs}/spec.py +23 -11
- krons/{types → core/types}/base.py +4 -2
- krons/{types → core/types}/db_types.py +2 -2
- krons/errors.py +13 -13
- krons/protocols.py +9 -4
- krons/resource/__init__.py +89 -0
- krons/{services → resource}/backend.py +48 -22
- krons/{services → resource}/endpoint.py +28 -14
- krons/{services → resource}/hook.py +20 -7
- krons/{services → resource}/imodel.py +46 -28
- krons/{services → resource}/registry.py +26 -24
- krons/{services → resource}/utilities/rate_limited_executor.py +7 -3
- krons/{services → resource}/utilities/rate_limiter.py +3 -1
- krons/{services → resource}/utilities/resilience.py +15 -5
- krons/resource/utilities/token_calculator.py +185 -0
- krons/session/__init__.py +12 -17
- krons/session/constraints.py +70 -0
- krons/session/exchange.py +11 -3
- krons/session/message.py +3 -1
- krons/session/registry.py +35 -0
- krons/session/session.py +165 -174
- krons/utils/__init__.py +45 -0
- krons/utils/_function_arg_parser.py +99 -0
- krons/utils/_pythonic_function_call.py +249 -0
- krons/utils/_to_list.py +9 -3
- krons/utils/_utils.py +6 -2
- krons/utils/concurrency/_async_call.py +4 -2
- krons/utils/concurrency/_errors.py +3 -1
- krons/utils/concurrency/_patterns.py +3 -1
- krons/utils/concurrency/_resource_tracker.py +6 -2
- krons/utils/display.py +257 -0
- krons/utils/fuzzy/__init__.py +6 -1
- krons/utils/fuzzy/_fuzzy_match.py +14 -8
- krons/utils/fuzzy/_string_similarity.py +3 -1
- krons/utils/fuzzy/_to_dict.py +3 -1
- krons/utils/schemas/__init__.py +26 -0
- krons/utils/schemas/_breakdown_pydantic_annotation.py +131 -0
- krons/utils/schemas/_formatter.py +72 -0
- krons/utils/schemas/_minimal_yaml.py +151 -0
- krons/utils/schemas/_typescript.py +153 -0
- krons/utils/validators/__init__.py +3 -0
- krons/utils/validators/_validate_image_url.py +56 -0
- krons/work/__init__.py +115 -0
- krons/work/engine.py +333 -0
- krons/work/form.py +242 -0
- krons/{operations → work/operations}/__init__.py +7 -4
- krons/{operations → work/operations}/builder.py +1 -1
- krons/{enforcement → work/operations}/context.py +36 -5
- krons/{operations → work/operations}/flow.py +13 -5
- krons/{operations → work/operations}/node.py +45 -43
- krons/work/operations/registry.py +103 -0
- krons/work/report.py +268 -0
- krons/work/rules/__init__.py +47 -0
- krons/{enforcement → work/rules}/common/boolean.py +3 -1
- krons/{enforcement → work/rules}/common/choice.py +9 -3
- krons/{enforcement → work/rules}/common/number.py +3 -1
- krons/{enforcement → work/rules}/common/string.py +9 -3
- krons/{enforcement → work/rules}/rule.py +1 -1
- krons/{enforcement → work/rules}/validator.py +20 -5
- krons/work/worker.py +266 -0
- {krons-0.1.1.dist-info → krons-0.2.1.dist-info}/METADATA +15 -1
- krons-0.2.1.dist-info/RECORD +151 -0
- krons/enforcement/__init__.py +0 -57
- krons/enforcement/policy.py +0 -80
- krons/enforcement/service.py +0 -370
- krons/operations/registry.py +0 -92
- krons/services/__init__.py +0 -81
- krons/specs/phrase.py +0 -405
- krons-0.1.1.dist-info/RECORD +0 -101
- /krons/{specs → core/specs}/adapters/__init__.py +0 -0
- /krons/{specs → core/specs}/adapters/_utils.py +0 -0
- /krons/{specs → core/specs}/adapters/factory.py +0 -0
- /krons/{types → core/types}/__init__.py +0 -0
- /krons/{types → core/types}/_sentinel.py +0 -0
- /krons/{types → core/types}/identity.py +0 -0
- /krons/{services → resource}/utilities/__init__.py +0 -0
- /krons/{services → resource}/utilities/header_factory.py +0 -0
- /krons/{enforcement → work/rules}/common/__init__.py +0 -0
- /krons/{enforcement → work/rules}/common/mapping.py +0 -0
- /krons/{enforcement → work/rules}/common/model.py +0 -0
- /krons/{enforcement → work/rules}/registry.py +0 -0
- {krons-0.1.1.dist-info → krons-0.2.1.dist-info}/WHEEL +0 -0
- {krons-0.1.1.dist-info → krons-0.2.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -10,7 +10,7 @@ from pydantic import Field, PrivateAttr, field_validator
|
|
|
10
10
|
from typing_extensions import TypedDict
|
|
11
11
|
|
|
12
12
|
from krons.core import Broadcaster, Event, EventStatus
|
|
13
|
-
from krons.types import Enum, Undefined
|
|
13
|
+
from krons.core.types import Enum, Undefined
|
|
14
14
|
from krons.utils import concurrency
|
|
15
15
|
|
|
16
16
|
SC = TypeVar("SC")
|
|
@@ -84,7 +84,11 @@ class HookEvent(Event):
|
|
|
84
84
|
)
|
|
85
85
|
|
|
86
86
|
# Unpack the result - hook_phase returns tuple of (inner_tuple, meta)
|
|
87
|
-
if
|
|
87
|
+
if (
|
|
88
|
+
isinstance(result, tuple)
|
|
89
|
+
and len(result) == 2
|
|
90
|
+
and isinstance(result[1], dict)
|
|
91
|
+
):
|
|
88
92
|
inner_tuple, meta = result
|
|
89
93
|
res, se, _ = inner_tuple
|
|
90
94
|
else:
|
|
@@ -93,7 +97,9 @@ class HookEvent(Event):
|
|
|
93
97
|
meta = {}
|
|
94
98
|
|
|
95
99
|
# Build associated event info from meta dict
|
|
96
|
-
event_info: AssociatedEventInfo = {
|
|
100
|
+
event_info: AssociatedEventInfo = {
|
|
101
|
+
"kron_class": str(meta.get("kron_class", ""))
|
|
102
|
+
}
|
|
97
103
|
if "event_id" in meta:
|
|
98
104
|
event_info["event_id"] = str(meta["event_id"])
|
|
99
105
|
if "event_created_at" in meta:
|
|
@@ -180,7 +186,9 @@ def validate_stream_handlers(kw: dict[Any, Any]) -> None:
|
|
|
180
186
|
|
|
181
187
|
for k, v in kw.items():
|
|
182
188
|
if not isinstance(k, str | type):
|
|
183
|
-
raise ValueError(
|
|
189
|
+
raise ValueError(
|
|
190
|
+
f"Stream handler key must be a string or type, got {type(k)}"
|
|
191
|
+
)
|
|
184
192
|
|
|
185
193
|
if not callable(v):
|
|
186
194
|
raise ValueError(f"Stream handler for {k} must be callable, got {type(v)}")
|
|
@@ -402,7 +410,8 @@ class HookRegistry:
|
|
|
402
410
|
exit: bool = False,
|
|
403
411
|
**kw: Any,
|
|
404
412
|
) -> (
|
|
405
|
-
tuple[tuple[Any, bool, EventStatus], dict[str, Any]]
|
|
413
|
+
tuple[tuple[Any, bool, EventStatus], dict[str, Any]]
|
|
414
|
+
| tuple[Any, bool, EventStatus | None]
|
|
406
415
|
):
|
|
407
416
|
"""Call a hook or stream handler.
|
|
408
417
|
|
|
@@ -437,7 +446,9 @@ class HookRegistry:
|
|
|
437
446
|
await self.pre_invocation(event_like, exit=exit, **kw),
|
|
438
447
|
meta,
|
|
439
448
|
)
|
|
440
|
-
raise TypeError(
|
|
449
|
+
raise TypeError(
|
|
450
|
+
"PreInvocation requires an Event instance, not a type"
|
|
451
|
+
)
|
|
441
452
|
case HookPhase.PostInvocation | HookPhase.PostInvocation.value:
|
|
442
453
|
# For post_invocation, event_like should be an instance
|
|
443
454
|
if isinstance(event_like, Event):
|
|
@@ -447,7 +458,9 @@ class HookRegistry:
|
|
|
447
458
|
await self.post_invocation(event_like, exit=exit, **kw),
|
|
448
459
|
meta,
|
|
449
460
|
)
|
|
450
|
-
raise TypeError(
|
|
461
|
+
raise TypeError(
|
|
462
|
+
"PostInvocation requires an Event instance, not a type"
|
|
463
|
+
)
|
|
451
464
|
return await self.handle_streaming_chunk(chunk_type, chunk, exit=exit, **kw)
|
|
452
465
|
|
|
453
466
|
def _can_handle(
|
|
@@ -11,7 +11,7 @@ from krons.core import Element, Executor
|
|
|
11
11
|
from krons.protocols import Invocable, implements
|
|
12
12
|
from krons.utils.concurrency import sleep
|
|
13
13
|
|
|
14
|
-
from .backend import
|
|
14
|
+
from .backend import ResourceBackend
|
|
15
15
|
from .endpoint import Endpoint
|
|
16
16
|
from .hook import HookRegistry
|
|
17
17
|
from .utilities.rate_limited_executor import RateLimitedExecutor
|
|
@@ -26,14 +26,14 @@ __all__ = ("iModel",)
|
|
|
26
26
|
|
|
27
27
|
@implements(Invocable)
|
|
28
28
|
class iModel(Element): # noqa: N801
|
|
29
|
-
"""Unified
|
|
29
|
+
"""Unified resource interface wrapping ResourceBackend with rate limiting and hooks.
|
|
30
30
|
|
|
31
|
-
Combines
|
|
31
|
+
Combines ResourceBackend (API abstraction) with optional:
|
|
32
32
|
- Rate limiting: TokenBucket (simple) or Executor (event-driven)
|
|
33
33
|
- Hook registry: Lifecycle callbacks at PreEventCreate/PreInvocation/PostInvocation
|
|
34
34
|
|
|
35
35
|
Attributes:
|
|
36
|
-
backend:
|
|
36
|
+
backend: ResourceBackend instance (e.g., Endpoint for HTTP APIs).
|
|
37
37
|
rate_limiter: Optional TokenBucket for simple blocking rate limits.
|
|
38
38
|
executor: Optional Executor for event-driven processing with rate limiting.
|
|
39
39
|
hook_registry: Optional HookRegistry for invocation lifecycle callbacks.
|
|
@@ -43,9 +43,9 @@ class iModel(Element): # noqa: N801
|
|
|
43
43
|
_EXECUTOR_POLL_TIMEOUT_ITERATIONS = 100
|
|
44
44
|
_EXECUTOR_POLL_SLEEP_INTERVAL = 0.1
|
|
45
45
|
|
|
46
|
-
backend:
|
|
46
|
+
backend: ResourceBackend | None = Field(
|
|
47
47
|
None,
|
|
48
|
-
description="
|
|
48
|
+
description="ResourceBackend instance (e.g., Endpoint)",
|
|
49
49
|
)
|
|
50
50
|
|
|
51
51
|
rate_limiter: TokenBucket | None = Field(
|
|
@@ -70,7 +70,7 @@ class iModel(Element): # noqa: N801
|
|
|
70
70
|
|
|
71
71
|
def __init__(
|
|
72
72
|
self,
|
|
73
|
-
backend:
|
|
73
|
+
backend: ResourceBackend,
|
|
74
74
|
rate_limiter: TokenBucket | None = None,
|
|
75
75
|
executor: Executor | None = None,
|
|
76
76
|
hook_registry: HookRegistry | None = None,
|
|
@@ -78,10 +78,10 @@ class iModel(Element): # noqa: N801
|
|
|
78
78
|
capacity_refresh_time: float = 60,
|
|
79
79
|
limit_requests: int | None = None,
|
|
80
80
|
):
|
|
81
|
-
"""Initialize iModel with
|
|
81
|
+
"""Initialize iModel with ResourceBackend.
|
|
82
82
|
|
|
83
83
|
Args:
|
|
84
|
-
backend:
|
|
84
|
+
backend: ResourceBackend instance (required).
|
|
85
85
|
rate_limiter: TokenBucket for simple blocking rate limits.
|
|
86
86
|
executor: Executor for event-driven processing.
|
|
87
87
|
hook_registry: HookRegistry for lifecycle callbacks.
|
|
@@ -114,21 +114,21 @@ class iModel(Element): # noqa: N801
|
|
|
114
114
|
|
|
115
115
|
@property
|
|
116
116
|
def name(self) -> str:
|
|
117
|
-
"""
|
|
117
|
+
"""Resource name from backend."""
|
|
118
118
|
if self.backend is None:
|
|
119
119
|
raise RuntimeError("Backend not configured")
|
|
120
120
|
return self.backend.name
|
|
121
121
|
|
|
122
122
|
@property
|
|
123
123
|
def version(self) -> str:
|
|
124
|
-
"""
|
|
124
|
+
"""Resource version from backend."""
|
|
125
125
|
if self.backend is None:
|
|
126
126
|
raise RuntimeError("Backend not configured")
|
|
127
127
|
return self.backend.version or ""
|
|
128
128
|
|
|
129
129
|
@property
|
|
130
130
|
def tags(self) -> set[str]:
|
|
131
|
-
"""
|
|
131
|
+
"""Resource tags from backend."""
|
|
132
132
|
if self.backend is None:
|
|
133
133
|
raise RuntimeError("Backend not configured")
|
|
134
134
|
return self.backend.tags
|
|
@@ -181,7 +181,11 @@ class iModel(Element): # noqa: N801
|
|
|
181
181
|
hook_phase=HookPhase.PreEventCreate,
|
|
182
182
|
event_like=calling_type,
|
|
183
183
|
registry=self.hook_registry,
|
|
184
|
-
exit=(
|
|
184
|
+
exit=(
|
|
185
|
+
create_event_exit_hook
|
|
186
|
+
if create_event_exit_hook is not None
|
|
187
|
+
else False
|
|
188
|
+
),
|
|
185
189
|
timeout=create_event_hook_timeout,
|
|
186
190
|
streaming=False,
|
|
187
191
|
params=create_event_hook_params or {},
|
|
@@ -194,11 +198,12 @@ class iModel(Element): # noqa: N801
|
|
|
194
198
|
)
|
|
195
199
|
|
|
196
200
|
payload = self.backend.create_payload(request=arguments)
|
|
201
|
+
# Handle backends that return (payload, headers) tuple
|
|
202
|
+
if isinstance(payload, tuple):
|
|
203
|
+
payload, _ = payload
|
|
197
204
|
calling: Calling = calling_type(
|
|
198
205
|
backend=self.backend,
|
|
199
206
|
payload=payload,
|
|
200
|
-
timeout=timeout,
|
|
201
|
-
streaming=streaming,
|
|
202
207
|
)
|
|
203
208
|
|
|
204
209
|
if self.hook_registry is not None and self.hook_registry._can_handle(
|
|
@@ -206,7 +211,9 @@ class iModel(Element): # noqa: N801
|
|
|
206
211
|
):
|
|
207
212
|
calling.create_pre_invoke_hook(
|
|
208
213
|
hook_registry=self.hook_registry,
|
|
209
|
-
exit_hook=(
|
|
214
|
+
exit_hook=(
|
|
215
|
+
pre_invoke_exit_hook if pre_invoke_exit_hook is not None else False
|
|
216
|
+
),
|
|
210
217
|
hook_timeout=pre_invoke_hook_timeout,
|
|
211
218
|
hook_params=pre_invoke_hook_params or {},
|
|
212
219
|
)
|
|
@@ -216,7 +223,11 @@ class iModel(Element): # noqa: N801
|
|
|
216
223
|
):
|
|
217
224
|
calling.create_post_invoke_hook(
|
|
218
225
|
hook_registry=self.hook_registry,
|
|
219
|
-
exit_hook=(
|
|
226
|
+
exit_hook=(
|
|
227
|
+
post_invoke_exit_hook
|
|
228
|
+
if post_invoke_exit_hook is not None
|
|
229
|
+
else False
|
|
230
|
+
),
|
|
220
231
|
hook_timeout=post_invoke_hook_timeout,
|
|
221
232
|
hook_params=post_invoke_hook_params or {},
|
|
222
233
|
)
|
|
@@ -281,7 +292,8 @@ class iModel(Element): # noqa: N801
|
|
|
281
292
|
# Poll for completion (fast backends see ~100-200% overhead, slow backends <10%)
|
|
282
293
|
interval = poll_interval or self._EXECUTOR_POLL_SLEEP_INTERVAL
|
|
283
294
|
timeout_seconds = poll_timeout or (
|
|
284
|
-
self._EXECUTOR_POLL_TIMEOUT_ITERATIONS
|
|
295
|
+
self._EXECUTOR_POLL_TIMEOUT_ITERATIONS
|
|
296
|
+
* self._EXECUTOR_POLL_SLEEP_INTERVAL
|
|
285
297
|
)
|
|
286
298
|
max_iterations = int(timeout_seconds / interval)
|
|
287
299
|
ctr = 0
|
|
@@ -300,7 +312,9 @@ class iModel(Element): # noqa: N801
|
|
|
300
312
|
f"Event aborted after 3 permission denials (rate limited): {calling.id}"
|
|
301
313
|
)
|
|
302
314
|
elif calling.execution.status.value == "failed":
|
|
303
|
-
raise calling.execution.error or RuntimeError(
|
|
315
|
+
raise calling.execution.error or RuntimeError(
|
|
316
|
+
f"Event failed: {calling.id}"
|
|
317
|
+
)
|
|
304
318
|
|
|
305
319
|
self._store_claude_code_session_id(calling)
|
|
306
320
|
return calling
|
|
@@ -317,7 +331,7 @@ class iModel(Element): # noqa: N801
|
|
|
317
331
|
|
|
318
332
|
def _store_claude_code_session_id(self, calling: Calling) -> None:
|
|
319
333
|
"""Extract and store Claude Code session_id for context continuation."""
|
|
320
|
-
from krons.types import is_sentinel
|
|
334
|
+
from krons.core.types import is_sentinel
|
|
321
335
|
|
|
322
336
|
from .backend import NormalizedResponse
|
|
323
337
|
|
|
@@ -334,7 +348,7 @@ class iModel(Element): # noqa: N801
|
|
|
334
348
|
self.provider_metadata["session_id"] = session_id
|
|
335
349
|
|
|
336
350
|
@field_serializer("backend")
|
|
337
|
-
def _serialize_backend(self, backend:
|
|
351
|
+
def _serialize_backend(self, backend: ResourceBackend) -> dict[str, Any] | None:
|
|
338
352
|
"""Serialize backend to dict with kron_class for polymorphic restoration."""
|
|
339
353
|
if backend is None:
|
|
340
354
|
return None
|
|
@@ -384,24 +398,24 @@ class iModel(Element): # noqa: N801
|
|
|
384
398
|
|
|
385
399
|
@field_validator("backend", mode="before")
|
|
386
400
|
@classmethod
|
|
387
|
-
def _deserialize_backend(cls, v: Any) ->
|
|
401
|
+
def _deserialize_backend(cls, v: Any) -> ResourceBackend:
|
|
388
402
|
"""Reconstruct backend via Element polymorphic deserialization."""
|
|
389
403
|
if v is None:
|
|
390
404
|
raise ValueError("backend is required")
|
|
391
405
|
|
|
392
|
-
if isinstance(v,
|
|
406
|
+
if isinstance(v, ResourceBackend):
|
|
393
407
|
return v
|
|
394
408
|
|
|
395
409
|
if not isinstance(v, dict):
|
|
396
|
-
raise ValueError("backend must be a dict or
|
|
410
|
+
raise ValueError("backend must be a dict or ResourceBackend instance")
|
|
397
411
|
|
|
398
412
|
from krons.core import Element
|
|
399
413
|
|
|
400
414
|
backend = Element.from_dict(v)
|
|
401
415
|
|
|
402
|
-
if not isinstance(backend,
|
|
416
|
+
if not isinstance(backend, ResourceBackend):
|
|
403
417
|
raise ValueError(
|
|
404
|
-
f"Deserialized backend must be
|
|
418
|
+
f"Deserialized backend must be ResourceBackend subclass, got: {type(backend).__name__}"
|
|
405
419
|
)
|
|
406
420
|
return backend
|
|
407
421
|
|
|
@@ -420,9 +434,13 @@ class iModel(Element): # noqa: N801
|
|
|
420
434
|
|
|
421
435
|
config = {**v}
|
|
422
436
|
if "request_bucket" in config and isinstance(config["request_bucket"], dict):
|
|
423
|
-
config["request_bucket"] = TokenBucket(
|
|
437
|
+
config["request_bucket"] = TokenBucket(
|
|
438
|
+
RateLimitConfig(**config["request_bucket"])
|
|
439
|
+
)
|
|
424
440
|
if "token_bucket" in config and isinstance(config["token_bucket"], dict):
|
|
425
|
-
config["token_bucket"] = TokenBucket(
|
|
441
|
+
config["token_bucket"] = TokenBucket(
|
|
442
|
+
RateLimitConfig(**config["token_bucket"])
|
|
443
|
+
)
|
|
426
444
|
|
|
427
445
|
return RateLimitedExecutor(processor_config=config)
|
|
428
446
|
|
|
@@ -7,23 +7,23 @@ from typing import Any
|
|
|
7
7
|
from uuid import UUID
|
|
8
8
|
|
|
9
9
|
from krons.core import Pile
|
|
10
|
-
from krons.types import Undefined, UndefinedType, is_sentinel
|
|
10
|
+
from krons.core.types import Undefined, UndefinedType, is_sentinel
|
|
11
11
|
|
|
12
12
|
from .imodel import iModel
|
|
13
13
|
|
|
14
|
-
__all__ = ("
|
|
14
|
+
__all__ = ("ResourceRegistry",)
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
class
|
|
18
|
-
"""
|
|
17
|
+
class ResourceRegistry:
|
|
18
|
+
"""Resource registry managing iModel instances with O(1) name-based lookup.
|
|
19
19
|
|
|
20
20
|
Provides type-safe storage via Pile[iModel] with name-based indexing.
|
|
21
|
-
|
|
21
|
+
Resources must have unique names; duplicates raise ValueError unless update=True.
|
|
22
22
|
|
|
23
23
|
Example:
|
|
24
|
-
>>> registry =
|
|
24
|
+
>>> registry = ResourceRegistry()
|
|
25
25
|
>>> registry.register(iModel(backend=my_endpoint))
|
|
26
|
-
>>> model = registry.get("
|
|
26
|
+
>>> model = registry.get("my_resource")
|
|
27
27
|
>>> tagged = registry.list_by_tag("api")
|
|
28
28
|
"""
|
|
29
29
|
|
|
@@ -39,17 +39,17 @@ class ServiceRegistry:
|
|
|
39
39
|
|
|
40
40
|
Args:
|
|
41
41
|
model: iModel instance to register.
|
|
42
|
-
update: If True, replaces existing
|
|
42
|
+
update: If True, replaces existing resource with same name.
|
|
43
43
|
|
|
44
44
|
Returns:
|
|
45
|
-
UUID of registered
|
|
45
|
+
UUID of registered resource.
|
|
46
46
|
|
|
47
47
|
Raises:
|
|
48
|
-
ValueError: If
|
|
48
|
+
ValueError: If resource name exists and update=False.
|
|
49
49
|
"""
|
|
50
50
|
if model.name in self._name_index:
|
|
51
51
|
if not update:
|
|
52
|
-
raise ValueError(f"
|
|
52
|
+
raise ValueError(f"Resource '{model.name}' already registered")
|
|
53
53
|
# Update: remove old, add new
|
|
54
54
|
old_uid = self._name_index[model.name]
|
|
55
55
|
self._pile.remove(old_uid)
|
|
@@ -60,15 +60,17 @@ class ServiceRegistry:
|
|
|
60
60
|
return model.id
|
|
61
61
|
|
|
62
62
|
def unregister(self, name: str) -> iModel:
|
|
63
|
-
"""Remove and return
|
|
63
|
+
"""Remove and return resource by name. Raises KeyError if not found."""
|
|
64
64
|
if name not in self._name_index:
|
|
65
|
-
raise KeyError(f"
|
|
65
|
+
raise KeyError(f"Resource '{name}' not found")
|
|
66
66
|
|
|
67
67
|
uid = self._name_index.pop(name)
|
|
68
68
|
return self._pile.remove(uid)
|
|
69
69
|
|
|
70
|
-
def get(
|
|
71
|
-
|
|
70
|
+
def get(
|
|
71
|
+
self, name: str | UUID | iModel, default: Any | UndefinedType = Undefined
|
|
72
|
+
) -> iModel:
|
|
73
|
+
"""Get resource by name, UUID, or return iModel passthrough. Raises KeyError if not found."""
|
|
72
74
|
if isinstance(name, UUID):
|
|
73
75
|
return self._pile[name]
|
|
74
76
|
if isinstance(name, iModel):
|
|
@@ -76,40 +78,40 @@ class ServiceRegistry:
|
|
|
76
78
|
if name not in self._name_index:
|
|
77
79
|
if not is_sentinel(default):
|
|
78
80
|
return default
|
|
79
|
-
raise KeyError(f"
|
|
81
|
+
raise KeyError(f"Resource '{name}' not found")
|
|
80
82
|
|
|
81
83
|
uid = self._name_index[name]
|
|
82
84
|
return self._pile[uid]
|
|
83
85
|
|
|
84
86
|
def has(self, name: str) -> bool:
|
|
85
|
-
"""Check if
|
|
87
|
+
"""Check if resource exists."""
|
|
86
88
|
return name in self._name_index
|
|
87
89
|
|
|
88
90
|
def list_names(self) -> list[str]:
|
|
89
|
-
"""List all registered
|
|
91
|
+
"""List all registered resource names."""
|
|
90
92
|
return list(self._name_index.keys())
|
|
91
93
|
|
|
92
94
|
def list_by_tag(self, tag: str) -> Pile[iModel]:
|
|
93
|
-
"""Filter
|
|
95
|
+
"""Filter resources by tag, returns Pile of matching iModels."""
|
|
94
96
|
return self._pile[lambda m: tag in m.tags]
|
|
95
97
|
|
|
96
98
|
def count(self) -> int:
|
|
97
|
-
"""Count registered
|
|
99
|
+
"""Count registered resources."""
|
|
98
100
|
return len(self._pile)
|
|
99
101
|
|
|
100
102
|
def clear(self) -> None:
|
|
101
|
-
"""Remove all registered
|
|
103
|
+
"""Remove all registered resources."""
|
|
102
104
|
self._pile.clear()
|
|
103
105
|
self._name_index.clear()
|
|
104
106
|
|
|
105
107
|
def __len__(self) -> int:
|
|
106
|
-
"""Return number of registered
|
|
108
|
+
"""Return number of registered resources."""
|
|
107
109
|
return len(self._pile)
|
|
108
110
|
|
|
109
111
|
def __contains__(self, name: str) -> bool:
|
|
110
|
-
"""Check if
|
|
112
|
+
"""Check if resource exists (supports `name in registry`)."""
|
|
111
113
|
return name in self._name_index
|
|
112
114
|
|
|
113
115
|
def __repr__(self) -> str:
|
|
114
116
|
"""String representation."""
|
|
115
|
-
return f"
|
|
117
|
+
return f"ResourceRegistry(count={len(self)})"
|
|
@@ -15,7 +15,7 @@ from typing import TYPE_CHECKING, Any, Self
|
|
|
15
15
|
from typing_extensions import override
|
|
16
16
|
|
|
17
17
|
from krons.core import Event, Executor, Processor
|
|
18
|
-
from krons.
|
|
18
|
+
from krons.resource.endpoint import APICalling
|
|
19
19
|
from krons.utils.concurrency import get_cancelled_exc_class, sleep
|
|
20
20
|
|
|
21
21
|
from .rate_limiter import TokenBucket
|
|
@@ -213,8 +213,12 @@ class RateLimitedProcessor(Processor):
|
|
|
213
213
|
"concurrency_limit": self.concurrency_limit,
|
|
214
214
|
"max_queue_size": self.max_queue_size,
|
|
215
215
|
"max_denial_tracking": self.max_denial_tracking,
|
|
216
|
-
"request_bucket": (
|
|
217
|
-
|
|
216
|
+
"request_bucket": (
|
|
217
|
+
self.request_bucket.to_dict() if self.request_bucket else None
|
|
218
|
+
),
|
|
219
|
+
"token_bucket": (
|
|
220
|
+
self.token_bucket.to_dict() if self.token_bucket else None
|
|
221
|
+
),
|
|
218
222
|
}
|
|
219
223
|
|
|
220
224
|
|
|
@@ -105,7 +105,9 @@ class TokenBucket:
|
|
|
105
105
|
|
|
106
106
|
if self.tokens >= tokens:
|
|
107
107
|
self.tokens -= tokens
|
|
108
|
-
logger.debug(
|
|
108
|
+
logger.debug(
|
|
109
|
+
f"Acquired {tokens} tokens, {self.tokens:.2f} remaining"
|
|
110
|
+
)
|
|
109
111
|
return True
|
|
110
112
|
|
|
111
113
|
deficit = tokens - self.tokens
|
|
@@ -199,7 +199,9 @@ class CircuitBreaker:
|
|
|
199
199
|
if now - self.last_failure_time >= self.recovery_time:
|
|
200
200
|
await self._change_state(CircuitState.HALF_OPEN)
|
|
201
201
|
else:
|
|
202
|
-
recovery_remaining = self.recovery_time - (
|
|
202
|
+
recovery_remaining = self.recovery_time - (
|
|
203
|
+
now - self.last_failure_time
|
|
204
|
+
)
|
|
203
205
|
self._metrics["rejected_count"] += 1
|
|
204
206
|
|
|
205
207
|
logger.warning(
|
|
@@ -223,7 +225,9 @@ class CircuitBreaker:
|
|
|
223
225
|
|
|
224
226
|
return True, 0.0
|
|
225
227
|
|
|
226
|
-
async def execute(
|
|
228
|
+
async def execute(
|
|
229
|
+
self, func: Callable[..., Awaitable[T]], *args: Any, **kwargs: Any
|
|
230
|
+
) -> T:
|
|
227
231
|
"""Execute async function with circuit breaker protection.
|
|
228
232
|
|
|
229
233
|
Args:
|
|
@@ -259,7 +263,9 @@ class CircuitBreaker:
|
|
|
259
263
|
return result
|
|
260
264
|
|
|
261
265
|
except Exception as e:
|
|
262
|
-
is_excluded = any(
|
|
266
|
+
is_excluded = any(
|
|
267
|
+
isinstance(e, exc_type) for exc_type in self.excluded_exceptions
|
|
268
|
+
)
|
|
263
269
|
|
|
264
270
|
if not is_excluded:
|
|
265
271
|
async with self._lock:
|
|
@@ -322,7 +328,9 @@ class RetryConfig:
|
|
|
322
328
|
Returns:
|
|
323
329
|
Delay in seconds before next retry
|
|
324
330
|
"""
|
|
325
|
-
delay = min(
|
|
331
|
+
delay = min(
|
|
332
|
+
self.initial_delay * (self.exponential_base**attempt), self.max_delay
|
|
333
|
+
)
|
|
326
334
|
if self.jitter:
|
|
327
335
|
delay = delay * (0.5 + random.random() * 0.5)
|
|
328
336
|
return delay
|
|
@@ -394,7 +402,9 @@ async def retry_with_backoff(
|
|
|
394
402
|
last_exception = e
|
|
395
403
|
|
|
396
404
|
if attempt >= max_retries:
|
|
397
|
-
logger.error(
|
|
405
|
+
logger.error(
|
|
406
|
+
f"All {max_retries} retry attempts exhausted for {func.__name__}: {e}"
|
|
407
|
+
)
|
|
398
408
|
raise
|
|
399
409
|
|
|
400
410
|
delay = min(initial_delay * (exponential_base**attempt), max_delay)
|