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.
- kronos/__init__.py +0 -0
- kronos/core/__init__.py +145 -0
- kronos/core/broadcaster.py +116 -0
- kronos/core/element.py +225 -0
- kronos/core/event.py +316 -0
- kronos/core/eventbus.py +116 -0
- kronos/core/flow.py +356 -0
- kronos/core/graph.py +442 -0
- kronos/core/node.py +982 -0
- kronos/core/pile.py +575 -0
- kronos/core/processor.py +494 -0
- kronos/core/progression.py +296 -0
- kronos/enforcement/__init__.py +57 -0
- kronos/enforcement/common/__init__.py +34 -0
- kronos/enforcement/common/boolean.py +85 -0
- kronos/enforcement/common/choice.py +97 -0
- kronos/enforcement/common/mapping.py +118 -0
- kronos/enforcement/common/model.py +102 -0
- kronos/enforcement/common/number.py +98 -0
- kronos/enforcement/common/string.py +140 -0
- kronos/enforcement/context.py +129 -0
- kronos/enforcement/policy.py +80 -0
- kronos/enforcement/registry.py +153 -0
- kronos/enforcement/rule.py +312 -0
- kronos/enforcement/service.py +370 -0
- kronos/enforcement/validator.py +198 -0
- kronos/errors.py +146 -0
- kronos/operations/__init__.py +32 -0
- kronos/operations/builder.py +228 -0
- kronos/operations/flow.py +398 -0
- kronos/operations/node.py +101 -0
- kronos/operations/registry.py +92 -0
- kronos/protocols.py +414 -0
- kronos/py.typed +0 -0
- kronos/services/__init__.py +81 -0
- kronos/services/backend.py +286 -0
- kronos/services/endpoint.py +608 -0
- kronos/services/hook.py +471 -0
- kronos/services/imodel.py +465 -0
- kronos/services/registry.py +115 -0
- kronos/services/utilities/__init__.py +36 -0
- kronos/services/utilities/header_factory.py +87 -0
- kronos/services/utilities/rate_limited_executor.py +271 -0
- kronos/services/utilities/rate_limiter.py +180 -0
- kronos/services/utilities/resilience.py +414 -0
- kronos/session/__init__.py +41 -0
- kronos/session/exchange.py +258 -0
- kronos/session/message.py +60 -0
- kronos/session/session.py +411 -0
- kronos/specs/__init__.py +25 -0
- kronos/specs/adapters/__init__.py +0 -0
- kronos/specs/adapters/_utils.py +45 -0
- kronos/specs/adapters/dataclass_field.py +246 -0
- kronos/specs/adapters/factory.py +56 -0
- kronos/specs/adapters/pydantic_adapter.py +309 -0
- kronos/specs/adapters/sql_ddl.py +946 -0
- kronos/specs/catalog/__init__.py +36 -0
- kronos/specs/catalog/_audit.py +39 -0
- kronos/specs/catalog/_common.py +43 -0
- kronos/specs/catalog/_content.py +59 -0
- kronos/specs/catalog/_enforcement.py +70 -0
- kronos/specs/factory.py +120 -0
- kronos/specs/operable.py +314 -0
- kronos/specs/phrase.py +405 -0
- kronos/specs/protocol.py +140 -0
- kronos/specs/spec.py +506 -0
- kronos/types/__init__.py +60 -0
- kronos/types/_sentinel.py +311 -0
- kronos/types/base.py +369 -0
- kronos/types/db_types.py +260 -0
- kronos/types/identity.py +66 -0
- kronos/utils/__init__.py +40 -0
- kronos/utils/_hash.py +234 -0
- kronos/utils/_json_dump.py +392 -0
- kronos/utils/_lazy_init.py +63 -0
- kronos/utils/_to_list.py +165 -0
- kronos/utils/_to_num.py +85 -0
- kronos/utils/_utils.py +375 -0
- kronos/utils/concurrency/__init__.py +205 -0
- kronos/utils/concurrency/_async_call.py +333 -0
- kronos/utils/concurrency/_cancel.py +122 -0
- kronos/utils/concurrency/_errors.py +96 -0
- kronos/utils/concurrency/_patterns.py +363 -0
- kronos/utils/concurrency/_primitives.py +328 -0
- kronos/utils/concurrency/_priority_queue.py +135 -0
- kronos/utils/concurrency/_resource_tracker.py +110 -0
- kronos/utils/concurrency/_run_async.py +67 -0
- kronos/utils/concurrency/_task.py +95 -0
- kronos/utils/concurrency/_utils.py +79 -0
- kronos/utils/fuzzy/__init__.py +14 -0
- kronos/utils/fuzzy/_extract_json.py +90 -0
- kronos/utils/fuzzy/_fuzzy_json.py +288 -0
- kronos/utils/fuzzy/_fuzzy_match.py +149 -0
- kronos/utils/fuzzy/_string_similarity.py +187 -0
- kronos/utils/fuzzy/_to_dict.py +396 -0
- kronos/utils/sql/__init__.py +13 -0
- kronos/utils/sql/_sql_validation.py +142 -0
- krons-0.1.0.dist-info/METADATA +70 -0
- krons-0.1.0.dist-info/RECORD +101 -0
- krons-0.1.0.dist-info/WHEEL +4 -0
- 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.")
|