krons 0.1.0__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 (162) 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 +127 -0
  37. krons/core/base/__init__.py +121 -0
  38. {kronos/core → krons/core/base}/broadcaster.py +7 -3
  39. {kronos/core → krons/core/base}/element.py +15 -7
  40. {kronos/core → krons/core/base}/event.py +41 -8
  41. {kronos/core → krons/core/base}/eventbus.py +4 -2
  42. {kronos/core → krons/core/base}/flow.py +14 -7
  43. {kronos/core → krons/core/base}/graph.py +27 -11
  44. {kronos/core → krons/core/base}/node.py +47 -22
  45. {kronos/core → krons/core/base}/pile.py +26 -12
  46. {kronos/core → krons/core/base}/processor.py +23 -9
  47. {kronos/core → krons/core/base}/progression.py +5 -3
  48. {kronos → krons/core}/specs/__init__.py +0 -5
  49. {kronos → krons/core}/specs/adapters/dataclass_field.py +16 -8
  50. {kronos → krons/core}/specs/adapters/pydantic_adapter.py +11 -5
  51. {kronos → krons/core}/specs/adapters/sql_ddl.py +16 -10
  52. {kronos → krons/core}/specs/catalog/__init__.py +2 -2
  53. {kronos → krons/core}/specs/catalog/_audit.py +3 -3
  54. {kronos → krons/core}/specs/catalog/_common.py +2 -2
  55. {kronos → krons/core}/specs/catalog/_content.py +5 -5
  56. {kronos → krons/core}/specs/catalog/_enforcement.py +4 -4
  57. {kronos → krons/core}/specs/factory.py +7 -7
  58. {kronos → krons/core}/specs/operable.py +9 -3
  59. {kronos → krons/core}/specs/protocol.py +4 -2
  60. {kronos → krons/core}/specs/spec.py +25 -13
  61. {kronos → krons/core}/types/base.py +7 -5
  62. {kronos → krons/core}/types/db_types.py +2 -2
  63. {kronos → krons/core}/types/identity.py +1 -1
  64. {kronos → krons}/errors.py +13 -13
  65. {kronos → krons}/protocols.py +9 -4
  66. krons/resource/__init__.py +89 -0
  67. {kronos/services → krons/resource}/backend.py +50 -24
  68. {kronos/services → krons/resource}/endpoint.py +28 -14
  69. {kronos/services → krons/resource}/hook.py +22 -9
  70. {kronos/services → krons/resource}/imodel.py +50 -32
  71. {kronos/services → krons/resource}/registry.py +27 -25
  72. {kronos/services → krons/resource}/utilities/rate_limited_executor.py +10 -6
  73. {kronos/services → krons/resource}/utilities/rate_limiter.py +4 -2
  74. {kronos/services → krons/resource}/utilities/resilience.py +17 -7
  75. krons/resource/utilities/token_calculator.py +185 -0
  76. {kronos → krons}/session/__init__.py +12 -17
  77. krons/session/constraints.py +70 -0
  78. {kronos → krons}/session/exchange.py +14 -6
  79. {kronos → krons}/session/message.py +4 -2
  80. krons/session/registry.py +35 -0
  81. {kronos → krons}/session/session.py +165 -174
  82. krons/utils/__init__.py +85 -0
  83. krons/utils/_function_arg_parser.py +99 -0
  84. krons/utils/_pythonic_function_call.py +249 -0
  85. {kronos → krons}/utils/_to_list.py +9 -3
  86. {kronos → krons}/utils/_utils.py +9 -5
  87. {kronos → krons}/utils/concurrency/__init__.py +38 -38
  88. {kronos → krons}/utils/concurrency/_async_call.py +6 -4
  89. {kronos → krons}/utils/concurrency/_errors.py +3 -1
  90. {kronos → krons}/utils/concurrency/_patterns.py +3 -1
  91. {kronos → krons}/utils/concurrency/_resource_tracker.py +6 -2
  92. krons/utils/display.py +257 -0
  93. {kronos → krons}/utils/fuzzy/__init__.py +6 -1
  94. {kronos → krons}/utils/fuzzy/_fuzzy_match.py +14 -8
  95. {kronos → krons}/utils/fuzzy/_string_similarity.py +3 -1
  96. {kronos → krons}/utils/fuzzy/_to_dict.py +3 -1
  97. krons/utils/schemas/__init__.py +26 -0
  98. krons/utils/schemas/_breakdown_pydantic_annotation.py +131 -0
  99. krons/utils/schemas/_formatter.py +72 -0
  100. krons/utils/schemas/_minimal_yaml.py +151 -0
  101. krons/utils/schemas/_typescript.py +153 -0
  102. {kronos → krons}/utils/sql/_sql_validation.py +1 -1
  103. krons/utils/validators/__init__.py +3 -0
  104. krons/utils/validators/_validate_image_url.py +56 -0
  105. krons/work/__init__.py +126 -0
  106. krons/work/engine.py +333 -0
  107. krons/work/form.py +305 -0
  108. {kronos → krons/work}/operations/__init__.py +7 -4
  109. {kronos → krons/work}/operations/builder.py +4 -4
  110. {kronos/enforcement → krons/work/operations}/context.py +37 -6
  111. {kronos → krons/work}/operations/flow.py +17 -9
  112. krons/work/operations/node.py +103 -0
  113. krons/work/operations/registry.py +103 -0
  114. {kronos/specs → krons/work}/phrase.py +131 -14
  115. {kronos/enforcement → krons/work}/policy.py +3 -3
  116. krons/work/report.py +268 -0
  117. krons/work/rules/__init__.py +47 -0
  118. {kronos/enforcement → krons/work/rules}/common/boolean.py +3 -1
  119. {kronos/enforcement → krons/work/rules}/common/choice.py +9 -3
  120. {kronos/enforcement → krons/work/rules}/common/number.py +3 -1
  121. {kronos/enforcement → krons/work/rules}/common/string.py +9 -3
  122. {kronos/enforcement → krons/work/rules}/rule.py +2 -2
  123. {kronos/enforcement → krons/work/rules}/validator.py +21 -6
  124. {kronos/enforcement → krons/work}/service.py +16 -7
  125. krons/work/worker.py +266 -0
  126. {krons-0.1.0.dist-info → krons-0.2.0.dist-info}/METADATA +19 -5
  127. krons-0.2.0.dist-info/RECORD +154 -0
  128. kronos/core/__init__.py +0 -145
  129. kronos/enforcement/__init__.py +0 -57
  130. kronos/operations/node.py +0 -101
  131. kronos/operations/registry.py +0 -92
  132. kronos/services/__init__.py +0 -81
  133. kronos/specs/adapters/__init__.py +0 -0
  134. kronos/utils/__init__.py +0 -40
  135. krons-0.1.0.dist-info/RECORD +0 -101
  136. {kronos → krons/core/specs/adapters}/__init__.py +0 -0
  137. {kronos → krons/core}/specs/adapters/_utils.py +0 -0
  138. {kronos → krons/core}/specs/adapters/factory.py +0 -0
  139. {kronos → krons/core}/types/__init__.py +0 -0
  140. {kronos → krons/core}/types/_sentinel.py +0 -0
  141. {kronos → krons}/py.typed +0 -0
  142. {kronos/services → krons/resource}/utilities/__init__.py +0 -0
  143. {kronos/services → krons/resource}/utilities/header_factory.py +0 -0
  144. {kronos → krons}/utils/_hash.py +0 -0
  145. {kronos → krons}/utils/_json_dump.py +0 -0
  146. {kronos → krons}/utils/_lazy_init.py +0 -0
  147. {kronos → krons}/utils/_to_num.py +0 -0
  148. {kronos → krons}/utils/concurrency/_cancel.py +0 -0
  149. {kronos → krons}/utils/concurrency/_primitives.py +0 -0
  150. {kronos → krons}/utils/concurrency/_priority_queue.py +0 -0
  151. {kronos → krons}/utils/concurrency/_run_async.py +0 -0
  152. {kronos → krons}/utils/concurrency/_task.py +0 -0
  153. {kronos → krons}/utils/concurrency/_utils.py +0 -0
  154. {kronos → krons}/utils/fuzzy/_extract_json.py +0 -0
  155. {kronos → krons}/utils/fuzzy/_fuzzy_json.py +0 -0
  156. {kronos → krons}/utils/sql/__init__.py +0 -0
  157. {kronos/enforcement → krons/work/rules}/common/__init__.py +0 -0
  158. {kronos/enforcement → krons/work/rules}/common/mapping.py +0 -0
  159. {kronos/enforcement → krons/work/rules}/common/model.py +0 -0
  160. {kronos/enforcement → krons/work/rules}/registry.py +0 -0
  161. {krons-0.1.0.dist-info → krons-0.2.0.dist-info}/WHEEL +0 -0
  162. {krons-0.1.0.dist-info → krons-0.2.0.dist-info}/licenses/LICENSE +0 -0
krons/work/report.py ADDED
@@ -0,0 +1,268 @@
1
+ # Copyright (c) 2025 - 2026, HaiyangLi <quantocean.li at gmail dot com>
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Report - Multi-step workflow orchestration.
5
+
6
+ A Report orchestrates multiple Forms based on data availability:
7
+ - Schedules forms when their inputs become available
8
+ - Groups forms by branch for sequential execution within branch
9
+ - Propagates outputs between forms
10
+ - Tracks overall workflow completion
11
+
12
+ This is the scheduling layer that enables data-driven DAG execution.
13
+
14
+ The Report pattern supports declarative workflow definition:
15
+ - Fields as class attributes (typed outputs)
16
+ - form_assignments DSL with branch/resource hints
17
+ - Implicit dependencies from data flow
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from collections import defaultdict
23
+ from typing import Any
24
+
25
+ from pydantic import Field
26
+
27
+ from krons.core import Element, Pile
28
+
29
+ from .form import Form, parse_assignment
30
+
31
+ __all__ = ("Report",)
32
+
33
+
34
+ class Report(Element):
35
+ """Workflow orchestrator - schedules forms based on field availability.
36
+
37
+ A Report manages a collection of Forms, executing them as their
38
+ inputs become available. Forms are grouped by branch - forms on the
39
+ same branch execute sequentially, different branches can run in parallel.
40
+
41
+ Example (simple):
42
+ report = Report(
43
+ assignment="context -> final_score",
44
+ form_assignments=[
45
+ "context -> analysis",
46
+ "analysis -> score",
47
+ "score -> final_score",
48
+ ],
49
+ )
50
+ report.initialize(context="some input")
51
+
52
+ while not report.is_complete():
53
+ for form in report.next_forms():
54
+ await form.execute(ctx)
55
+ report.complete_form(form)
56
+
57
+ Example (with branches and resources):
58
+ class HiringBriefReport(Report):
59
+ role_classification: RoleClassification | None = None
60
+ strategic_context: StrategicContext | None = None
61
+
62
+ assignment: str = "job_input -> executive_summary"
63
+
64
+ form_assignments: list[str] = [
65
+ "classifier: job_input -> role_classification | api:fast",
66
+ "strategist: job_input, role_classification -> strategic_context | api:synthesis",
67
+ "writer: strategic_context -> executive_summary | api:reasoning",
68
+ ]
69
+
70
+ Attributes:
71
+ assignment: Overall workflow 'inputs -> final_outputs'
72
+ form_assignments: List of form assignments with optional branch/resource
73
+ input_fields: Workflow input fields
74
+ output_fields: Workflow output fields
75
+ forms: All forms in workflow
76
+ completed_forms: Forms that have finished
77
+ available_data: Current state of all field values
78
+ """
79
+
80
+ assignment: str = Field(
81
+ default="",
82
+ description="Overall workflow: 'inputs -> final_outputs'",
83
+ )
84
+ form_assignments: list[str] = Field(
85
+ default_factory=list,
86
+ description="List of form assignments: ['branch: a -> b | resource', ...]",
87
+ )
88
+
89
+ input_fields: list[str] = Field(default_factory=list)
90
+ output_fields: list[str] = Field(default_factory=list)
91
+
92
+ forms: Pile[Form] = Field(
93
+ default_factory=lambda: Pile(item_type=Form),
94
+ description="All forms in the workflow",
95
+ )
96
+ completed_forms: Pile[Form] = Field(
97
+ default_factory=lambda: Pile(item_type=Form),
98
+ description="Completed forms",
99
+ )
100
+ available_data: dict[str, Any] = Field(
101
+ default_factory=dict,
102
+ description="Current state of all field values",
103
+ )
104
+
105
+ # Branch tracking: branch_name -> list of form IDs in order
106
+ _branch_forms: dict[str, list[Form]] = {}
107
+ # Track last completed form per branch for sequential execution
108
+ _branch_progress: dict[str, int] = {}
109
+
110
+ def model_post_init(self, _: Any) -> None:
111
+ """Parse assignment and create forms."""
112
+ self._branch_forms = defaultdict(list)
113
+ self._branch_progress = defaultdict(int)
114
+
115
+ if not self.assignment:
116
+ return
117
+
118
+ # Parse overall assignment
119
+ self.input_fields, self.output_fields = parse_assignment(self.assignment)
120
+
121
+ # Create forms from form_assignments
122
+ for fa in self.form_assignments:
123
+ form = Form(assignment=fa)
124
+ self.forms.include(form)
125
+
126
+ # Track by branch
127
+ branch = form.branch or "_default"
128
+ self._branch_forms[branch].append(form)
129
+
130
+ def initialize(self, **inputs: Any) -> None:
131
+ """Provide initial input data.
132
+
133
+ Args:
134
+ **inputs: Initial field values
135
+
136
+ Raises:
137
+ ValueError: If required input is missing
138
+ """
139
+ for field in self.input_fields:
140
+ if field not in inputs:
141
+ raise ValueError(f"Missing required input: '{field}'")
142
+ self.available_data[field] = inputs[field]
143
+
144
+ def next_forms(self) -> list[Form]:
145
+ """Get forms that are ready to execute.
146
+
147
+ Forms with explicit branches execute sequentially within their branch.
148
+ Forms without branches (None) execute in parallel based on data availability.
149
+
150
+ Returns:
151
+ List of forms with all inputs available and not yet filled
152
+ """
153
+ ready = []
154
+
155
+ for branch, forms in self._branch_forms.items():
156
+ if branch == "_default":
157
+ # No explicit branch - parallel execution based on data
158
+ for form in forms:
159
+ if form.filled:
160
+ continue
161
+ form.available_data = self.available_data.copy()
162
+ if form.is_workable():
163
+ ready.append(form)
164
+ else:
165
+ # Explicit branch - sequential execution
166
+ progress = self._branch_progress[branch]
167
+
168
+ # Only consider the next form in this branch
169
+ if progress < len(forms):
170
+ form = forms[progress]
171
+ if form.filled:
172
+ # Already done, advance progress
173
+ self._branch_progress[branch] += 1
174
+ continue
175
+
176
+ form.available_data = self.available_data.copy()
177
+ if form.is_workable():
178
+ ready.append(form)
179
+
180
+ return ready
181
+
182
+ def complete_form(self, form: Form) -> None:
183
+ """Mark a form as completed and update available data.
184
+
185
+ Args:
186
+ form: The completed form
187
+
188
+ Raises:
189
+ ValueError: If form is not filled
190
+ """
191
+ if not form.filled:
192
+ raise ValueError("Form is not filled")
193
+
194
+ self.completed_forms.include(form)
195
+
196
+ # Advance branch progress
197
+ branch = form.branch or "_default"
198
+ if branch in self._branch_forms:
199
+ forms = self._branch_forms[branch]
200
+ progress = self._branch_progress[branch]
201
+ if progress < len(forms) and forms[progress].id == form.id:
202
+ self._branch_progress[branch] += 1
203
+
204
+ # Update available data with form outputs
205
+ output_data = form.get_output_data()
206
+ self.available_data.update(output_data)
207
+
208
+ def is_complete(self) -> bool:
209
+ """Check if all output fields are available.
210
+
211
+ Returns:
212
+ True if workflow is complete
213
+ """
214
+ return all(field in self.available_data for field in self.output_fields)
215
+
216
+ def get_deliverable(self) -> dict[str, Any]:
217
+ """Get final output values.
218
+
219
+ Returns:
220
+ Dict of output field values
221
+ """
222
+ return {f: self.available_data.get(f) for f in self.output_fields}
223
+
224
+ @property
225
+ def progress(self) -> tuple[int, int]:
226
+ """Get progress as (completed, total).
227
+
228
+ Returns:
229
+ Tuple of (completed forms, total forms)
230
+ """
231
+ return len(self.completed_forms), len(self.forms)
232
+
233
+ def get_forms_by_branch(self, branch: str) -> list[Form]:
234
+ """Get all forms for a specific branch.
235
+
236
+ Args:
237
+ branch: Branch name
238
+
239
+ Returns:
240
+ List of forms on that branch (in order)
241
+ """
242
+ return list(self._branch_forms.get(branch, []))
243
+
244
+ def get_forms_by_resource(self, resource: str) -> list[Form]:
245
+ """Get all forms requiring a specific resource.
246
+
247
+ Args:
248
+ resource: Resource hint (e.g., 'api:fast')
249
+
250
+ Returns:
251
+ List of forms with that resource hint
252
+ """
253
+ return [f for f in self.forms if f.resource == resource]
254
+
255
+ @property
256
+ def branches(self) -> list[str]:
257
+ """Get all branch names in this report."""
258
+ return list(self._branch_forms.keys())
259
+
260
+ @property
261
+ def resources(self) -> set[str]:
262
+ """Get all resource hints used in this report."""
263
+ return {f.resource for f in self.forms if f.resource}
264
+
265
+ def __repr__(self) -> str:
266
+ completed, total = self.progress
267
+ branches = len(self._branch_forms)
268
+ return f"Report('{self.assignment}', {completed}/{total} forms, {branches} branches)"
@@ -0,0 +1,47 @@
1
+ # Copyright (c) 2025 - 2026, HaiyangLi <quantocean.li at gmail dot com>
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Rules module: validation rules with auto-correction support.
5
+
6
+ Core exports:
7
+ - Rule, RuleParams, RuleQualifier: Base rule classes
8
+ - ValidationError: Validation failure exception
9
+ - Validator: Spec-aware validation orchestrator
10
+ - RuleRegistry: Type-to-rule mapping with inheritance
11
+ - Common rules: StringRule, NumberRule, BooleanRule, ChoiceRule, MappingRule, BaseModelRule
12
+ """
13
+
14
+ from krons.errors import ValidationError
15
+
16
+ from .common import (
17
+ BaseModelRule,
18
+ BooleanRule,
19
+ ChoiceRule,
20
+ MappingRule,
21
+ NumberRule,
22
+ StringRule,
23
+ )
24
+ from .registry import RuleRegistry, get_default_registry, reset_default_registry
25
+ from .rule import Rule, RuleParams, RuleQualifier
26
+ from .validator import Validator
27
+
28
+ __all__ = (
29
+ # Base classes
30
+ "Rule",
31
+ "RuleParams",
32
+ "RuleQualifier",
33
+ "ValidationError",
34
+ # Validator
35
+ "Validator",
36
+ # Registry
37
+ "RuleRegistry",
38
+ "get_default_registry",
39
+ "reset_default_registry",
40
+ # Common rules
41
+ "BaseModelRule",
42
+ "BooleanRule",
43
+ "ChoiceRule",
44
+ "MappingRule",
45
+ "NumberRule",
46
+ "StringRule",
47
+ )
@@ -50,7 +50,9 @@ class BooleanRule(Rule):
50
50
  ValueError: If not a boolean
51
51
  """
52
52
  if not isinstance(v, bool):
53
- raise ValueError(f"Invalid boolean value: expected bool, got {type(v).__name__}")
53
+ raise ValueError(
54
+ f"Invalid boolean value: expected bool, got {type(v).__name__}"
55
+ )
54
56
 
55
57
  async def perform_fix(self, v: Any, _t: type) -> Any:
56
58
  """Attempt to convert value to boolean.
@@ -58,7 +58,9 @@ class ChoiceRule(Rule):
58
58
  self.case_sensitive = case_sensitive
59
59
 
60
60
  if not case_sensitive:
61
- self._lower_map = {str(c).lower(): c for c in self.choices if isinstance(c, str)}
61
+ self._lower_map = {
62
+ str(c).lower(): c for c in self.choices if isinstance(c, str)
63
+ }
62
64
 
63
65
  async def validate(self, v: Any, t: type, **kw) -> None:
64
66
  """Validate that value is in allowed choices (exact match only).
@@ -72,7 +74,9 @@ class ChoiceRule(Rule):
72
74
  if v in self.choices:
73
75
  return
74
76
 
75
- raise ValueError(f"Invalid choice: {v} not in {sorted(str(c) for c in self.choices)}")
77
+ raise ValueError(
78
+ f"Invalid choice: {v} not in {sorted(str(c) for c in self.choices)}"
79
+ )
76
80
 
77
81
  async def perform_fix(self, v: Any, _t: type) -> Any:
78
82
  """Attempt to fix value to closest choice.
@@ -94,4 +98,6 @@ class ChoiceRule(Rule):
94
98
  if v_lower in self._lower_map:
95
99
  return self._lower_map[v_lower]
96
100
 
97
- raise ValueError(f"Cannot fix choice: {v} not in {sorted(str(c) for c in self.choices)}")
101
+ raise ValueError(
102
+ f"Cannot fix choice: {v} not in {sorted(str(c) for c in self.choices)}"
103
+ )
@@ -66,7 +66,9 @@ class NumberRule(Rule):
66
66
  ValueError: If not a number or constraints violated
67
67
  """
68
68
  if not isinstance(v, (int, float)):
69
- raise ValueError(f"Invalid number value: expected int or float, got {type(v).__name__}")
69
+ raise ValueError(
70
+ f"Invalid number value: expected int or float, got {type(v).__name__}"
71
+ )
70
72
 
71
73
  if self.ge is not None and v < self.ge:
72
74
  raise ValueError(f"Number too small: {v} < {self.ge}")
@@ -103,14 +103,18 @@ class StringRule(Rule):
103
103
  ValueError: If not a string or constraints violated
104
104
  """
105
105
  if not isinstance(v, str):
106
- raise ValueError(f"Invalid string value: expected str, got {type(v).__name__}")
106
+ raise ValueError(
107
+ f"Invalid string value: expected str, got {type(v).__name__}"
108
+ )
107
109
 
108
110
  if self.min_length is not None and len(v) < self.min_length:
109
111
  raise ValueError(
110
112
  f"String too short: got {len(v)} characters, minimum {self.min_length}"
111
113
  )
112
114
  if self.max_length is not None and len(v) > self.max_length:
113
- raise ValueError(f"String too long: got {len(v)} characters, maximum {self.max_length}")
115
+ raise ValueError(
116
+ f"String too long: got {len(v)} characters, maximum {self.max_length}"
117
+ )
114
118
 
115
119
  if self._compiled_pattern is not None:
116
120
  if len(v) > self.regex_max_input_length:
@@ -119,7 +123,9 @@ class StringRule(Rule):
119
123
  f"maximum {self.regex_max_input_length}"
120
124
  )
121
125
  if not self._compiled_pattern.match(v):
122
- raise ValueError(f"String does not match required pattern: {self.pattern}")
126
+ raise ValueError(
127
+ f"String does not match required pattern: {self.pattern}"
128
+ )
123
129
 
124
130
  async def perform_fix(self, v: Any, t: type) -> Any:
125
131
  """Attempt to convert value to string and re-validate.
@@ -8,8 +8,8 @@ from dataclasses import dataclass, field
8
8
  from enum import IntEnum, auto
9
9
  from typing import Any
10
10
 
11
- from kronos.errors import ValidationError
12
- from kronos.types import Params
11
+ from krons.core.types import Params
12
+ from krons.errors import ValidationError
13
13
 
14
14
  __all__ = ("Rule", "RuleParams", "RuleQualifier", "ValidationError")
15
15
 
@@ -7,14 +7,16 @@ from collections import deque
7
7
  from datetime import datetime
8
8
  from typing import TYPE_CHECKING, Any, ClassVar
9
9
 
10
- from kronos.types import is_sentinel
11
- from kronos.utils.concurrency import is_coro_func
10
+ from krons.core.types import is_sentinel, not_sentinel
11
+ from krons.utils.concurrency import is_coro_func
12
12
 
13
13
  from .registry import RuleRegistry, get_default_registry
14
14
  from .rule import Rule, ValidationError
15
15
 
16
16
  if TYPE_CHECKING:
17
- from kronos.specs import Operable, Spec
17
+ from pydantic import BaseModel
18
+
19
+ from krons.core.specs import Operable, Spec
18
20
 
19
21
  __all__ = ("Validator",)
20
22
 
@@ -29,7 +31,9 @@ class Validator:
29
31
  ):
30
32
  self.registry = registry or get_default_registry()
31
33
  max_entries = (
32
- max_log_entries if max_log_entries is not None else self.DEFAULT_MAX_LOG_ENTRIES
34
+ max_log_entries
35
+ if max_log_entries is not None
36
+ else self.DEFAULT_MAX_LOG_ENTRIES
33
37
  )
34
38
  self.validation_log: deque[dict[str, Any]] = deque(
35
39
  maxlen=max_entries if max_entries > 0 else None
@@ -141,7 +145,9 @@ class Validator:
141
145
  raise ValidationError(error_msg)
142
146
  else:
143
147
  try:
144
- value = await rule.invoke(field_name, value, spec.base_type, auto_fix=auto_fix)
148
+ value = await rule.invoke(
149
+ field_name, value, spec.base_type, auto_fix=auto_fix
150
+ )
145
151
  except Exception as e:
146
152
  self.log_validation_error(field_name, value, str(e))
147
153
  raise
@@ -171,14 +177,20 @@ class Validator:
171
177
 
172
178
  return value
173
179
 
174
- async def validate_operable(
180
+ async def validate(
175
181
  self,
176
182
  data: dict[str, Any],
177
183
  operable: Operable,
178
184
  capabilities: set[str] | None = None,
179
185
  auto_fix: bool = True,
180
186
  strict: bool = True,
187
+ structure: type[BaseModel] | None = None,
181
188
  ) -> dict[str, Any]:
189
+ if not_sentinel(capabilities, {"none"}) and not capabilities.issubset(
190
+ operable.allowed()
191
+ ):
192
+ raise ValidationError("Capabilities exceed operable's allowed set")
193
+
182
194
  capabilities = capabilities or operable.allowed()
183
195
  validated: dict[str, Any] = {}
184
196
 
@@ -195,4 +207,7 @@ class Validator:
195
207
  spec, value, auto_fix=auto_fix, strict=strict
196
208
  )
197
209
 
210
+ if structure is not None:
211
+ validated = operable.validate_instance(structure, validated)
212
+
198
213
  return validated
@@ -12,9 +12,9 @@ from typing import Any
12
12
 
13
13
  from pydantic import Field, PrivateAttr
14
14
 
15
- from kronos.services import ServiceBackend, ServiceConfig
15
+ from krons.resource import ResourceBackend, ResourceConfig
16
+ from krons.work.operations.context import RequestContext
16
17
 
17
- from .context import RequestContext
18
18
  from .policy import EnforcementLevel, PolicyEngine, PolicyResolver
19
19
 
20
20
  logger = logging.getLogger(__name__)
@@ -28,7 +28,7 @@ __all__ = (
28
28
  )
29
29
 
30
30
 
31
- class KronConfig(ServiceConfig):
31
+ class KronConfig(ResourceConfig):
32
32
  """Configuration for KronService.
33
33
 
34
34
  Attributes:
@@ -135,7 +135,7 @@ def get_action_meta(handler: Callable) -> ActionMeta | None:
135
135
  # =============================================================================
136
136
 
137
137
 
138
- class KronService(ServiceBackend):
138
+ class KronService(ResourceBackend):
139
139
  """Service backend with typed actions.
140
140
 
141
141
  Subclasses implement action handlers with @action decorator.
@@ -168,7 +168,9 @@ class KronService(ServiceBackend):
168
168
  config: KronConfig = Field(default_factory=KronConfig)
169
169
  _policy_engine: PolicyEngine | None = PrivateAttr(default=None)
170
170
  _policy_resolver: PolicyResolver | None = PrivateAttr(default=None)
171
- _action_registry: dict[str, tuple[Callable, ActionMeta]] = PrivateAttr(default_factory=dict)
171
+ _action_registry: dict[str, tuple[Callable, ActionMeta]] = PrivateAttr(
172
+ default_factory=dict
173
+ )
172
174
 
173
175
  def __init__(
174
176
  self,
@@ -193,6 +195,9 @@ class KronService(ServiceBackend):
193
195
  def _register_actions(self) -> None:
194
196
  """Scan for @action decorated methods and register them."""
195
197
  for name in dir(self):
198
+ # Skip dunder attributes to avoid Pydantic deprecation warnings
199
+ if name.startswith("__"):
200
+ continue
196
201
  if name.startswith("_"):
197
202
  method = getattr(self, name, None)
198
203
  if method and callable(method):
@@ -287,7 +292,9 @@ class KronService(ServiceBackend):
287
292
 
288
293
  # Validate options if we have typed options_type
289
294
  if meta._options_type and self.config.operable:
290
- options = self.config.operable.validate_instance(meta._options_type, options)
295
+ options = self.config.operable.validate_instance(
296
+ meta._options_type, options
297
+ )
291
298
 
292
299
  # Execute handler
293
300
  result = await handler(options, ctx)
@@ -349,7 +356,9 @@ class KronService(ServiceBackend):
349
356
 
350
357
  for result in results:
351
358
  if EnforcementLevel.is_blocking(result):
352
- raise PermissionError(f"Policy {result.policy_id} blocked: {result.message}")
359
+ raise PermissionError(
360
+ f"Policy {result.policy_id} blocked: {result.message}"
361
+ )
353
362
 
354
363
  except PermissionError:
355
364
  raise