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,411 @@
1
+ # Copyright (c) 2025 - 2026, HaiyangLi <quantocean.li at gmail dot com>
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Session and Branch: central orchestration for messages, services, and operations.
5
+
6
+ Session owns branches, messages, services registry, and operations registry.
7
+ Branch is a named message progression with capability/resource access control.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import contextlib
13
+ from collections.abc import Iterable
14
+ from typing import TYPE_CHECKING, Any, Literal
15
+ from uuid import UUID
16
+
17
+ from pydantic import Field
18
+
19
+ from kronos.core import Element, Flow, Progression
20
+ from kronos.errors import AccessError, NotFoundError
21
+ from kronos.operations.node import Operation
22
+ from kronos.operations.registry import OperationRegistry
23
+ from kronos.services import ServiceRegistry
24
+ from kronos.types import HashableModel, Unset, UnsetType, not_sentinel
25
+
26
+ from .message import Message
27
+
28
+ if TYPE_CHECKING:
29
+ from kronos.services.backend import Calling
30
+
31
+ __all__ = (
32
+ "Branch",
33
+ "Session",
34
+ "SessionConfig",
35
+ "capabilities_must_be_subset_of_branch",
36
+ "resource_must_be_accessible_by_branch",
37
+ "resource_must_exist_in_session",
38
+ )
39
+
40
+
41
+ class SessionConfig(HashableModel):
42
+ """Session initialization configuration.
43
+
44
+ Attributes:
45
+ default_branch_name: Name for auto-created default branch.
46
+ default_capabilities: Capabilities granted to default branch.
47
+ default_resources: Resources accessible by default branch.
48
+ auto_create_default_branch: Create "main" branch on init.
49
+ """
50
+
51
+ default_branch_name: str | None = None
52
+ default_capabilities: set[str] = Field(default_factory=set)
53
+ default_resources: set[str] = Field(default_factory=set)
54
+ auto_create_default_branch: bool = True
55
+
56
+
57
+ class Branch(Progression):
58
+ """Message progression with capability and resource access control.
59
+
60
+ Branch extends Progression with session binding and access control.
61
+ Messages are referenced by UUID in the order list.
62
+
63
+ Attributes:
64
+ session_id: Owning session (immutable after creation).
65
+ capabilities: Allowed structured output schema names.
66
+ resources: Allowed service names for access control.
67
+ """
68
+
69
+ session_id: UUID = Field(..., frozen=True)
70
+ capabilities: set[str] = Field(default_factory=set)
71
+ resources: set[str] = Field(default_factory=set)
72
+
73
+ def __repr__(self) -> str:
74
+ name_str = f" name='{self.name}'" if self.name else ""
75
+ return f"Branch(messages={len(self)}, session={str(self.session_id)[:8]}{name_str})"
76
+
77
+
78
+ class Session(Element):
79
+ """Central orchestrator: messages, branches, services, operations.
80
+
81
+ Lifecycle:
82
+ 1. Create session (auto-creates default branch unless disabled)
83
+ 2. Register services and operations
84
+ 3. Create branches for different contexts/users
85
+ 4. Add messages, conduct operations, make service requests
86
+
87
+ Attributes:
88
+ user: Optional user identifier.
89
+ communications: Flow containing messages and branches.
90
+ services: Service registry for backend access.
91
+ operations: Operation factory registry.
92
+ config: Session configuration.
93
+ default_branch_id: Branch used when none specified.
94
+
95
+ Example:
96
+ session = Session(user="agent-1")
97
+ session.services.register("openai", openai_service)
98
+ session.operations.register("chat", chat_factory)
99
+ result = await session.conduct("chat", params={"prompt": "hello"})
100
+ """
101
+
102
+ user: str | None = None
103
+ communications: Flow[Message, Branch] = None # type: ignore
104
+ services: ServiceRegistry = None # type: ignore
105
+ operations: OperationRegistry = None # type: ignore
106
+ config: SessionConfig = None # type: ignore
107
+ default_branch_id: UUID | None = None
108
+
109
+ def __init__(
110
+ self,
111
+ user: str | None = None,
112
+ communications: Flow[Message, Branch] | None = None,
113
+ services: ServiceRegistry | None = None,
114
+ operations: OperationRegistry | None = None,
115
+ config: SessionConfig | dict | None = None,
116
+ default_branch_id: UUID | None = None,
117
+ **data,
118
+ ):
119
+ """Initialize session with optional pre-configured components.
120
+
121
+ Args:
122
+ user: User identifier.
123
+ communications: Pre-existing Flow (creates new if None).
124
+ services: Pre-existing ServiceRegistry (creates new if None).
125
+ operations: Pre-existing OperationRegistry (creates new if None).
126
+ config: SessionConfig or dict (uses defaults if None).
127
+ default_branch_id: Pre-set default branch.
128
+ **data: Additional Element fields.
129
+ """
130
+ super().__init__(**data)
131
+ self.user = user
132
+ self.communications = communications or Flow(item_type=Message)
133
+ self.services = services or ServiceRegistry()
134
+ self.operations = operations or OperationRegistry()
135
+ self.default_branch_id = default_branch_id
136
+
137
+ if config is None:
138
+ self.config = SessionConfig()
139
+ elif isinstance(config, dict):
140
+ self.config = SessionConfig(**config)
141
+ else:
142
+ self.config = config
143
+
144
+ if self.config.auto_create_default_branch and self.default_branch_id is None:
145
+ branch = self.create_branch(
146
+ name=self.config.default_branch_name or "main",
147
+ capabilities=self.config.default_capabilities,
148
+ resources=self.config.default_resources,
149
+ )
150
+ self.default_branch_id = branch.id
151
+
152
+ @property
153
+ def messages(self):
154
+ """All messages in session (Pile[Message])."""
155
+ return self.communications.items
156
+
157
+ @property
158
+ def branches(self):
159
+ """All branches in session (Pile[Branch])."""
160
+ return self.communications.progressions
161
+
162
+ @property
163
+ def default_branch(self) -> Branch | None:
164
+ """Default branch, or None if unset or deleted."""
165
+ if self.default_branch_id is None:
166
+ return None
167
+ with contextlib.suppress(KeyError, NotFoundError):
168
+ return self.communications.get_progression(self.default_branch_id)
169
+ return None
170
+
171
+ def create_branch(
172
+ self,
173
+ *,
174
+ name: str | None = None,
175
+ capabilities: set[str] | None = None,
176
+ resources: set[str] | None = None,
177
+ messages: Iterable[UUID | Message] | None = None,
178
+ ) -> Branch:
179
+ """Create and register a new branch.
180
+
181
+ Args:
182
+ name: Branch name (auto: "branch_N").
183
+ capabilities: Allowed schema names.
184
+ resources: Allowed service names.
185
+ messages: Initial message UUIDs or objects.
186
+
187
+ Returns:
188
+ Created Branch added to session.
189
+ """
190
+ order: list[UUID] = []
191
+ if messages:
192
+ for msg in messages:
193
+ order.append(msg.id if isinstance(msg, Message) else msg)
194
+
195
+ branch = Branch(
196
+ session_id=self.id,
197
+ name=name or f"branch_{len(self.branches)}",
198
+ capabilities=capabilities or set(),
199
+ resources=resources or set(),
200
+ order=order,
201
+ )
202
+
203
+ self.communications.add_progression(branch)
204
+ return branch
205
+
206
+ def get_branch(
207
+ self, branch: UUID | str | Branch, default: Branch | UnsetType = Unset, /
208
+ ) -> Branch:
209
+ """Get branch by UUID, name, or instance.
210
+
211
+ Args:
212
+ branch: Branch identifier.
213
+ default: Return this if not found (else raise).
214
+
215
+ Returns:
216
+ Branch instance.
217
+
218
+ Raises:
219
+ NotFoundError: If not found and no default.
220
+ """
221
+ if isinstance(branch, Branch) and branch in self.branches:
222
+ return branch
223
+ with contextlib.suppress(KeyError):
224
+ return self.communications.get_progression(branch)
225
+ if not_sentinel(default):
226
+ return default
227
+ raise NotFoundError("Branch not found")
228
+
229
+ def set_default_branch(self, branch: Branch | UUID | str) -> None:
230
+ """Set the default branch for operations.
231
+
232
+ Args:
233
+ branch: Branch to set as default (must exist).
234
+
235
+ Raises:
236
+ NotFoundError: If branch not in session.
237
+ """
238
+ resolved = self.get_branch(branch)
239
+ self.default_branch_id = resolved.id
240
+
241
+ def fork(
242
+ self,
243
+ branch: Branch | UUID | str,
244
+ *,
245
+ name: str | None = None,
246
+ capabilities: set[str] | Literal[True] | None = None,
247
+ resources: set[str] | Literal[True] | None = None,
248
+ ) -> Branch:
249
+ """Fork branch for divergent exploration.
250
+
251
+ Creates new branch with same messages. Use True to copy access control.
252
+
253
+ Args:
254
+ branch: Source branch (Branch|UUID|str).
255
+ name: Fork name (auto: "{source}_fork").
256
+ capabilities: True=copy, None=empty, or explicit set.
257
+ resources: True=copy, None=empty, or explicit set.
258
+
259
+ Returns:
260
+ New Branch with forked_from metadata.
261
+ """
262
+ source = self.get_branch(branch)
263
+
264
+ forked = self.create_branch(
265
+ name=name or f"{source.name}_fork",
266
+ messages=source.order,
267
+ capabilities=(
268
+ {*source.capabilities} if capabilities is True else (capabilities or set())
269
+ ),
270
+ resources=({*source.resources} if resources is True else (resources or set())),
271
+ )
272
+
273
+ forked.metadata["forked_from"] = {
274
+ "branch_id": str(source.id),
275
+ "branch_name": source.name,
276
+ "created_at": source.created_at.isoformat(),
277
+ "message_count": len(source),
278
+ }
279
+ return forked
280
+
281
+ def add_message(
282
+ self,
283
+ message: Message,
284
+ branches: list[Branch | UUID | str] | Branch | UUID | str | None = None,
285
+ ) -> None:
286
+ """Add message to session, optionally appending to branch(es)."""
287
+ self.communications.add_item(message, progressions=branches)
288
+
289
+ async def request(
290
+ self,
291
+ service_name: str,
292
+ *,
293
+ branch: Branch | UUID | str | None = None,
294
+ poll_timeout: float | None = None,
295
+ poll_interval: float | None = None,
296
+ **kwargs,
297
+ ) -> Calling:
298
+ """Direct service invocation with optional access control.
299
+
300
+ Args:
301
+ service_name: Registered service name.
302
+ branch: If provided, checks service in branch.resources.
303
+ poll_timeout: Max wait seconds.
304
+ poll_interval: Poll interval seconds.
305
+ **kwargs: Service-specific arguments.
306
+
307
+ Returns:
308
+ Calling with execution results.
309
+
310
+ Raises:
311
+ AccessError: If branch lacks access to service.
312
+ NotFoundError: If service not registered.
313
+ """
314
+ if branch is not None:
315
+ resolved_branch = self.get_branch(branch)
316
+ resource_must_be_accessible_by_branch(resolved_branch, service_name)
317
+
318
+ service = self.services.get(service_name)
319
+ return await service.invoke(
320
+ poll_timeout=poll_timeout,
321
+ poll_interval=poll_interval,
322
+ **kwargs,
323
+ )
324
+
325
+ async def conduct(
326
+ self,
327
+ operation_type: str,
328
+ branch: Branch | UUID | str | None = None,
329
+ params: Any | None = None,
330
+ ) -> Operation:
331
+ """Execute operation via registry.
332
+
333
+ Args:
334
+ operation_type: Registry key.
335
+ branch: Target branch (default if None).
336
+ params: Operation parameters.
337
+
338
+ Returns:
339
+ Invoked Operation (result in op.execution.response).
340
+
341
+ Raises:
342
+ RuntimeError: No branch and no default.
343
+ KeyError: Operation not registered.
344
+ """
345
+ resolved = self._resolve_branch(branch)
346
+ op = Operation(
347
+ operation_type=operation_type,
348
+ parameters=params,
349
+ timeout=None,
350
+ streaming=False,
351
+ )
352
+ op.bind(self, resolved)
353
+ await op.invoke()
354
+ return op
355
+
356
+ def _resolve_branch(self, branch: Branch | UUID | str | None) -> Branch:
357
+ """Resolve to Branch, falling back to default. Raises if neither available."""
358
+ if branch is not None:
359
+ return self.get_branch(branch)
360
+ if self.default_branch is not None:
361
+ return self.default_branch
362
+ raise RuntimeError("No branch provided and no default branch set")
363
+
364
+ def __repr__(self) -> str:
365
+ return (
366
+ f"Session(messages={len(self.messages)}, "
367
+ f"branches={len(self.branches)}, "
368
+ f"services={len(self.services)})"
369
+ )
370
+
371
+
372
+ def resource_must_exist_in_session(session: Session, name: str) -> None:
373
+ """Validate service exists. Raise NotFoundError with available names if not."""
374
+ if not session.services.has(name):
375
+ raise NotFoundError(
376
+ f"Service '{name}' not found in session services",
377
+ details={"available": session.services.list_names()},
378
+ )
379
+
380
+
381
+ def resource_must_be_accessible_by_branch(branch: Branch, name: str) -> None:
382
+ """Validate branch has resource access. Raise AccessError if not."""
383
+ if name not in branch.resources:
384
+ raise AccessError(
385
+ f"Branch '{branch.name}' has no access to resource '{name}'",
386
+ details={
387
+ "branch": branch.name,
388
+ "resource": name,
389
+ "available": list(branch.resources),
390
+ },
391
+ )
392
+
393
+
394
+ def capabilities_must_be_subset_of_branch(branch: Branch, capabilities: set[str]) -> None:
395
+ """Validate branch has all capabilities. Raise AccessError listing missing."""
396
+ if not capabilities.issubset(branch.capabilities):
397
+ missing = capabilities - branch.capabilities
398
+ raise AccessError(
399
+ f"Branch '{branch.name}' missing capabilities: {missing}",
400
+ details={
401
+ "requested": sorted(capabilities),
402
+ "available": sorted(branch.capabilities),
403
+ },
404
+ )
405
+
406
+
407
+ def resolve_branch_exists_in_session(session: Session, branch: Branch | str) -> Branch:
408
+ """Get branch from session. Raise NotFoundError if not found."""
409
+ if (b_ := session.get_branch(branch, None)) is None:
410
+ raise NotFoundError(f"Branch '{branch}' does not exist in session")
411
+ return b_
@@ -0,0 +1,25 @@
1
+ # Copyright (c) 2025 - 2026, HaiyangLi <quantocean.li at gmail dot com>
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from .adapters.factory import AdapterType, get_adapter
5
+ from .catalog import AuditSpecs, CommonSpecs, ContentSpecs
6
+ from .operable import Operable
7
+ from .phrase import CrudOperation, CrudPattern, Phrase, phrase
8
+ from .protocol import SpecAdapter
9
+ from .spec import CommonMeta, Spec
10
+
11
+ __all__ = (
12
+ "AdapterType",
13
+ "AuditSpecs",
14
+ "CommonMeta",
15
+ "CommonSpecs",
16
+ "ContentSpecs",
17
+ "CrudOperation",
18
+ "CrudPattern",
19
+ "Operable",
20
+ "Phrase",
21
+ "Spec",
22
+ "SpecAdapter",
23
+ "get_adapter",
24
+ "phrase",
25
+ )
File without changes
@@ -0,0 +1,45 @@
1
+ from __future__ import annotations
2
+
3
+ import types
4
+ from functools import reduce
5
+ from typing import Any, Union, get_args, get_origin
6
+
7
+
8
+ def resolve_annotation_to_base_types(annotation: Any) -> dict[str, Any]:
9
+ def resolve_nullable_inner_type(_anno: Any) -> tuple[bool, Any]:
10
+ origin = get_origin(_anno)
11
+
12
+ if origin is type(None):
13
+ return True, type(None)
14
+
15
+ if origin in (type(int | str), types.UnionType) or origin is Union:
16
+ args = get_args(_anno)
17
+ non_none_args = [a for a in args if a is not type(None)]
18
+ if len(args) != len(non_none_args):
19
+ if len(non_none_args) == 1:
20
+ return True, non_none_args[0]
21
+ if non_none_args:
22
+ return True, reduce(lambda a, b: a | b, non_none_args)
23
+ return False, _anno
24
+
25
+ return False, _anno
26
+
27
+ def resolve_listable_element_type(_anno: Any) -> Any:
28
+ origin = get_origin(_anno)
29
+
30
+ if origin is list:
31
+ args = get_args(_anno)
32
+ if args:
33
+ return True, args[0]
34
+ return True, Any
35
+
36
+ return False, _anno
37
+
38
+ _null, _inner = resolve_nullable_inner_type(annotation)
39
+ _list, _elem = resolve_listable_element_type(_inner)
40
+
41
+ return {
42
+ "base_type": _elem,
43
+ "nullable": _null,
44
+ "listable": _list,
45
+ }