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,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_
|
kronos/specs/__init__.py
ADDED
|
@@ -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
|
+
}
|