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.
Files changed (142) hide show
  1. krons/__init__.py +49 -0
  2. krons/agent/__init__.py +144 -0
  3. krons/agent/mcps/__init__.py +14 -0
  4. krons/agent/mcps/loader.py +287 -0
  5. krons/agent/mcps/wrapper.py +799 -0
  6. krons/agent/message/__init__.py +20 -0
  7. krons/agent/message/action.py +69 -0
  8. krons/agent/message/assistant.py +52 -0
  9. krons/agent/message/common.py +49 -0
  10. krons/agent/message/instruction.py +130 -0
  11. krons/agent/message/prepare_msg.py +187 -0
  12. krons/agent/message/role.py +53 -0
  13. krons/agent/message/system.py +53 -0
  14. krons/agent/operations/__init__.py +82 -0
  15. krons/agent/operations/act.py +100 -0
  16. krons/agent/operations/generate.py +145 -0
  17. krons/agent/operations/llm_reparse.py +89 -0
  18. krons/agent/operations/operate.py +247 -0
  19. krons/agent/operations/parse.py +243 -0
  20. krons/agent/operations/react.py +286 -0
  21. krons/agent/operations/specs.py +235 -0
  22. krons/agent/operations/structure.py +151 -0
  23. krons/agent/operations/utils.py +79 -0
  24. krons/agent/providers/__init__.py +17 -0
  25. krons/agent/providers/anthropic_messages.py +146 -0
  26. krons/agent/providers/claude_code.py +276 -0
  27. krons/agent/providers/gemini.py +268 -0
  28. krons/agent/providers/match.py +75 -0
  29. krons/agent/providers/oai_chat.py +174 -0
  30. krons/agent/third_party/__init__.py +2 -0
  31. krons/agent/third_party/anthropic_models.py +154 -0
  32. krons/agent/third_party/claude_code.py +682 -0
  33. krons/agent/third_party/gemini_models.py +508 -0
  34. krons/agent/third_party/openai_models.py +295 -0
  35. krons/agent/tool.py +291 -0
  36. krons/core/__init__.py +56 -74
  37. krons/core/base/__init__.py +121 -0
  38. krons/core/{broadcaster.py → base/broadcaster.py} +7 -3
  39. krons/core/{element.py → base/element.py} +13 -5
  40. krons/core/{event.py → base/event.py} +39 -6
  41. krons/core/{eventbus.py → base/eventbus.py} +3 -1
  42. krons/core/{flow.py → base/flow.py} +11 -4
  43. krons/core/{graph.py → base/graph.py} +24 -8
  44. krons/core/{node.py → base/node.py} +44 -19
  45. krons/core/{pile.py → base/pile.py} +22 -8
  46. krons/core/{processor.py → base/processor.py} +21 -7
  47. krons/core/{progression.py → base/progression.py} +3 -1
  48. krons/{specs → core/specs}/__init__.py +0 -5
  49. krons/{specs → core/specs}/adapters/dataclass_field.py +16 -8
  50. krons/{specs → core/specs}/adapters/pydantic_adapter.py +11 -5
  51. krons/{specs → core/specs}/adapters/sql_ddl.py +14 -8
  52. krons/{specs → core/specs}/catalog/__init__.py +2 -2
  53. krons/{specs → core/specs}/catalog/_audit.py +2 -2
  54. krons/{specs → core/specs}/catalog/_common.py +2 -2
  55. krons/{specs → core/specs}/catalog/_content.py +4 -4
  56. krons/{specs → core/specs}/catalog/_enforcement.py +3 -3
  57. krons/{specs → core/specs}/factory.py +5 -5
  58. krons/{specs → core/specs}/operable.py +8 -2
  59. krons/{specs → core/specs}/protocol.py +4 -2
  60. krons/{specs → core/specs}/spec.py +23 -11
  61. krons/{types → core/types}/base.py +4 -2
  62. krons/{types → core/types}/db_types.py +2 -2
  63. krons/errors.py +13 -13
  64. krons/protocols.py +9 -4
  65. krons/resource/__init__.py +89 -0
  66. krons/{services → resource}/backend.py +48 -22
  67. krons/{services → resource}/endpoint.py +28 -14
  68. krons/{services → resource}/hook.py +20 -7
  69. krons/{services → resource}/imodel.py +46 -28
  70. krons/{services → resource}/registry.py +26 -24
  71. krons/{services → resource}/utilities/rate_limited_executor.py +7 -3
  72. krons/{services → resource}/utilities/rate_limiter.py +3 -1
  73. krons/{services → resource}/utilities/resilience.py +15 -5
  74. krons/resource/utilities/token_calculator.py +185 -0
  75. krons/session/__init__.py +12 -17
  76. krons/session/constraints.py +70 -0
  77. krons/session/exchange.py +11 -3
  78. krons/session/message.py +3 -1
  79. krons/session/registry.py +35 -0
  80. krons/session/session.py +165 -174
  81. krons/utils/__init__.py +45 -0
  82. krons/utils/_function_arg_parser.py +99 -0
  83. krons/utils/_pythonic_function_call.py +249 -0
  84. krons/utils/_to_list.py +9 -3
  85. krons/utils/_utils.py +6 -2
  86. krons/utils/concurrency/_async_call.py +4 -2
  87. krons/utils/concurrency/_errors.py +3 -1
  88. krons/utils/concurrency/_patterns.py +3 -1
  89. krons/utils/concurrency/_resource_tracker.py +6 -2
  90. krons/utils/display.py +257 -0
  91. krons/utils/fuzzy/__init__.py +6 -1
  92. krons/utils/fuzzy/_fuzzy_match.py +14 -8
  93. krons/utils/fuzzy/_string_similarity.py +3 -1
  94. krons/utils/fuzzy/_to_dict.py +3 -1
  95. krons/utils/schemas/__init__.py +26 -0
  96. krons/utils/schemas/_breakdown_pydantic_annotation.py +131 -0
  97. krons/utils/schemas/_formatter.py +72 -0
  98. krons/utils/schemas/_minimal_yaml.py +151 -0
  99. krons/utils/schemas/_typescript.py +153 -0
  100. krons/utils/validators/__init__.py +3 -0
  101. krons/utils/validators/_validate_image_url.py +56 -0
  102. krons/work/__init__.py +115 -0
  103. krons/work/engine.py +333 -0
  104. krons/work/form.py +242 -0
  105. krons/{operations → work/operations}/__init__.py +7 -4
  106. krons/{operations → work/operations}/builder.py +1 -1
  107. krons/{enforcement → work/operations}/context.py +36 -5
  108. krons/{operations → work/operations}/flow.py +13 -5
  109. krons/{operations → work/operations}/node.py +45 -43
  110. krons/work/operations/registry.py +103 -0
  111. krons/work/report.py +268 -0
  112. krons/work/rules/__init__.py +47 -0
  113. krons/{enforcement → work/rules}/common/boolean.py +3 -1
  114. krons/{enforcement → work/rules}/common/choice.py +9 -3
  115. krons/{enforcement → work/rules}/common/number.py +3 -1
  116. krons/{enforcement → work/rules}/common/string.py +9 -3
  117. krons/{enforcement → work/rules}/rule.py +1 -1
  118. krons/{enforcement → work/rules}/validator.py +20 -5
  119. krons/work/worker.py +266 -0
  120. {krons-0.1.1.dist-info → krons-0.2.1.dist-info}/METADATA +15 -1
  121. krons-0.2.1.dist-info/RECORD +151 -0
  122. krons/enforcement/__init__.py +0 -57
  123. krons/enforcement/policy.py +0 -80
  124. krons/enforcement/service.py +0 -370
  125. krons/operations/registry.py +0 -92
  126. krons/services/__init__.py +0 -81
  127. krons/specs/phrase.py +0 -405
  128. krons-0.1.1.dist-info/RECORD +0 -101
  129. /krons/{specs → core/specs}/adapters/__init__.py +0 -0
  130. /krons/{specs → core/specs}/adapters/_utils.py +0 -0
  131. /krons/{specs → core/specs}/adapters/factory.py +0 -0
  132. /krons/{types → core/types}/__init__.py +0 -0
  133. /krons/{types → core/types}/_sentinel.py +0 -0
  134. /krons/{types → core/types}/identity.py +0 -0
  135. /krons/{services → resource}/utilities/__init__.py +0 -0
  136. /krons/{services → resource}/utilities/header_factory.py +0 -0
  137. /krons/{enforcement → work/rules}/common/__init__.py +0 -0
  138. /krons/{enforcement → work/rules}/common/mapping.py +0 -0
  139. /krons/{enforcement → work/rules}/common/model.py +0 -0
  140. /krons/{enforcement → work/rules}/registry.py +0 -0
  141. {krons-0.1.1.dist-info → krons-0.2.1.dist-info}/WHEEL +0 -0
  142. {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.protocols import Hashable, implements
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(ValueError("Cannot provide both 'default' and 'default_factory'"))
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(ValueError("Validators must be a list of functions or a function"))
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(f"base_type must be a type or type annotation, got {base_type}")
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(self, validator: Callable[..., Any] | list[Callable[..., Any]]) -> Self:
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 = Any if is_sentinel(self.base_type, {"none"}) else self.base_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 {meta.key: meta.value for meta in self.metadata if meta.key not in exclude}
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(f.name for f in fields(cls) if not f.name.startswith("_"))
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 ServiceConfig(HashableModel):
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 KronError and include:
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
- "KronError",
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 KronError(Exception):
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(KronError):
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(KronError):
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(KronError):
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(KronError):
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(KronError):
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(KronError):
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(KronError):
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(KronError):
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(KronError):
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(KronError):
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(" - 'kwargs': protocol accepts **kwargs but implementation doesn't")
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 for p in protocol_params.values()
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 for p in protocol_params.values()
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" + "\n".join(errors)
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 ServiceConfig(HashableModel):
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
- class NormalizedResponse(HashableModel):
77
- """Generic normalized response for all service backends.
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 service.
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(default=None, description="Error message if status='error'")
86
- raw_response: dict[str, Any] = Field(..., description="Original unmodified response")
87
- metadata: dict[str, Any] | None = Field(default=None, description="Provider-specific metadata")
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 service invocation.
114
+ Always delegates to backend.call() for actual resource invocation.
99
115
 
100
116
  Attributes:
101
- backend: ServiceBackend instance (Tool, Endpoint, etc.)
117
+ backend: ResourceBackend instance (Tool, Endpoint, etc.)
102
118
  payload: Request payload/arguments for backend call
103
119
  """
104
120
 
105
- backend: ServiceBackend = Field(..., exclude=True, description="Service backend instance")
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 service call via backend (post-hook runs in finally)
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 ServiceBackend(Element):
222
- """Base class for all service backends (Tool, Endpoint, etc.).
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: ServiceConfig = Field(..., description="Service configuration")
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
- """Service name from config."""
263
+ """Resource name from config."""
238
264
  return self.config.name
239
265
 
240
266
  @property
241
267
  def version(self) -> str | None:
242
- """Service version from config."""
268
+ """Resource version from config."""
243
269
  return self.config.version
244
270
 
245
271
  @property
246
272
  def tags(self) -> set[str]:
247
- """Service tags from config."""
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 service call
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 NormalizedResponse(
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 service call and return normalized response."""
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, ServiceBackend, ServiceConfig
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(ServiceConfig):
84
+ class EndpointConfig(ResourceConfig):
85
85
  """HTTP endpoint configuration with secure credential handling.
86
86
 
87
- Extends ServiceConfig with HTTP-specific settings: URL construction,
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__(self, "_api_key", SecretStr(self.api_key.strip()))
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(ServiceBackend):
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(config_dict["api_key"], SecretStr):
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 = request if isinstance(request, dict) else request.model_dump(exclude_none=True)
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(p: dict[Any, Any], h: dict[Any, Any], **kw: Any) -> Any:
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(payload=payload, headers=headers, **kwargs):
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(self, retry_config: RetryConfig | None) -> dict[str, Any] | None:
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("circuit_breaker must be a dict or CircuitBreaker instance")
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")