krons 0.1.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 (101) hide show
  1. kronos/__init__.py +0 -0
  2. kronos/core/__init__.py +145 -0
  3. kronos/core/broadcaster.py +116 -0
  4. kronos/core/element.py +225 -0
  5. kronos/core/event.py +316 -0
  6. kronos/core/eventbus.py +116 -0
  7. kronos/core/flow.py +356 -0
  8. kronos/core/graph.py +442 -0
  9. kronos/core/node.py +982 -0
  10. kronos/core/pile.py +575 -0
  11. kronos/core/processor.py +494 -0
  12. kronos/core/progression.py +296 -0
  13. kronos/enforcement/__init__.py +57 -0
  14. kronos/enforcement/common/__init__.py +34 -0
  15. kronos/enforcement/common/boolean.py +85 -0
  16. kronos/enforcement/common/choice.py +97 -0
  17. kronos/enforcement/common/mapping.py +118 -0
  18. kronos/enforcement/common/model.py +102 -0
  19. kronos/enforcement/common/number.py +98 -0
  20. kronos/enforcement/common/string.py +140 -0
  21. kronos/enforcement/context.py +129 -0
  22. kronos/enforcement/policy.py +80 -0
  23. kronos/enforcement/registry.py +153 -0
  24. kronos/enforcement/rule.py +312 -0
  25. kronos/enforcement/service.py +370 -0
  26. kronos/enforcement/validator.py +198 -0
  27. kronos/errors.py +146 -0
  28. kronos/operations/__init__.py +32 -0
  29. kronos/operations/builder.py +228 -0
  30. kronos/operations/flow.py +398 -0
  31. kronos/operations/node.py +101 -0
  32. kronos/operations/registry.py +92 -0
  33. kronos/protocols.py +414 -0
  34. kronos/py.typed +0 -0
  35. kronos/services/__init__.py +81 -0
  36. kronos/services/backend.py +286 -0
  37. kronos/services/endpoint.py +608 -0
  38. kronos/services/hook.py +471 -0
  39. kronos/services/imodel.py +465 -0
  40. kronos/services/registry.py +115 -0
  41. kronos/services/utilities/__init__.py +36 -0
  42. kronos/services/utilities/header_factory.py +87 -0
  43. kronos/services/utilities/rate_limited_executor.py +271 -0
  44. kronos/services/utilities/rate_limiter.py +180 -0
  45. kronos/services/utilities/resilience.py +414 -0
  46. kronos/session/__init__.py +41 -0
  47. kronos/session/exchange.py +258 -0
  48. kronos/session/message.py +60 -0
  49. kronos/session/session.py +411 -0
  50. kronos/specs/__init__.py +25 -0
  51. kronos/specs/adapters/__init__.py +0 -0
  52. kronos/specs/adapters/_utils.py +45 -0
  53. kronos/specs/adapters/dataclass_field.py +246 -0
  54. kronos/specs/adapters/factory.py +56 -0
  55. kronos/specs/adapters/pydantic_adapter.py +309 -0
  56. kronos/specs/adapters/sql_ddl.py +946 -0
  57. kronos/specs/catalog/__init__.py +36 -0
  58. kronos/specs/catalog/_audit.py +39 -0
  59. kronos/specs/catalog/_common.py +43 -0
  60. kronos/specs/catalog/_content.py +59 -0
  61. kronos/specs/catalog/_enforcement.py +70 -0
  62. kronos/specs/factory.py +120 -0
  63. kronos/specs/operable.py +314 -0
  64. kronos/specs/phrase.py +405 -0
  65. kronos/specs/protocol.py +140 -0
  66. kronos/specs/spec.py +506 -0
  67. kronos/types/__init__.py +60 -0
  68. kronos/types/_sentinel.py +311 -0
  69. kronos/types/base.py +369 -0
  70. kronos/types/db_types.py +260 -0
  71. kronos/types/identity.py +66 -0
  72. kronos/utils/__init__.py +40 -0
  73. kronos/utils/_hash.py +234 -0
  74. kronos/utils/_json_dump.py +392 -0
  75. kronos/utils/_lazy_init.py +63 -0
  76. kronos/utils/_to_list.py +165 -0
  77. kronos/utils/_to_num.py +85 -0
  78. kronos/utils/_utils.py +375 -0
  79. kronos/utils/concurrency/__init__.py +205 -0
  80. kronos/utils/concurrency/_async_call.py +333 -0
  81. kronos/utils/concurrency/_cancel.py +122 -0
  82. kronos/utils/concurrency/_errors.py +96 -0
  83. kronos/utils/concurrency/_patterns.py +363 -0
  84. kronos/utils/concurrency/_primitives.py +328 -0
  85. kronos/utils/concurrency/_priority_queue.py +135 -0
  86. kronos/utils/concurrency/_resource_tracker.py +110 -0
  87. kronos/utils/concurrency/_run_async.py +67 -0
  88. kronos/utils/concurrency/_task.py +95 -0
  89. kronos/utils/concurrency/_utils.py +79 -0
  90. kronos/utils/fuzzy/__init__.py +14 -0
  91. kronos/utils/fuzzy/_extract_json.py +90 -0
  92. kronos/utils/fuzzy/_fuzzy_json.py +288 -0
  93. kronos/utils/fuzzy/_fuzzy_match.py +149 -0
  94. kronos/utils/fuzzy/_string_similarity.py +187 -0
  95. kronos/utils/fuzzy/_to_dict.py +396 -0
  96. kronos/utils/sql/__init__.py +13 -0
  97. kronos/utils/sql/_sql_validation.py +142 -0
  98. krons-0.1.0.dist-info/METADATA +70 -0
  99. krons-0.1.0.dist-info/RECORD +101 -0
  100. krons-0.1.0.dist-info/WHEEL +4 -0
  101. krons-0.1.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,286 @@
1
+ # Copyright (c) 2025 - 2026, HaiyangLi <quantocean.li at gmail dot com>
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from __future__ import annotations
5
+
6
+ import logging
7
+ from abc import abstractmethod
8
+ from typing import Any
9
+
10
+ from pydantic import BaseModel, Field, PrivateAttr, field_validator, model_validator
11
+
12
+ from kronos.core import Element, Event, EventStatus
13
+ from kronos.types import HashableModel, Unset, UnsetType, is_sentinel
14
+
15
+ from .hook import HookBroadcaster, HookEvent, HookPhase, HookRegistry
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ # Module-level cache for schema field keys (keyed by class)
21
+ _SCHEMA_FIELD_KEYS_CACHE: dict[type[BaseModel], set[str]] = {}
22
+
23
+
24
+ def _get_schema_field_keys(cls: type[BaseModel]) -> set[str]:
25
+ """Get field names for a Pydantic model (cached).
26
+
27
+ Uses model_fields instead of model_json_schema to include fields
28
+ that may be excluded from JSON schema (e.g., SkipJsonSchema fields).
29
+ """
30
+ if cls not in _SCHEMA_FIELD_KEYS_CACHE:
31
+ _SCHEMA_FIELD_KEYS_CACHE[cls] = set(cls.model_fields.keys())
32
+ return _SCHEMA_FIELD_KEYS_CACHE[cls]
33
+
34
+
35
+ class ServiceConfig(HashableModel):
36
+ provider: str = Field(..., min_length=4, max_length=50)
37
+ name: str = Field(..., min_length=4, max_length=100)
38
+ request_options: type[BaseModel] | None = Field(default=None, exclude=True)
39
+ timeout: int = Field(default=300, ge=1, le=3600)
40
+ max_retries: int = Field(default=3, ge=0, le=10)
41
+ version: str | None = None
42
+ tags: list[str] = Field(default_factory=list)
43
+ kwargs: dict = Field(default_factory=dict)
44
+
45
+ @model_validator(mode="before")
46
+ @classmethod
47
+ def _validate_kwargs(cls, data: dict[str, Any]) -> dict[str, Any]:
48
+ kwargs = data.pop("kwargs", {})
49
+ field_keys = _get_schema_field_keys(cls)
50
+ for k in list(data.keys()):
51
+ if k not in field_keys:
52
+ kwargs[k] = data.pop(k)
53
+ data["kwargs"] = kwargs
54
+ return data
55
+
56
+ @field_validator("request_options", mode="before")
57
+ def _validate_request_options(cls, v): # noqa: N805
58
+ if v is None:
59
+ return None
60
+ if isinstance(v, type) and issubclass(v, BaseModel):
61
+ return v
62
+ if isinstance(v, BaseModel):
63
+ return v.__class__
64
+ raise ValueError("request_options must be a Pydantic model type")
65
+
66
+ def validate_payload(self, data: dict[str, Any]) -> dict[str, Any]:
67
+ if not self.request_options:
68
+ return data
69
+ try:
70
+ self.request_options.model_validate(data)
71
+ return data
72
+ except Exception as e:
73
+ raise ValueError("Invalid payload") from e
74
+
75
+
76
+ class NormalizedResponse(HashableModel):
77
+ """Generic normalized response for all service backends.
78
+
79
+ Works for any backend type: HTTP endpoints, tools, LLM APIs, etc.
80
+ Provides consistent interface regardless of underlying service.
81
+ """
82
+
83
+ status: str = Field(..., description="Response status: 'success' or 'error'")
84
+ 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")
88
+
89
+ def _to_dict(self, **kwargs: Any) -> dict[str, Any]:
90
+ """Convert to dict, excluding None values."""
91
+ return self.model_dump(exclude_none=True, **kwargs)
92
+
93
+
94
+ class Calling(Event):
95
+ """Base calling event with hook support.
96
+
97
+ Extends kron.Event with pre/post invocation hooks.
98
+ Always delegates to backend.call() for actual service invocation.
99
+
100
+ Attributes:
101
+ backend: ServiceBackend instance (Tool, Endpoint, etc.)
102
+ payload: Request payload/arguments for backend call
103
+ """
104
+
105
+ backend: ServiceBackend = Field(..., exclude=True, description="Service backend instance")
106
+ payload: dict[str, Any] = Field(..., description="Request payload/arguments")
107
+ _pre_invoke_hook_event: HookEvent | None = PrivateAttr(None)
108
+ _post_invoke_hook_event: HookEvent | None = PrivateAttr(None)
109
+
110
+ @property
111
+ def response(self) -> NormalizedResponse | UnsetType:
112
+ """Get normalized response from execution."""
113
+ if is_sentinel(self.execution.response):
114
+ return Unset
115
+ resp = self.execution.response
116
+ if isinstance(resp, NormalizedResponse):
117
+ return resp
118
+ return Unset
119
+
120
+ @property
121
+ @abstractmethod
122
+ def call_args(self) -> dict:
123
+ """Get arguments for backend.call(**self.call_args).
124
+
125
+ Subclasses must implement this to return their specific call arguments:
126
+ - APICalling: {"request": ..., "extra_headers": ..., "skip_payload_creation": True}
127
+ - ToolCalling: {"arguments": ...}
128
+
129
+ Returns:
130
+ Dict of keyword arguments for backend.call()
131
+ """
132
+ ...
133
+
134
+ async def _invoke(self) -> NormalizedResponse:
135
+ """Execute with hook lifecycle (called by parent Event.invoke()).
136
+
137
+ Hook execution order:
138
+ 1. Pre-invocation hook (if configured) - can abort
139
+ 2. backend.call(**self.call_args) - actual service invocation
140
+ 3. Post-invocation hook (if configured) - runs even on failure (finally)
141
+
142
+ Returns:
143
+ NormalizedResponse from backend
144
+ """
145
+ # Pre-invocation hook
146
+ if h_ev := self._pre_invoke_hook_event:
147
+ await h_ev.invoke()
148
+
149
+ # Check hook status and propagate failures
150
+ if h_ev.execution.status in (EventStatus.FAILED, EventStatus.CANCELLED):
151
+ raise RuntimeError(
152
+ f"Pre-invoke hook {h_ev.execution.status.value}: {h_ev.execution.error}"
153
+ )
154
+
155
+ if h_ev._should_exit:
156
+ raise h_ev._exit_cause or RuntimeError(
157
+ "Pre-invocation hook requested exit without a cause"
158
+ )
159
+ await HookBroadcaster.broadcast(h_ev)
160
+
161
+ # Actual service call via backend (post-hook runs in finally)
162
+ try:
163
+ response = await self.backend.call(**self.call_args)
164
+ return response
165
+ finally:
166
+ # Post-invocation hook runs even on failure (for cleanup, metrics, logging)
167
+ if h_ev := self._post_invoke_hook_event:
168
+ await h_ev.invoke()
169
+
170
+ # Check hook status (post-hook failures don't block, just log)
171
+ if h_ev.execution.status in (EventStatus.FAILED, EventStatus.CANCELLED):
172
+ logger.warning(
173
+ f"Post-invoke hook {h_ev.execution.status.value}: {h_ev.execution.error}"
174
+ )
175
+
176
+ if h_ev._should_exit:
177
+ raise h_ev._exit_cause or RuntimeError(
178
+ "Post-invocation hook requested exit without a cause"
179
+ )
180
+ await HookBroadcaster.broadcast(h_ev)
181
+
182
+ def create_pre_invoke_hook(
183
+ self,
184
+ hook_registry: HookRegistry,
185
+ exit_hook: bool | None = None,
186
+ hook_timeout: float = 30.0,
187
+ hook_params: dict[str, Any] | None = None,
188
+ ) -> None:
189
+ """Create pre-invocation hook event."""
190
+ h_ev = HookEvent(
191
+ hook_phase=HookPhase.PreInvocation,
192
+ event_like=self,
193
+ registry=hook_registry,
194
+ exit=exit_hook if exit_hook is not None else False,
195
+ timeout=hook_timeout,
196
+ streaming=False,
197
+ params=hook_params or {},
198
+ )
199
+ self._pre_invoke_hook_event = h_ev
200
+
201
+ def create_post_invoke_hook(
202
+ self,
203
+ hook_registry: HookRegistry,
204
+ exit_hook: bool | None = None,
205
+ hook_timeout: float = 30.0,
206
+ hook_params: dict[str, Any] | None = None,
207
+ ) -> None:
208
+ """Create post-invocation hook event."""
209
+ h_ev = HookEvent(
210
+ hook_phase=HookPhase.PostInvocation,
211
+ event_like=self,
212
+ registry=hook_registry,
213
+ exit=exit_hook if exit_hook is not None else False,
214
+ timeout=hook_timeout,
215
+ streaming=False,
216
+ params=hook_params or {},
217
+ )
218
+ self._post_invoke_hook_event = h_ev
219
+
220
+
221
+ class ServiceBackend(Element):
222
+ """Base class for all service backends (Tool, Endpoint, etc.).
223
+
224
+ Inherits from kronos.Element for UUID-based identity.
225
+ Subclasses must implement event_type and call() methods.
226
+ """
227
+
228
+ config: ServiceConfig = Field(..., description="Service configuration")
229
+
230
+ @property
231
+ def provider(self) -> str:
232
+ """Provider name from config."""
233
+ return self.config.provider
234
+
235
+ @property
236
+ def name(self) -> str:
237
+ """Service name from config."""
238
+ return self.config.name
239
+
240
+ @property
241
+ def version(self) -> str | None:
242
+ """Service version from config."""
243
+ return self.config.version
244
+
245
+ @property
246
+ def tags(self) -> set[str]:
247
+ """Service tags from config."""
248
+ return set(self.config.tags) if self.config.tags else set()
249
+
250
+ @property
251
+ def request_options(self) -> type[BaseModel] | None:
252
+ """Request options schema (Pydantic model type) from config."""
253
+ return self.config.request_options
254
+
255
+ @property
256
+ @abstractmethod
257
+ def event_type(self) -> type[Calling]:
258
+ """Return Calling type for this backend (e.g., ToolCalling, APICalling)."""
259
+ ...
260
+
261
+ def normalize_response(self, raw_response: Any) -> NormalizedResponse:
262
+ """Normalize raw response into NormalizedResponse.
263
+
264
+ Default implementation wraps response as-is. Subclasses can override
265
+ to extract specific fields or add metadata.
266
+
267
+ Args:
268
+ raw_response: Raw response from service call
269
+
270
+ Returns:
271
+ NormalizedResponse with status, data, raw_response
272
+ """
273
+ return NormalizedResponse(
274
+ status="success",
275
+ data=raw_response,
276
+ raw_response=raw_response,
277
+ )
278
+
279
+ @abstractmethod
280
+ async def call(self, *args, **kw) -> NormalizedResponse:
281
+ """Execute service call and return normalized response."""
282
+ ...
283
+
284
+ async def stream(self, *args, **kw):
285
+ """Stream responses (not supported by default)."""
286
+ raise NotImplementedError("This backend does not support streaming calls.")