krons 0.1.1__py3-none-any.whl → 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +126 -0
  103. krons/work/engine.py +333 -0
  104. krons/work/form.py +305 -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/{specs → work}/phrase.py +130 -13
  112. krons/{enforcement → work}/policy.py +3 -3
  113. krons/work/report.py +268 -0
  114. krons/work/rules/__init__.py +47 -0
  115. krons/{enforcement → work/rules}/common/boolean.py +3 -1
  116. krons/{enforcement → work/rules}/common/choice.py +9 -3
  117. krons/{enforcement → work/rules}/common/number.py +3 -1
  118. krons/{enforcement → work/rules}/common/string.py +9 -3
  119. krons/{enforcement → work/rules}/rule.py +1 -1
  120. krons/{enforcement → work/rules}/validator.py +20 -5
  121. krons/{enforcement → work}/service.py +16 -7
  122. krons/work/worker.py +266 -0
  123. {krons-0.1.1.dist-info → krons-0.2.0.dist-info}/METADATA +15 -1
  124. krons-0.2.0.dist-info/RECORD +154 -0
  125. krons/enforcement/__init__.py +0 -57
  126. krons/operations/registry.py +0 -92
  127. krons/services/__init__.py +0 -81
  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.0.dist-info}/WHEEL +0 -0
  142. {krons-0.1.1.dist-info → krons-0.2.0.dist-info}/licenses/LICENSE +0 -0
krons/core/__init__.py CHANGED
@@ -1,55 +1,47 @@
1
1
  # Copyright (c) 2025 - 2026, HaiyangLi <quantocean.li at gmail dot com>
2
2
  # SPDX-License-Identifier: Apache-2.0
3
3
 
4
- """Core primitives with lazy loading for fast import."""
4
+ """Core module - Foundation primitives and submodules.
5
+
6
+ Re-exports base classes from core/base/ and exposes submodules:
7
+ - types: Sentinels, base types, DB types
8
+ - specs: Spec definitions, Operable, adapters
9
+
10
+ Note: Session (Message, Branch, Session, Exchange) is now at krons.session
11
+ """
5
12
 
6
13
  from __future__ import annotations
7
14
 
8
15
  from typing import TYPE_CHECKING
9
16
 
10
- # Lazy import mapping
17
+ # Lazy import mapping - delegates to core.base
11
18
  _LAZY_IMPORTS: dict[str, tuple[str, str]] = {
12
- # broadcaster
13
- "Broadcaster": ("krons.core.broadcaster", "Broadcaster"),
14
- # element
15
- "Element": ("krons.core.element", "Element"),
16
- # event
17
- "Event": ("krons.core.event", "Event"),
18
- "EventStatus": ("krons.core.event", "EventStatus"),
19
- "Execution": ("krons.core.event", "Execution"),
20
- # eventbus
21
- "EventBus": ("krons.core.eventbus", "EventBus"),
22
- "Handler": ("krons.core.eventbus", "Handler"),
23
- # flow
24
- "Flow": ("krons.core.flow", "Flow"),
25
- # graph
26
- "Edge": ("krons.core.graph", "Edge"),
27
- "EdgeCondition": ("krons.core.graph", "EdgeCondition"),
28
- "Graph": ("krons.core.graph", "Graph"),
29
- # node
30
- "DEFAULT_NODE_CONFIG": ("krons.core.node", "DEFAULT_NODE_CONFIG"),
31
- "NODE_REGISTRY": ("krons.core.node", "NODE_REGISTRY"),
32
- "PERSISTABLE_NODE_REGISTRY": ("krons.core.node", "PERSISTABLE_NODE_REGISTRY"),
33
- "Node": ("krons.core.node", "Node"),
34
- "NodeConfig": ("krons.core.node", "NodeConfig"),
35
- "create_node": ("krons.core.node", "create_node"),
36
- "generate_ddl": ("krons.core.node", "generate_ddl"),
37
- # phrase
38
- "PHRASE_REGISTRY": ("krons.core.phrase", "PHRASE_REGISTRY"),
39
- "Phrase": ("krons.core.phrase", "Phrase"),
40
- "PhraseConfig": ("krons.core.phrase", "PhraseConfig"),
41
- "PhraseError": ("krons.core.phrase", "PhraseError"),
42
- "RequirementNotMet": ("krons.core.phrase", "RequirementNotMet"),
43
- "create_phrase": ("krons.core.phrase", "create_phrase"),
44
- "get_phrase": ("krons.core.phrase", "get_phrase"),
45
- "list_phrases": ("krons.core.phrase", "list_phrases"),
46
- # pile
47
- "Pile": ("krons.core.pile", "Pile"),
48
- # processor
49
- "Executor": ("krons.core.processor", "Executor"),
50
- "Processor": ("krons.core.processor", "Processor"),
51
- # progression
52
- "Progression": ("krons.core.progression", "Progression"),
19
+ # Registries
20
+ "NODE_REGISTRY": ("krons.core.base", "NODE_REGISTRY"),
21
+ "PERSISTABLE_NODE_REGISTRY": ("krons.core.base", "PERSISTABLE_NODE_REGISTRY"),
22
+ # Classes
23
+ "Broadcaster": ("krons.core.base", "Broadcaster"),
24
+ "Edge": ("krons.core.base", "Edge"),
25
+ "EdgeCondition": ("krons.core.base", "EdgeCondition"),
26
+ "Element": ("krons.core.base", "Element"),
27
+ "Event": ("krons.core.base", "Event"),
28
+ "EventBus": ("krons.core.base", "EventBus"),
29
+ "EventStatus": ("krons.core.base", "EventStatus"),
30
+ "Execution": ("krons.core.base", "Execution"),
31
+ "Executor": ("krons.core.base", "Executor"),
32
+ "Flow": ("krons.core.base", "Flow"),
33
+ "Graph": ("krons.core.base", "Graph"),
34
+ "Handler": ("krons.core.base", "Handler"),
35
+ "Node": ("krons.core.base", "Node"),
36
+ "NodeConfig": ("krons.core.base", "NodeConfig"),
37
+ "Pile": ("krons.core.base", "Pile"),
38
+ "Processor": ("krons.core.base", "Processor"),
39
+ "Progression": ("krons.core.base", "Progression"),
40
+ # Functions
41
+ "create_node": ("krons.core.base", "create_node"),
42
+ "generate_all_ddl": ("krons.core.base", "generate_all_ddl"),
43
+ "generate_ddl": ("krons.core.base", "generate_ddl"),
44
+ "get_fk_dependencies": ("krons.core.base", "get_fk_dependencies"),
53
45
  }
54
46
 
55
47
  _LOADED: dict[str, object] = {}
@@ -79,41 +71,36 @@ def __dir__() -> list[str]:
79
71
 
80
72
  # TYPE_CHECKING block for static analysis
81
73
  if TYPE_CHECKING:
82
- from .broadcaster import Broadcaster
83
- from .element import Element
84
- from .event import Event, EventStatus, Execution
85
- from .eventbus import EventBus, Handler
86
- from .flow import Flow
87
- from .graph import Edge, EdgeCondition, Graph
88
- from .node import (
89
- DEFAULT_NODE_CONFIG,
74
+ from krons.core.base import (
90
75
  NODE_REGISTRY,
91
76
  PERSISTABLE_NODE_REGISTRY,
77
+ Broadcaster,
78
+ Edge,
79
+ EdgeCondition,
80
+ Element,
81
+ Event,
82
+ EventBus,
83
+ EventStatus,
84
+ Execution,
85
+ Executor,
86
+ Flow,
87
+ Graph,
88
+ Handler,
92
89
  Node,
93
90
  NodeConfig,
91
+ Pile,
92
+ Processor,
93
+ Progression,
94
94
  create_node,
95
+ generate_all_ddl,
95
96
  generate_ddl,
97
+ get_fk_dependencies,
96
98
  )
97
- from .phrase import (
98
- PHRASE_REGISTRY,
99
- Phrase,
100
- PhraseConfig,
101
- PhraseError,
102
- RequirementNotMet,
103
- create_phrase,
104
- get_phrase,
105
- list_phrases,
106
- )
107
- from .pile import Pile
108
- from .processor import Executor, Processor
109
- from .progression import Progression
110
99
 
111
100
  __all__ = [
112
- # constants
113
- "DEFAULT_NODE_CONFIG",
101
+ # constants/registries
114
102
  "NODE_REGISTRY",
115
103
  "PERSISTABLE_NODE_REGISTRY",
116
- "PHRASE_REGISTRY",
117
104
  # classes
118
105
  "Broadcaster",
119
106
  "Edge",
@@ -129,17 +116,12 @@ __all__ = [
129
116
  "Handler",
130
117
  "Node",
131
118
  "NodeConfig",
132
- "Phrase",
133
- "PhraseConfig",
134
- "PhraseError",
135
119
  "Pile",
136
120
  "Processor",
137
121
  "Progression",
138
- "RequirementNotMet",
139
122
  # functions
140
123
  "create_node",
141
- "create_phrase",
124
+ "generate_all_ddl",
142
125
  "generate_ddl",
143
- "get_phrase",
144
- "list_phrases",
126
+ "get_fk_dependencies",
145
127
  ]
@@ -0,0 +1,121 @@
1
+ # Copyright (c) 2025 - 2026, HaiyangLi <quantocean.li at gmail dot com>
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Core primitives with lazy loading for fast import."""
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import TYPE_CHECKING
9
+
10
+ # Lazy import mapping - all modules are in krons.core.base.*
11
+ _LAZY_IMPORTS: dict[str, tuple[str, str]] = {
12
+ # broadcaster
13
+ "Broadcaster": ("krons.core.base.broadcaster", "Broadcaster"),
14
+ # element
15
+ "Element": ("krons.core.base.element", "Element"),
16
+ # event
17
+ "Event": ("krons.core.base.event", "Event"),
18
+ "EventStatus": ("krons.core.base.event", "EventStatus"),
19
+ "Execution": ("krons.core.base.event", "Execution"),
20
+ # eventbus
21
+ "EventBus": ("krons.core.base.eventbus", "EventBus"),
22
+ "Handler": ("krons.core.base.eventbus", "Handler"),
23
+ # flow
24
+ "Flow": ("krons.core.base.flow", "Flow"),
25
+ # graph
26
+ "Edge": ("krons.core.base.graph", "Edge"),
27
+ "EdgeCondition": ("krons.core.base.graph", "EdgeCondition"),
28
+ "Graph": ("krons.core.base.graph", "Graph"),
29
+ # node
30
+ "NODE_REGISTRY": ("krons.core.base.node", "NODE_REGISTRY"),
31
+ "PERSISTABLE_NODE_REGISTRY": ("krons.core.base.node", "PERSISTABLE_NODE_REGISTRY"),
32
+ "Node": ("krons.core.base.node", "Node"),
33
+ "NodeConfig": ("krons.core.base.node", "NodeConfig"),
34
+ "create_node": ("krons.core.base.node", "create_node"),
35
+ "generate_ddl": ("krons.core.base.node", "generate_ddl"),
36
+ "generate_all_ddl": ("krons.core.base.node", "generate_all_ddl"),
37
+ "get_fk_dependencies": ("krons.core.base.node", "get_fk_dependencies"),
38
+ # pile
39
+ "Pile": ("krons.core.base.pile", "Pile"),
40
+ # processor
41
+ "Executor": ("krons.core.base.processor", "Executor"),
42
+ "Processor": ("krons.core.base.processor", "Processor"),
43
+ # progression
44
+ "Progression": ("krons.core.base.progression", "Progression"),
45
+ }
46
+
47
+ _LOADED: dict[str, object] = {}
48
+
49
+
50
+ def __getattr__(name: str) -> object:
51
+ """Lazy import attributes on first access."""
52
+ if name in _LOADED:
53
+ return _LOADED[name]
54
+
55
+ if name in _LAZY_IMPORTS:
56
+ from importlib import import_module
57
+
58
+ module_name, attr_name = _LAZY_IMPORTS[name]
59
+ module = import_module(module_name)
60
+ value = getattr(module, attr_name)
61
+ _LOADED[name] = value
62
+ return value
63
+
64
+ raise AttributeError(f"module 'krons.core' has no attribute {name!r}")
65
+
66
+
67
+ def __dir__() -> list[str]:
68
+ """Return all available attributes for autocomplete."""
69
+ return list(__all__)
70
+
71
+
72
+ # TYPE_CHECKING block for static analysis
73
+ if TYPE_CHECKING:
74
+ from .broadcaster import Broadcaster
75
+ from .element import Element
76
+ from .event import Event, EventStatus, Execution
77
+ from .eventbus import EventBus, Handler
78
+ from .flow import Flow
79
+ from .graph import Edge, EdgeCondition, Graph
80
+ from .node import (
81
+ NODE_REGISTRY,
82
+ PERSISTABLE_NODE_REGISTRY,
83
+ Node,
84
+ NodeConfig,
85
+ create_node,
86
+ generate_all_ddl,
87
+ generate_ddl,
88
+ get_fk_dependencies,
89
+ )
90
+ from .pile import Pile
91
+ from .processor import Executor, Processor
92
+ from .progression import Progression
93
+
94
+ __all__ = [
95
+ # constants/registries
96
+ "NODE_REGISTRY",
97
+ "PERSISTABLE_NODE_REGISTRY",
98
+ # classes
99
+ "Broadcaster",
100
+ "Edge",
101
+ "EdgeCondition",
102
+ "Element",
103
+ "Event",
104
+ "EventBus",
105
+ "EventStatus",
106
+ "Execution",
107
+ "Executor",
108
+ "Flow",
109
+ "Graph",
110
+ "Handler",
111
+ "Node",
112
+ "NodeConfig",
113
+ "Pile",
114
+ "Processor",
115
+ "Progression",
116
+ # functions
117
+ "create_node",
118
+ "generate_all_ddl",
119
+ "generate_ddl",
120
+ "get_fk_dependencies",
121
+ ]
@@ -8,7 +8,7 @@ import weakref
8
8
  from collections.abc import Awaitable, Callable
9
9
  from typing import Any, ClassVar
10
10
 
11
- from krons.utils import is_coro_func
11
+ from krons.utils.concurrency import is_coro_func
12
12
 
13
13
  logger = logging.getLogger(__name__)
14
14
 
@@ -41,7 +41,9 @@ class Broadcaster:
41
41
  return cls._instance
42
42
 
43
43
  @classmethod
44
- def subscribe(cls, callback: Callable[[Any], None] | Callable[[Any], Awaitable[None]]) -> None:
44
+ def subscribe(
45
+ cls, callback: Callable[[Any], None] | Callable[[Any], Awaitable[None]]
46
+ ) -> None:
45
47
  """Add subscriber callback (idempotent, stored as weakref).
46
48
 
47
49
  Args:
@@ -51,7 +53,9 @@ class Broadcaster:
51
53
  for weak_ref in cls._subscribers:
52
54
  if weak_ref() is callback:
53
55
  return
54
- weak_callback: weakref.ref[Callable[[Any], None] | Callable[[Any], Awaitable[None]]]
56
+ weak_callback: weakref.ref[
57
+ Callable[[Any], None] | Callable[[Any], Awaitable[None]]
58
+ ]
55
59
  if hasattr(callback, "__self__"):
56
60
  weak_callback = weakref.WeakMethod(callback) # type: ignore[assignment]
57
61
  else:
@@ -11,6 +11,7 @@ from uuid import UUID, uuid4
11
11
  import orjson
12
12
  from pydantic import BaseModel, ConfigDict, Field, field_validator
13
13
 
14
+ from krons.core.types import MaybeSentinel, Unset, UnsetType, is_sentinel, is_unset
14
15
  from krons.protocols import (
15
16
  Deserializable,
16
17
  Hashable,
@@ -18,7 +19,6 @@ from krons.protocols import (
18
19
  Serializable,
19
20
  implements,
20
21
  )
21
- from krons.types import MaybeSentinel, Unset, UnsetType, is_sentinel, is_unset
22
22
  from krons.utils import (
23
23
  coerce_created_at,
24
24
  json_dump,
@@ -68,7 +68,9 @@ class Element(BaseModel):
68
68
 
69
69
  @field_validator("metadata", mode="before")
70
70
  @classmethod
71
- def _validate_meta_integrity(cls, val: dict[str, Any] | MaybeSentinel) -> dict[str, Any]:
71
+ def _validate_meta_integrity(
72
+ cls, val: dict[str, Any] | MaybeSentinel
73
+ ) -> dict[str, Any]:
72
74
  """Validate and coerce metadata to dict. Raises ValueError if conversion fails."""
73
75
  if is_sentinel(val, {"none"}):
74
76
  return {}
@@ -108,7 +110,9 @@ class Element(BaseModel):
108
110
  def to_dict(
109
111
  self,
110
112
  mode: Literal["python", "json", "db"] = "python",
111
- created_at_format: (Literal["datetime", "isoformat", "timestamp"] | UnsetType) = Unset,
113
+ created_at_format: (
114
+ Literal["datetime", "isoformat", "timestamp"] | UnsetType
115
+ ) = Unset,
112
116
  meta_key: str | UnsetType = Unset,
113
117
  **kwargs: Any,
114
118
  ) -> dict[str, Any]:
@@ -187,7 +191,9 @@ class Element(BaseModel):
187
191
  try:
188
192
  target_cls = load_type_from_string(kron_class)
189
193
  except ValueError as e:
190
- raise ValueError(f"Failed to deserialize class '{kron_class}': {e}") from e
194
+ raise ValueError(
195
+ f"Failed to deserialize class '{kron_class}': {e}"
196
+ ) from e
191
197
 
192
198
  if not issubclass(target_cls, Element):
193
199
  raise ValueError(
@@ -195,7 +201,9 @@ class Element(BaseModel):
195
201
  f"Cannot deserialize into {cls.__name__}"
196
202
  )
197
203
 
198
- target_func = getattr(target_cls.from_dict, "__func__", target_cls.from_dict)
204
+ target_func = getattr(
205
+ target_cls.from_dict, "__func__", target_cls.from_dict
206
+ )
199
207
  cls_func = getattr(cls.from_dict, "__func__", cls.from_dict)
200
208
  if target_func is cls_func:
201
209
  return target_cls.model_validate(data, **kwargs)
@@ -11,9 +11,16 @@ from typing import Any, final
11
11
  import orjson
12
12
  from pydantic import Field, field_serializer, field_validator
13
13
 
14
- from krons.errors import KronError, KronTimeoutError
14
+ from krons.core.types import (
15
+ Enum,
16
+ MaybeSentinel,
17
+ MaybeUnset,
18
+ Unset,
19
+ is_sentinel,
20
+ is_unset,
21
+ )
22
+ from krons.errors import KronsError, KronTimeoutError, ValidationError
15
23
  from krons.protocols import Invocable, Serializable, implements
16
- from krons.types import Enum, MaybeSentinel, MaybeUnset, Unset, is_sentinel, is_unset
17
24
  from krons.utils import async_synchronized, concurrency, json_dumpb
18
25
 
19
26
  from .element import LN_ELEMENT_FIELDS, Element
@@ -142,7 +149,9 @@ class Execution:
142
149
  if isinstance(exc, Serializable):
143
150
  exceptions.append(exc.to_dict())
144
151
  elif isinstance(exc, ExceptionGroup):
145
- exceptions.append(self._serialize_exception_group(exc, depth + 1, _seen))
152
+ exceptions.append(
153
+ self._serialize_exception_group(exc, depth + 1, _seen)
154
+ )
146
155
  else:
147
156
  exceptions.append(
148
157
  {
@@ -262,11 +271,14 @@ class Event(Element):
262
271
  except Exception as e:
263
272
  if isinstance(e, ExceptionGroup):
264
273
  retryable = all(
265
- not isinstance(exc, KronError) or exc.retryable for exc in e.exceptions
274
+ not isinstance(exc, KronsError) or exc.retryable
275
+ for exc in e.exceptions
266
276
  )
267
277
  self.execution.retryable = retryable
268
278
  else:
269
- self.execution.retryable = e.retryable if isinstance(e, KronError) else True
279
+ self.execution.retryable = (
280
+ e.retryable if isinstance(e, KronsError) else True
281
+ )
270
282
 
271
283
  self.execution.response = Unset
272
284
  self.execution.error = e
@@ -286,7 +298,9 @@ class Event(Element):
286
298
 
287
299
  async def stream(self) -> Any:
288
300
  """Stream execution results. Override if streaming=True."""
289
- raise NotImplementedError("Subclasses must implement stream() if streaming=True")
301
+ raise NotImplementedError(
302
+ "Subclasses must implement stream() if streaming=True"
303
+ )
290
304
 
291
305
  def as_fresh_event(self, copy_meta: bool = False) -> Event:
292
306
  """Clone with reset execution state (new ID, PENDING status).
@@ -314,3 +328,22 @@ class Event(Element):
314
328
  "created_at": self.created_at,
315
329
  }
316
330
  return fresh
331
+
332
+ def assert_completed(self, *, retryable: MaybeUnset[bool] = Unset):
333
+ if self.execution.status != EventStatus.COMPLETED:
334
+ retryable_value = (
335
+ self.execution.retryable if is_unset(retryable) else retryable
336
+ )
337
+ retryable_value = True if retryable_value is True else False
338
+ exec_dict = self.execution.to_dict()
339
+ exec_dict.pop("response", None)
340
+ exec_dict.pop("retryable", None)
341
+
342
+ raise ValidationError(
343
+ "Event did not complete successfully.",
344
+ details={
345
+ "event_id": str(self.id),
346
+ **exec_dict,
347
+ },
348
+ retryable=retryable_value,
349
+ )
@@ -85,7 +85,9 @@ class EventBus:
85
85
  if topic not in self._subs:
86
86
  return
87
87
  if handlers := self._cleanup_dead_refs(topic):
88
- await gather(*(h(*args, **kwargs) for h in handlers), return_exceptions=True)
88
+ await gather(
89
+ *(h(*args, **kwargs) for h in handlers), return_exceptions=True
90
+ )
89
91
 
90
92
  def clear(self, topic: str | None = None) -> None:
91
93
  """Clear subscriptions.
@@ -9,9 +9,9 @@ from uuid import UUID
9
9
 
10
10
  from pydantic import Field, PrivateAttr, field_validator, model_validator
11
11
 
12
+ from krons.core.types import Unset, UnsetType
12
13
  from krons.errors import ExistsError, NotFoundError
13
14
  from krons.protocols import Serializable, implements
14
- from krons.types import Unset, UnsetType
15
15
  from krons.utils import extract_types, synchronized
16
16
 
17
17
  from .element import Element
@@ -99,7 +99,9 @@ class Flow(Element, Generic[E, P]):
99
99
 
100
100
  # Create Pile with items and type validation (item_type/strict_type are frozen)
101
101
  # Even if items=None, create Pile if item_type/strict_type specified
102
- data["items"] = Pile(items=items, item_type=item_type, strict_type=strict_type)
102
+ data["items"] = Pile(
103
+ items=items, item_type=item_type, strict_type=strict_type
104
+ )
103
105
 
104
106
  # Handle progressions - let field validator convert dict/list to Pile
105
107
  if progressions is not None:
@@ -217,7 +219,10 @@ class Flow(Element, Generic[E, P]):
217
219
  NotFoundError: If progression not found.
218
220
  """
219
221
  name_to_delete: str | None
220
- if isinstance(progression_id, str) and progression_id in self._progression_names:
222
+ if (
223
+ isinstance(progression_id, str)
224
+ and progression_id in self._progression_names
225
+ ):
221
226
  uid = self._progression_names[progression_id]
222
227
  name_to_delete = progression_id
223
228
  else:
@@ -323,7 +328,9 @@ class Flow(Element, Generic[E, P]):
323
328
  def to_dict(
324
329
  self,
325
330
  mode: Literal["python", "json", "db"] = "python",
326
- created_at_format: (Literal["datetime", "isoformat", "timestamp"] | UnsetType) = Unset,
331
+ created_at_format: (
332
+ Literal["datetime", "isoformat", "timestamp"] | UnsetType
333
+ ) = Unset,
327
334
  meta_key: str | UnsetType = Unset,
328
335
  **kwargs: Any,
329
336
  ) -> dict[str, Any]:
@@ -11,9 +11,9 @@ from uuid import UUID
11
11
  from pydantic import Field, PrivateAttr, field_validator, model_validator
12
12
  from typing_extensions import override
13
13
 
14
+ from krons.core.types import Unset, UnsetType, is_unset
14
15
  from krons.errors import NotFoundError
15
16
  from krons.protocols import Containable, Deserializable, Serializable, implements
16
- from krons.types import Unset, UnsetType, is_unset
17
17
  from krons.utils import synchronized
18
18
 
19
19
  from .element import Element
@@ -61,7 +61,9 @@ class Edge(Element):
61
61
  condition: EdgeCondition | None = Field(
62
62
  default=None, exclude=True, description="Runtime traversal predicate"
63
63
  )
64
- properties: dict[str, Any] = Field(default_factory=dict, description="Custom edge attributes")
64
+ properties: dict[str, Any] = Field(
65
+ default_factory=dict, description="Custom edge attributes"
66
+ )
65
67
 
66
68
  @field_validator("head", "tail", mode="before")
67
69
  @classmethod
@@ -212,12 +214,16 @@ class Graph(Element):
212
214
  def get_predecessors(self, node_id: UUID | Node) -> list[Node]:
213
215
  """Get nodes with edges pointing to this node (in-neighbors)."""
214
216
  nid = self._coerce_id(node_id)
215
- return [self.nodes[self.edges[eid].head] for eid in self._in_edges.get(nid, set())]
217
+ return [
218
+ self.nodes[self.edges[eid].head] for eid in self._in_edges.get(nid, set())
219
+ ]
216
220
 
217
221
  def get_successors(self, node_id: UUID | Node) -> list[Node]:
218
222
  """Get nodes this node points to (out-neighbors)."""
219
223
  nid = self._coerce_id(node_id)
220
- return [self.nodes[self.edges[eid].tail] for eid in self._out_edges.get(nid, set())]
224
+ return [
225
+ self.nodes[self.edges[eid].tail] for eid in self._out_edges.get(nid, set())
226
+ ]
221
227
 
222
228
  def get_node_edges(
223
229
  self,
@@ -240,11 +246,17 @@ class Graph(Element):
240
246
 
241
247
  def get_heads(self) -> list[Node]:
242
248
  """Get source nodes (no incoming edges)."""
243
- return [self.nodes[nid] for nid, in_edges in self._in_edges.items() if not in_edges]
249
+ return [
250
+ self.nodes[nid] for nid, in_edges in self._in_edges.items() if not in_edges
251
+ ]
244
252
 
245
253
  def get_tails(self) -> list[Node]:
246
254
  """Get sink nodes (no outgoing edges)."""
247
- return [self.nodes[nid] for nid, out_edges in self._out_edges.items() if not out_edges]
255
+ return [
256
+ self.nodes[nid]
257
+ for nid, out_edges in self._out_edges.items()
258
+ if not out_edges
259
+ ]
248
260
 
249
261
  # ==================== Graph Algorithms ====================
250
262
 
@@ -361,10 +373,14 @@ class Graph(Element):
361
373
  def to_dict(
362
374
  self,
363
375
  mode: Literal["python", "json", "db"] = "python",
364
- created_at_format: (Literal["datetime", "isoformat", "timestamp"] | UnsetType) = Unset,
376
+ created_at_format: (
377
+ Literal["datetime", "isoformat", "timestamp"] | UnsetType
378
+ ) = Unset,
365
379
  meta_key: str | UnsetType = Unset,
366
380
  item_meta_key: str | UnsetType = Unset,
367
- item_created_at_format: (Literal["datetime", "isoformat", "timestamp"] | UnsetType) = Unset,
381
+ item_created_at_format: (
382
+ Literal["datetime", "isoformat", "timestamp"] | UnsetType
383
+ ) = Unset,
368
384
  **kwargs: Any,
369
385
  ) -> dict[str, Any]:
370
386
  """Serialize graph with nodes and edges as nested Pile dicts.