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
|
@@ -11,15 +11,15 @@ from collections.abc import Callable
|
|
|
11
11
|
from dataclasses import dataclass
|
|
12
12
|
from typing import Annotated, Any, Self
|
|
13
13
|
|
|
14
|
-
from krons.
|
|
15
|
-
from krons.types._sentinel import (
|
|
14
|
+
from krons.core.types._sentinel import (
|
|
16
15
|
MaybeUndefined,
|
|
17
16
|
Undefined,
|
|
18
17
|
is_sentinel,
|
|
19
18
|
is_undefined,
|
|
20
19
|
not_sentinel,
|
|
21
20
|
)
|
|
22
|
-
from krons.types.base import Enum, Meta
|
|
21
|
+
from krons.core.types.base import Enum, Meta
|
|
22
|
+
from krons.protocols import Hashable, implements
|
|
23
23
|
from krons.utils.concurrency import is_coro_func
|
|
24
24
|
|
|
25
25
|
# Global cache for annotated types with bounded size
|
|
@@ -64,13 +64,17 @@ class CommonMeta(Enum):
|
|
|
64
64
|
errors: list[Exception] = []
|
|
65
65
|
|
|
66
66
|
if kw.get("default") and kw.get("default_factory"):
|
|
67
|
-
errors.append(
|
|
67
|
+
errors.append(
|
|
68
|
+
ValueError("Cannot provide both 'default' and 'default_factory'")
|
|
69
|
+
)
|
|
68
70
|
if (_df := kw.get("default_factory")) and not callable(_df):
|
|
69
71
|
errors.append(ValueError("'default_factory' must be callable"))
|
|
70
72
|
if _val := kw.get("validator"):
|
|
71
73
|
_val = [_val] if not isinstance(_val, list) else _val
|
|
72
74
|
if not all(callable(v) for v in _val):
|
|
73
|
-
errors.append(
|
|
75
|
+
errors.append(
|
|
76
|
+
ValueError("Validators must be a list of functions or a function")
|
|
77
|
+
)
|
|
74
78
|
|
|
75
79
|
if errors:
|
|
76
80
|
raise ExceptionGroup("Metadata validation failed", errors)
|
|
@@ -192,7 +196,9 @@ class Spec:
|
|
|
192
196
|
or isinstance(base_type, types.UnionType)
|
|
193
197
|
)
|
|
194
198
|
if not is_valid_type:
|
|
195
|
-
raise ValueError(
|
|
199
|
+
raise ValueError(
|
|
200
|
+
f"base_type must be a type or type annotation, got {base_type}"
|
|
201
|
+
)
|
|
196
202
|
|
|
197
203
|
if kw.get("default_factory") and is_coro_func(kw["default_factory"]):
|
|
198
204
|
import warnings
|
|
@@ -389,7 +395,9 @@ class Spec:
|
|
|
389
395
|
|
|
390
396
|
return spec
|
|
391
397
|
|
|
392
|
-
def with_validator(
|
|
398
|
+
def with_validator(
|
|
399
|
+
self, validator: Callable[..., Any] | list[Callable[..., Any]]
|
|
400
|
+
) -> Self:
|
|
393
401
|
"""Return new Spec with validator function(s) attached."""
|
|
394
402
|
return self.with_updates(validator=validator)
|
|
395
403
|
|
|
@@ -407,7 +415,7 @@ class Spec:
|
|
|
407
415
|
if not is_undefined(fk):
|
|
408
416
|
from uuid import UUID
|
|
409
417
|
|
|
410
|
-
from krons.types.db_types import FKMeta
|
|
418
|
+
from krons.core.types.db_types import FKMeta
|
|
411
419
|
|
|
412
420
|
t_ = Annotated[UUID, FKMeta(fk)] # type: ignore[valid-type]
|
|
413
421
|
if self.is_listable:
|
|
@@ -431,7 +439,9 @@ class Spec:
|
|
|
431
439
|
_annotated_cache.move_to_end(cache_key)
|
|
432
440
|
return _annotated_cache[cache_key]
|
|
433
441
|
|
|
434
|
-
actual_type =
|
|
442
|
+
actual_type = (
|
|
443
|
+
Any if is_sentinel(self.base_type, {"none"}) else self.base_type
|
|
444
|
+
)
|
|
435
445
|
current_metadata = self.metadata
|
|
436
446
|
|
|
437
447
|
# Resolve FK target (explicit or Observable base_type)
|
|
@@ -440,7 +450,7 @@ class Spec:
|
|
|
440
450
|
if not is_undefined(resolved_fk):
|
|
441
451
|
from uuid import UUID
|
|
442
452
|
|
|
443
|
-
from krons.types.db_types import FKMeta
|
|
453
|
+
from krons.core.types.db_types import FKMeta
|
|
444
454
|
|
|
445
455
|
actual_type = UUID # FK fields are UUID references
|
|
446
456
|
extra_annotations.append(FKMeta(resolved_fk))
|
|
@@ -475,7 +485,9 @@ class Spec:
|
|
|
475
485
|
exclude = set()
|
|
476
486
|
if exclude_common:
|
|
477
487
|
exclude = exclude | set(CommonMeta.allowed())
|
|
478
|
-
return {
|
|
488
|
+
return {
|
|
489
|
+
meta.key: meta.value for meta in self.metadata if meta.key not in exclude
|
|
490
|
+
}
|
|
479
491
|
|
|
480
492
|
|
|
481
493
|
def _is_observable(cls: type) -> bool:
|
|
@@ -111,7 +111,9 @@ class _SentinelMixin:
|
|
|
111
111
|
"""Return set of valid field names (excludes private/ClassVar)."""
|
|
112
112
|
if cls._allowed_keys:
|
|
113
113
|
return cls._allowed_keys
|
|
114
|
-
cls._allowed_keys = set(
|
|
114
|
+
cls._allowed_keys = set(
|
|
115
|
+
f.name for f in fields(cls) if not f.name.startswith("_")
|
|
116
|
+
)
|
|
115
117
|
return cls._allowed_keys
|
|
116
118
|
|
|
117
119
|
@classmethod
|
|
@@ -353,7 +355,7 @@ class HashableModel(_PydanticBaseModel):
|
|
|
353
355
|
as DataClass but for Pydantic models.
|
|
354
356
|
|
|
355
357
|
Usage:
|
|
356
|
-
class
|
|
358
|
+
class ResourceConfig(HashableModel):
|
|
357
359
|
provider: str
|
|
358
360
|
name: str
|
|
359
361
|
"""
|
|
@@ -18,7 +18,7 @@ import types
|
|
|
18
18
|
from typing import Annotated, Any, Literal, Union, get_args, get_origin
|
|
19
19
|
from uuid import UUID
|
|
20
20
|
|
|
21
|
-
from krons.types._sentinel import Unset, UnsetType, not_sentinel
|
|
21
|
+
from krons.core.types._sentinel import Unset, UnsetType, not_sentinel
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
def _is_field_info(obj: Any) -> bool:
|
|
@@ -237,7 +237,7 @@ def extract_kron_db_meta(
|
|
|
237
237
|
|
|
238
238
|
else:
|
|
239
239
|
# Try Spec (lazy import to avoid circular)
|
|
240
|
-
from krons.specs.spec import Spec
|
|
240
|
+
from krons.core.specs.spec import Spec
|
|
241
241
|
|
|
242
242
|
if isinstance(from_, Spec):
|
|
243
243
|
if metas in ("FK", "BOTH"):
|
krons/errors.py
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
"""Kron exception hierarchy with structured details and retryability.
|
|
5
5
|
|
|
6
|
-
All exceptions inherit from
|
|
6
|
+
All exceptions inherit from KronsError and include:
|
|
7
7
|
- message: Human-readable description
|
|
8
8
|
- details: Structured context dict
|
|
9
9
|
- retryable: Whether retry might succeed
|
|
@@ -23,7 +23,7 @@ __all__ = (
|
|
|
23
23
|
"ExecutionError",
|
|
24
24
|
"ExistsError",
|
|
25
25
|
"KronConnectionError",
|
|
26
|
-
"
|
|
26
|
+
"KronsError",
|
|
27
27
|
"KronTimeoutError",
|
|
28
28
|
"NotFoundError",
|
|
29
29
|
"OperationError",
|
|
@@ -33,7 +33,7 @@ __all__ = (
|
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
@implements(Serializable)
|
|
36
|
-
class
|
|
36
|
+
class KronsError(Exception):
|
|
37
37
|
"""Base exception for kron. Serializable with structured details.
|
|
38
38
|
|
|
39
39
|
Subclasses set default_message and default_retryable.
|
|
@@ -76,70 +76,70 @@ class KronError(Exception):
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
|
|
79
|
-
class ValidationError(
|
|
79
|
+
class ValidationError(KronsError):
|
|
80
80
|
"""Data validation failure. Raise when input fails schema/constraint checks."""
|
|
81
81
|
|
|
82
82
|
default_message = "Validation failed"
|
|
83
83
|
default_retryable = False
|
|
84
84
|
|
|
85
85
|
|
|
86
|
-
class AccessError(
|
|
86
|
+
class AccessError(KronsError):
|
|
87
87
|
"""Permission denied. Raise when capability/resource access is blocked."""
|
|
88
88
|
|
|
89
89
|
default_message = "Access denied"
|
|
90
90
|
default_retryable = False
|
|
91
91
|
|
|
92
92
|
|
|
93
|
-
class ConfigurationError(
|
|
93
|
+
class ConfigurationError(KronsError):
|
|
94
94
|
"""Invalid configuration. Raise when setup/config is incorrect."""
|
|
95
95
|
|
|
96
96
|
default_message = "Configuration error"
|
|
97
97
|
default_retryable = False
|
|
98
98
|
|
|
99
99
|
|
|
100
|
-
class ExecutionError(
|
|
100
|
+
class ExecutionError(KronsError):
|
|
101
101
|
"""Execution failure. Raise when Event/Calling invoke fails (often transient)."""
|
|
102
102
|
|
|
103
103
|
default_message = "Execution failed"
|
|
104
104
|
default_retryable = True
|
|
105
105
|
|
|
106
106
|
|
|
107
|
-
class KronConnectionError(
|
|
107
|
+
class KronConnectionError(KronsError):
|
|
108
108
|
"""Network/connection failure. Named to avoid shadowing builtins."""
|
|
109
109
|
|
|
110
110
|
default_message = "Connection error"
|
|
111
111
|
default_retryable = True
|
|
112
112
|
|
|
113
113
|
|
|
114
|
-
class KronTimeoutError(
|
|
114
|
+
class KronTimeoutError(KronsError):
|
|
115
115
|
"""Operation timeout. Named to avoid shadowing builtins."""
|
|
116
116
|
|
|
117
117
|
default_message = "Operation timed out"
|
|
118
118
|
default_retryable = True
|
|
119
119
|
|
|
120
120
|
|
|
121
|
-
class NotFoundError(
|
|
121
|
+
class NotFoundError(KronsError):
|
|
122
122
|
"""Resource/item not found. Raise when lookup fails."""
|
|
123
123
|
|
|
124
124
|
default_message = "Item not found"
|
|
125
125
|
default_retryable = False
|
|
126
126
|
|
|
127
127
|
|
|
128
|
-
class ExistsError(
|
|
128
|
+
class ExistsError(KronsError):
|
|
129
129
|
"""Duplicate item. Raise when creating item that already exists."""
|
|
130
130
|
|
|
131
131
|
default_message = "Item already exists"
|
|
132
132
|
default_retryable = False
|
|
133
133
|
|
|
134
134
|
|
|
135
|
-
class QueueFullError(
|
|
135
|
+
class QueueFullError(KronsError):
|
|
136
136
|
"""Capacity exceeded. Raise when queue/buffer is full."""
|
|
137
137
|
|
|
138
138
|
default_message = "Queue is full"
|
|
139
139
|
default_retryable = True
|
|
140
140
|
|
|
141
141
|
|
|
142
|
-
class OperationError(
|
|
142
|
+
class OperationError(KronsError):
|
|
143
143
|
"""Generic operation failure. Use for unclassified operation errors."""
|
|
144
144
|
|
|
145
145
|
default_message = "Operation failed"
|
krons/protocols.py
CHANGED
|
@@ -179,7 +179,9 @@ def _check_signature_compatibility(
|
|
|
179
179
|
# If protocol accepts **kwargs, implementation must also accept them
|
|
180
180
|
# Otherwise callers passing kwargs (allowed by protocol) will fail
|
|
181
181
|
if proto_has_var_keyword and not impl_has_var_keyword:
|
|
182
|
-
errors.append(
|
|
182
|
+
errors.append(
|
|
183
|
+
" - 'kwargs': protocol accepts **kwargs but implementation doesn't"
|
|
184
|
+
)
|
|
183
185
|
|
|
184
186
|
# If protocol accepts *args, implementation must also accept them
|
|
185
187
|
if proto_has_var_positional and not impl_has_var_positional:
|
|
@@ -277,10 +279,12 @@ def _check_signature_compatibility(
|
|
|
277
279
|
if param_name not in protocol_params:
|
|
278
280
|
# Check if protocol has *args or **kwargs that could provide it
|
|
279
281
|
proto_has_var_positional = any(
|
|
280
|
-
p.kind == inspect.Parameter.VAR_POSITIONAL
|
|
282
|
+
p.kind == inspect.Parameter.VAR_POSITIONAL
|
|
283
|
+
for p in protocol_params.values()
|
|
281
284
|
)
|
|
282
285
|
proto_has_var_keyword = any(
|
|
283
|
-
p.kind == inspect.Parameter.VAR_KEYWORD
|
|
286
|
+
p.kind == inspect.Parameter.VAR_KEYWORD
|
|
287
|
+
for p in protocol_params.values()
|
|
284
288
|
)
|
|
285
289
|
|
|
286
290
|
can_satisfy = False
|
|
@@ -396,7 +400,8 @@ def implements(
|
|
|
396
400
|
if errors:
|
|
397
401
|
error_msg = (
|
|
398
402
|
f"{cls.__name__}.{member_name} signature incompatible "
|
|
399
|
-
f"with {protocol.__name__}.{member_name}:\n"
|
|
403
|
+
f"with {protocol.__name__}.{member_name}:\n"
|
|
404
|
+
+ "\n".join(errors)
|
|
400
405
|
)
|
|
401
406
|
all_signature_errors.append(error_msg)
|
|
402
407
|
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Copyright (c) 2025 - 2026, HaiyangLi <quantocean.li at gmail dot com>
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""Resources module: iModel, ResourceBackend, hooks, and registry.
|
|
5
|
+
|
|
6
|
+
Core exports:
|
|
7
|
+
- iModel: Unified resource interface with rate limiting and hooks
|
|
8
|
+
- ResourceBackend/Endpoint: Backend abstractions for API calls
|
|
9
|
+
- HookRegistry/HookEvent/HookPhase: Lifecycle hook system
|
|
10
|
+
- ResourceRegistry: O(1) name-based resource 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": ("krons.resource.backend", "Calling"),
|
|
22
|
+
"NormalizedResponse": ("krons.resource.backend", "NormalizedResponse"),
|
|
23
|
+
"NormalizedResponseModel": ("krons.resource.backend", "NormalizedResponseModel"),
|
|
24
|
+
"ResourceBackend": ("krons.resource.backend", "ResourceBackend"),
|
|
25
|
+
"ResourceConfig": ("krons.resource.backend", "ResourceConfig"),
|
|
26
|
+
"ResourceRegistry": ("krons.resource.registry", "ResourceRegistry"),
|
|
27
|
+
"iModel": ("krons.resource.imodel", "iModel"),
|
|
28
|
+
"Endpoint": ("krons.resource.endpoint", "Endpoint"),
|
|
29
|
+
"EndpointConfig": ("krons.resource.endpoint", "EndpointConfig"),
|
|
30
|
+
"APICalling": ("krons.resource.endpoint", "APICalling"),
|
|
31
|
+
"HookRegistry": ("krons.resource.hook", "HookRegistry"),
|
|
32
|
+
"HookEvent": ("krons.resource.hook", "HookEvent"),
|
|
33
|
+
"HookPhase": ("krons.resource.hook", "HookPhase"),
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
_LOADED: dict[str, object] = {}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def __getattr__(name: str) -> object:
|
|
40
|
+
"""Lazy import attributes on first access."""
|
|
41
|
+
if name in _LOADED:
|
|
42
|
+
return _LOADED[name]
|
|
43
|
+
|
|
44
|
+
if name in _LAZY_IMPORTS:
|
|
45
|
+
from importlib import import_module
|
|
46
|
+
|
|
47
|
+
module_name, attr_name = _LAZY_IMPORTS[name]
|
|
48
|
+
module = import_module(module_name)
|
|
49
|
+
value = getattr(module, attr_name)
|
|
50
|
+
_LOADED[name] = value
|
|
51
|
+
return value
|
|
52
|
+
|
|
53
|
+
raise AttributeError(f"module 'krons.resource' has no attribute {name!r}")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def __dir__() -> list[str]:
|
|
57
|
+
"""Return all available attributes for autocomplete."""
|
|
58
|
+
return list(__all__)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# TYPE_CHECKING block for static analysis
|
|
62
|
+
if TYPE_CHECKING:
|
|
63
|
+
from .backend import (
|
|
64
|
+
Calling,
|
|
65
|
+
NormalizedResponse,
|
|
66
|
+
NormalizedResponseModel,
|
|
67
|
+
ResourceBackend,
|
|
68
|
+
ResourceConfig,
|
|
69
|
+
)
|
|
70
|
+
from .endpoint import APICalling, Endpoint, EndpointConfig
|
|
71
|
+
from .hook import HookEvent, HookPhase, HookRegistry
|
|
72
|
+
from .imodel import iModel
|
|
73
|
+
from .registry import ResourceRegistry
|
|
74
|
+
|
|
75
|
+
__all__ = (
|
|
76
|
+
"APICalling",
|
|
77
|
+
"Calling",
|
|
78
|
+
"Endpoint",
|
|
79
|
+
"EndpointConfig",
|
|
80
|
+
"HookEvent",
|
|
81
|
+
"HookPhase",
|
|
82
|
+
"HookRegistry",
|
|
83
|
+
"NormalizedResponse",
|
|
84
|
+
"NormalizedResponseModel",
|
|
85
|
+
"ResourceBackend",
|
|
86
|
+
"ResourceConfig",
|
|
87
|
+
"ResourceRegistry",
|
|
88
|
+
"iModel",
|
|
89
|
+
)
|
|
@@ -5,12 +5,13 @@ from __future__ import annotations
|
|
|
5
5
|
|
|
6
6
|
import logging
|
|
7
7
|
from abc import abstractmethod
|
|
8
|
-
from typing import Any
|
|
8
|
+
from typing import Any, Protocol, runtime_checkable
|
|
9
9
|
|
|
10
10
|
from pydantic import BaseModel, Field, PrivateAttr, field_validator, model_validator
|
|
11
11
|
|
|
12
12
|
from krons.core import Element, Event, EventStatus
|
|
13
|
-
from krons.types import HashableModel, Unset, UnsetType, is_sentinel
|
|
13
|
+
from krons.core.types import HashableModel, Unset, UnsetType, is_sentinel, is_unset
|
|
14
|
+
from krons.errors import ValidationError
|
|
14
15
|
|
|
15
16
|
from .hook import HookBroadcaster, HookEvent, HookPhase, HookRegistry
|
|
16
17
|
|
|
@@ -32,7 +33,7 @@ def _get_schema_field_keys(cls: type[BaseModel]) -> set[str]:
|
|
|
32
33
|
return _SCHEMA_FIELD_KEYS_CACHE[cls]
|
|
33
34
|
|
|
34
35
|
|
|
35
|
-
class
|
|
36
|
+
class ResourceConfig(HashableModel):
|
|
36
37
|
provider: str = Field(..., min_length=4, max_length=50)
|
|
37
38
|
name: str = Field(..., min_length=4, max_length=100)
|
|
38
39
|
request_options: type[BaseModel] | None = Field(default=None, exclude=True)
|
|
@@ -73,18 +74,33 @@ class ServiceConfig(HashableModel):
|
|
|
73
74
|
raise ValueError("Invalid payload") from e
|
|
74
75
|
|
|
75
76
|
|
|
76
|
-
|
|
77
|
-
|
|
77
|
+
@runtime_checkable
|
|
78
|
+
class NormalizedResponse(Protocol):
|
|
79
|
+
status: str
|
|
80
|
+
data: Any
|
|
81
|
+
error: str | None
|
|
82
|
+
raw_response: dict[str, Any]
|
|
83
|
+
metadata: dict[str, Any] | None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class NormalizedResponseModel(HashableModel):
|
|
87
|
+
"""Generic normalized response for all resource backends.
|
|
78
88
|
|
|
79
89
|
Works for any backend type: HTTP endpoints, tools, LLM APIs, etc.
|
|
80
|
-
Provides consistent interface regardless of underlying
|
|
90
|
+
Provides consistent interface regardless of underlying resource.
|
|
81
91
|
"""
|
|
82
92
|
|
|
83
93
|
status: str = Field(..., description="Response status: 'success' or 'error'")
|
|
84
94
|
data: Any = None
|
|
85
|
-
error: str | None = Field(
|
|
86
|
-
|
|
87
|
-
|
|
95
|
+
error: str | None = Field(
|
|
96
|
+
default=None, description="Error message if status='error'"
|
|
97
|
+
)
|
|
98
|
+
raw_response: dict[str, Any] = Field(
|
|
99
|
+
..., description="Original unmodified response"
|
|
100
|
+
)
|
|
101
|
+
metadata: dict[str, Any] | None = Field(
|
|
102
|
+
default=None, description="Provider-specific metadata"
|
|
103
|
+
)
|
|
88
104
|
|
|
89
105
|
def _to_dict(self, **kwargs: Any) -> dict[str, Any]:
|
|
90
106
|
"""Convert to dict, excluding None values."""
|
|
@@ -95,14 +111,16 @@ class Calling(Event):
|
|
|
95
111
|
"""Base calling event with hook support.
|
|
96
112
|
|
|
97
113
|
Extends kron.Event with pre/post invocation hooks.
|
|
98
|
-
Always delegates to backend.call() for actual
|
|
114
|
+
Always delegates to backend.call() for actual resource invocation.
|
|
99
115
|
|
|
100
116
|
Attributes:
|
|
101
|
-
backend:
|
|
117
|
+
backend: ResourceBackend instance (Tool, Endpoint, etc.)
|
|
102
118
|
payload: Request payload/arguments for backend call
|
|
103
119
|
"""
|
|
104
120
|
|
|
105
|
-
backend:
|
|
121
|
+
backend: ResourceBackend = Field(
|
|
122
|
+
..., exclude=True, description="Resource backend instance"
|
|
123
|
+
)
|
|
106
124
|
payload: dict[str, Any] = Field(..., description="Request payload/arguments")
|
|
107
125
|
_pre_invoke_hook_event: HookEvent | None = PrivateAttr(None)
|
|
108
126
|
_post_invoke_hook_event: HookEvent | None = PrivateAttr(None)
|
|
@@ -158,7 +176,7 @@ class Calling(Event):
|
|
|
158
176
|
)
|
|
159
177
|
await HookBroadcaster.broadcast(h_ev)
|
|
160
178
|
|
|
161
|
-
# Actual
|
|
179
|
+
# Actual resource call via backend (post-hook runs in finally)
|
|
162
180
|
try:
|
|
163
181
|
response = await self.backend.call(**self.call_args)
|
|
164
182
|
return response
|
|
@@ -217,15 +235,23 @@ class Calling(Event):
|
|
|
217
235
|
)
|
|
218
236
|
self._post_invoke_hook_event = h_ev
|
|
219
237
|
|
|
238
|
+
def assert_is_normalized(self) -> None:
|
|
239
|
+
"""Assert that response is normalized."""
|
|
240
|
+
self.assert_completed()
|
|
241
|
+
if is_unset(self.execution.response):
|
|
242
|
+
raise ValidationError("Calling response is not set")
|
|
243
|
+
if not isinstance(self.execution.response, NormalizedResponse):
|
|
244
|
+
raise ValidationError("Calling response is not normalized")
|
|
245
|
+
|
|
220
246
|
|
|
221
|
-
class
|
|
222
|
-
"""Base class for all
|
|
247
|
+
class ResourceBackend(Element):
|
|
248
|
+
"""Base class for all resource backends (Tool, Endpoint, etc.).
|
|
223
249
|
|
|
224
250
|
Inherits from krons.Element for UUID-based identity.
|
|
225
251
|
Subclasses must implement event_type and call() methods.
|
|
226
252
|
"""
|
|
227
253
|
|
|
228
|
-
config:
|
|
254
|
+
config: ResourceConfig = Field(..., description="Resource configuration")
|
|
229
255
|
|
|
230
256
|
@property
|
|
231
257
|
def provider(self) -> str:
|
|
@@ -234,17 +260,17 @@ class ServiceBackend(Element):
|
|
|
234
260
|
|
|
235
261
|
@property
|
|
236
262
|
def name(self) -> str:
|
|
237
|
-
"""
|
|
263
|
+
"""Resource name from config."""
|
|
238
264
|
return self.config.name
|
|
239
265
|
|
|
240
266
|
@property
|
|
241
267
|
def version(self) -> str | None:
|
|
242
|
-
"""
|
|
268
|
+
"""Resource version from config."""
|
|
243
269
|
return self.config.version
|
|
244
270
|
|
|
245
271
|
@property
|
|
246
272
|
def tags(self) -> set[str]:
|
|
247
|
-
"""
|
|
273
|
+
"""Resource tags from config."""
|
|
248
274
|
return set(self.config.tags) if self.config.tags else set()
|
|
249
275
|
|
|
250
276
|
@property
|
|
@@ -265,12 +291,12 @@ class ServiceBackend(Element):
|
|
|
265
291
|
to extract specific fields or add metadata.
|
|
266
292
|
|
|
267
293
|
Args:
|
|
268
|
-
raw_response: Raw response from
|
|
294
|
+
raw_response: Raw response from resource call
|
|
269
295
|
|
|
270
296
|
Returns:
|
|
271
297
|
NormalizedResponse with status, data, raw_response
|
|
272
298
|
"""
|
|
273
|
-
return
|
|
299
|
+
return NormalizedResponseModel(
|
|
274
300
|
status="success",
|
|
275
301
|
data=raw_response,
|
|
276
302
|
raw_response=raw_response,
|
|
@@ -278,7 +304,7 @@ class ServiceBackend(Element):
|
|
|
278
304
|
|
|
279
305
|
@abstractmethod
|
|
280
306
|
async def call(self, *args, **kw) -> NormalizedResponse:
|
|
281
|
-
"""Execute
|
|
307
|
+
"""Execute resource call and return normalized response."""
|
|
282
308
|
...
|
|
283
309
|
|
|
284
310
|
async def stream(self, *args, **kw):
|
|
@@ -44,7 +44,7 @@ from pydantic import (
|
|
|
44
44
|
model_validator,
|
|
45
45
|
)
|
|
46
46
|
|
|
47
|
-
from .backend import Calling, NormalizedResponse,
|
|
47
|
+
from .backend import Calling, NormalizedResponse, ResourceBackend, ResourceConfig
|
|
48
48
|
from .utilities.header_factory import AUTH_TYPES, HeaderFactory
|
|
49
49
|
from .utilities.resilience import CircuitBreaker, RetryConfig, retry_with_backoff
|
|
50
50
|
|
|
@@ -81,10 +81,10 @@ SYSTEM_ENV_VARS = frozenset(
|
|
|
81
81
|
B = TypeVar("B", bound=type[BaseModel])
|
|
82
82
|
|
|
83
83
|
|
|
84
|
-
class EndpointConfig(
|
|
84
|
+
class EndpointConfig(ResourceConfig):
|
|
85
85
|
"""HTTP endpoint configuration with secure credential handling.
|
|
86
86
|
|
|
87
|
-
Extends
|
|
87
|
+
Extends ResourceConfig with HTTP-specific settings: URL construction,
|
|
88
88
|
authentication, headers, and request validation.
|
|
89
89
|
|
|
90
90
|
Credential Security:
|
|
@@ -175,7 +175,9 @@ class EndpointConfig(ServiceConfig):
|
|
|
175
175
|
object.__setattr__(self, "_api_key", SecretStr(resolved.strip()))
|
|
176
176
|
else:
|
|
177
177
|
object.__setattr__(self, "api_key_is_env", False)
|
|
178
|
-
object.__setattr__(
|
|
178
|
+
object.__setattr__(
|
|
179
|
+
self, "_api_key", SecretStr(self.api_key.strip())
|
|
180
|
+
)
|
|
179
181
|
object.__setattr__(self, "api_key", None)
|
|
180
182
|
else:
|
|
181
183
|
object.__setattr__(self, "api_key_is_env", False)
|
|
@@ -204,7 +206,7 @@ class EndpointConfig(ServiceConfig):
|
|
|
204
206
|
return f"{self.base_url}/{self.endpoint.format(**self.params)}"
|
|
205
207
|
|
|
206
208
|
|
|
207
|
-
class Endpoint(
|
|
209
|
+
class Endpoint(ResourceBackend):
|
|
208
210
|
"""HTTP API backend with resilience patterns.
|
|
209
211
|
|
|
210
212
|
Wraps httpx.AsyncClient with circuit breaker and retry support.
|
|
@@ -252,7 +254,9 @@ class Endpoint(ServiceBackend):
|
|
|
252
254
|
secret_api_key = None
|
|
253
255
|
if isinstance(config, dict):
|
|
254
256
|
config_dict = {**config, **kwargs}
|
|
255
|
-
if "api_key" in config_dict and isinstance(
|
|
257
|
+
if "api_key" in config_dict and isinstance(
|
|
258
|
+
config_dict["api_key"], SecretStr
|
|
259
|
+
):
|
|
256
260
|
secret_api_key = config_dict.pop("api_key")
|
|
257
261
|
_config = EndpointConfig(**config_dict)
|
|
258
262
|
elif isinstance(config, EndpointConfig):
|
|
@@ -321,7 +325,11 @@ class Endpoint(ServiceBackend):
|
|
|
321
325
|
Raises:
|
|
322
326
|
ValueError: If request_options not defined or validation fails.
|
|
323
327
|
"""
|
|
324
|
-
request =
|
|
328
|
+
request = (
|
|
329
|
+
request
|
|
330
|
+
if isinstance(request, dict)
|
|
331
|
+
else request.model_dump(exclude_none=True)
|
|
332
|
+
)
|
|
325
333
|
|
|
326
334
|
payload = self.config.kwargs.copy()
|
|
327
335
|
payload.update(request)
|
|
@@ -397,7 +405,9 @@ class Endpoint(ServiceBackend):
|
|
|
397
405
|
|
|
398
406
|
if self.circuit_breaker:
|
|
399
407
|
|
|
400
|
-
async def cb_wrapped_call(
|
|
408
|
+
async def cb_wrapped_call(
|
|
409
|
+
p: dict[Any, Any], h: dict[Any, Any], **kw: Any
|
|
410
|
+
) -> Any:
|
|
401
411
|
return await self.circuit_breaker.execute(base_call, p, h, **kw) # type: ignore[union-attr]
|
|
402
412
|
|
|
403
413
|
inner_call = cb_wrapped_call
|
|
@@ -435,9 +445,7 @@ class Endpoint(ServiceBackend):
|
|
|
435
445
|
elif response.status_code != 200:
|
|
436
446
|
try:
|
|
437
447
|
error_body = response.json()
|
|
438
|
-
error_message =
|
|
439
|
-
f"Request failed with status {response.status_code}: {error_body}"
|
|
440
|
-
)
|
|
448
|
+
error_message = f"Request failed with status {response.status_code}: {error_body}"
|
|
441
449
|
except Exception:
|
|
442
450
|
error_message = f"Request failed with status {response.status_code}"
|
|
443
451
|
|
|
@@ -467,7 +475,9 @@ class Endpoint(ServiceBackend):
|
|
|
467
475
|
"""
|
|
468
476
|
payload, headers = self.create_payload(request, extra_headers, **kwargs)
|
|
469
477
|
|
|
470
|
-
async for chunk in self._stream_http(
|
|
478
|
+
async for chunk in self._stream_http(
|
|
479
|
+
payload=payload, headers=headers, **kwargs
|
|
480
|
+
):
|
|
471
481
|
yield chunk
|
|
472
482
|
|
|
473
483
|
async def _stream_http(self, payload: dict, headers: dict, **kwargs):
|
|
@@ -507,7 +517,9 @@ class Endpoint(ServiceBackend):
|
|
|
507
517
|
return circuit_breaker.to_dict()
|
|
508
518
|
|
|
509
519
|
@field_serializer("retry_config")
|
|
510
|
-
def _serialize_retry_config(
|
|
520
|
+
def _serialize_retry_config(
|
|
521
|
+
self, retry_config: RetryConfig | None
|
|
522
|
+
) -> dict[str, Any] | None:
|
|
511
523
|
"""Serialize RetryConfig to dict for transport."""
|
|
512
524
|
if retry_config is None:
|
|
513
525
|
return None
|
|
@@ -522,7 +534,9 @@ class Endpoint(ServiceBackend):
|
|
|
522
534
|
if isinstance(v, CircuitBreaker):
|
|
523
535
|
return v
|
|
524
536
|
if not isinstance(v, dict):
|
|
525
|
-
raise ValueError(
|
|
537
|
+
raise ValueError(
|
|
538
|
+
"circuit_breaker must be a dict or CircuitBreaker instance"
|
|
539
|
+
)
|
|
526
540
|
return CircuitBreaker(**v)
|
|
527
541
|
|
|
528
542
|
@field_validator("retry_config", mode="before")
|