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.
- krons/__init__.py +49 -0
- krons/agent/__init__.py +144 -0
- krons/agent/mcps/__init__.py +14 -0
- krons/agent/mcps/loader.py +287 -0
- krons/agent/mcps/wrapper.py +799 -0
- krons/agent/message/__init__.py +20 -0
- krons/agent/message/action.py +69 -0
- krons/agent/message/assistant.py +52 -0
- krons/agent/message/common.py +49 -0
- krons/agent/message/instruction.py +130 -0
- krons/agent/message/prepare_msg.py +187 -0
- krons/agent/message/role.py +53 -0
- krons/agent/message/system.py +53 -0
- krons/agent/operations/__init__.py +82 -0
- krons/agent/operations/act.py +100 -0
- krons/agent/operations/generate.py +145 -0
- krons/agent/operations/llm_reparse.py +89 -0
- krons/agent/operations/operate.py +247 -0
- krons/agent/operations/parse.py +243 -0
- krons/agent/operations/react.py +286 -0
- krons/agent/operations/specs.py +235 -0
- krons/agent/operations/structure.py +151 -0
- krons/agent/operations/utils.py +79 -0
- krons/agent/providers/__init__.py +17 -0
- krons/agent/providers/anthropic_messages.py +146 -0
- krons/agent/providers/claude_code.py +276 -0
- krons/agent/providers/gemini.py +268 -0
- krons/agent/providers/match.py +75 -0
- krons/agent/providers/oai_chat.py +174 -0
- krons/agent/third_party/__init__.py +2 -0
- krons/agent/third_party/anthropic_models.py +154 -0
- krons/agent/third_party/claude_code.py +682 -0
- krons/agent/third_party/gemini_models.py +508 -0
- krons/agent/third_party/openai_models.py +295 -0
- krons/agent/tool.py +291 -0
- krons/core/__init__.py +127 -0
- krons/core/base/__init__.py +121 -0
- {kronos/core → krons/core/base}/broadcaster.py +7 -3
- {kronos/core → krons/core/base}/element.py +15 -7
- {kronos/core → krons/core/base}/event.py +41 -8
- {kronos/core → krons/core/base}/eventbus.py +4 -2
- {kronos/core → krons/core/base}/flow.py +14 -7
- {kronos/core → krons/core/base}/graph.py +27 -11
- {kronos/core → krons/core/base}/node.py +47 -22
- {kronos/core → krons/core/base}/pile.py +26 -12
- {kronos/core → krons/core/base}/processor.py +23 -9
- {kronos/core → krons/core/base}/progression.py +5 -3
- {kronos → krons/core}/specs/__init__.py +0 -5
- {kronos → krons/core}/specs/adapters/dataclass_field.py +16 -8
- {kronos → krons/core}/specs/adapters/pydantic_adapter.py +11 -5
- {kronos → krons/core}/specs/adapters/sql_ddl.py +16 -10
- {kronos → krons/core}/specs/catalog/__init__.py +2 -2
- {kronos → krons/core}/specs/catalog/_audit.py +3 -3
- {kronos → krons/core}/specs/catalog/_common.py +2 -2
- {kronos → krons/core}/specs/catalog/_content.py +5 -5
- {kronos → krons/core}/specs/catalog/_enforcement.py +4 -4
- {kronos → krons/core}/specs/factory.py +7 -7
- {kronos → krons/core}/specs/operable.py +9 -3
- {kronos → krons/core}/specs/protocol.py +4 -2
- {kronos → krons/core}/specs/spec.py +25 -13
- {kronos → krons/core}/types/base.py +7 -5
- {kronos → krons/core}/types/db_types.py +2 -2
- {kronos → krons/core}/types/identity.py +1 -1
- {kronos → krons}/errors.py +13 -13
- {kronos → krons}/protocols.py +9 -4
- krons/resource/__init__.py +89 -0
- {kronos/services → krons/resource}/backend.py +50 -24
- {kronos/services → krons/resource}/endpoint.py +28 -14
- {kronos/services → krons/resource}/hook.py +22 -9
- {kronos/services → krons/resource}/imodel.py +50 -32
- {kronos/services → krons/resource}/registry.py +27 -25
- {kronos/services → krons/resource}/utilities/rate_limited_executor.py +10 -6
- {kronos/services → krons/resource}/utilities/rate_limiter.py +4 -2
- {kronos/services → krons/resource}/utilities/resilience.py +17 -7
- krons/resource/utilities/token_calculator.py +185 -0
- {kronos → krons}/session/__init__.py +12 -17
- krons/session/constraints.py +70 -0
- {kronos → krons}/session/exchange.py +14 -6
- {kronos → krons}/session/message.py +4 -2
- krons/session/registry.py +35 -0
- {kronos → krons}/session/session.py +165 -174
- krons/utils/__init__.py +85 -0
- krons/utils/_function_arg_parser.py +99 -0
- krons/utils/_pythonic_function_call.py +249 -0
- {kronos → krons}/utils/_to_list.py +9 -3
- {kronos → krons}/utils/_utils.py +9 -5
- {kronos → krons}/utils/concurrency/__init__.py +38 -38
- {kronos → krons}/utils/concurrency/_async_call.py +6 -4
- {kronos → krons}/utils/concurrency/_errors.py +3 -1
- {kronos → krons}/utils/concurrency/_patterns.py +3 -1
- {kronos → krons}/utils/concurrency/_resource_tracker.py +6 -2
- krons/utils/display.py +257 -0
- {kronos → krons}/utils/fuzzy/__init__.py +6 -1
- {kronos → krons}/utils/fuzzy/_fuzzy_match.py +14 -8
- {kronos → krons}/utils/fuzzy/_string_similarity.py +3 -1
- {kronos → krons}/utils/fuzzy/_to_dict.py +3 -1
- krons/utils/schemas/__init__.py +26 -0
- krons/utils/schemas/_breakdown_pydantic_annotation.py +131 -0
- krons/utils/schemas/_formatter.py +72 -0
- krons/utils/schemas/_minimal_yaml.py +151 -0
- krons/utils/schemas/_typescript.py +153 -0
- {kronos → krons}/utils/sql/_sql_validation.py +1 -1
- krons/utils/validators/__init__.py +3 -0
- krons/utils/validators/_validate_image_url.py +56 -0
- krons/work/__init__.py +126 -0
- krons/work/engine.py +333 -0
- krons/work/form.py +305 -0
- {kronos → krons/work}/operations/__init__.py +7 -4
- {kronos → krons/work}/operations/builder.py +4 -4
- {kronos/enforcement → krons/work/operations}/context.py +37 -6
- {kronos → krons/work}/operations/flow.py +17 -9
- krons/work/operations/node.py +103 -0
- krons/work/operations/registry.py +103 -0
- {kronos/specs → krons/work}/phrase.py +131 -14
- {kronos/enforcement → krons/work}/policy.py +3 -3
- krons/work/report.py +268 -0
- krons/work/rules/__init__.py +47 -0
- {kronos/enforcement → krons/work/rules}/common/boolean.py +3 -1
- {kronos/enforcement → krons/work/rules}/common/choice.py +9 -3
- {kronos/enforcement → krons/work/rules}/common/number.py +3 -1
- {kronos/enforcement → krons/work/rules}/common/string.py +9 -3
- {kronos/enforcement → krons/work/rules}/rule.py +2 -2
- {kronos/enforcement → krons/work/rules}/validator.py +21 -6
- {kronos/enforcement → krons/work}/service.py +16 -7
- krons/work/worker.py +266 -0
- {krons-0.1.0.dist-info → krons-0.2.0.dist-info}/METADATA +19 -5
- krons-0.2.0.dist-info/RECORD +154 -0
- kronos/core/__init__.py +0 -145
- kronos/enforcement/__init__.py +0 -57
- kronos/operations/node.py +0 -101
- kronos/operations/registry.py +0 -92
- kronos/services/__init__.py +0 -81
- kronos/specs/adapters/__init__.py +0 -0
- kronos/utils/__init__.py +0 -40
- krons-0.1.0.dist-info/RECORD +0 -101
- {kronos → krons/core/specs/adapters}/__init__.py +0 -0
- {kronos → krons/core}/specs/adapters/_utils.py +0 -0
- {kronos → krons/core}/specs/adapters/factory.py +0 -0
- {kronos → krons/core}/types/__init__.py +0 -0
- {kronos → krons/core}/types/_sentinel.py +0 -0
- {kronos → krons}/py.typed +0 -0
- {kronos/services → krons/resource}/utilities/__init__.py +0 -0
- {kronos/services → krons/resource}/utilities/header_factory.py +0 -0
- {kronos → krons}/utils/_hash.py +0 -0
- {kronos → krons}/utils/_json_dump.py +0 -0
- {kronos → krons}/utils/_lazy_init.py +0 -0
- {kronos → krons}/utils/_to_num.py +0 -0
- {kronos → krons}/utils/concurrency/_cancel.py +0 -0
- {kronos → krons}/utils/concurrency/_primitives.py +0 -0
- {kronos → krons}/utils/concurrency/_priority_queue.py +0 -0
- {kronos → krons}/utils/concurrency/_run_async.py +0 -0
- {kronos → krons}/utils/concurrency/_task.py +0 -0
- {kronos → krons}/utils/concurrency/_utils.py +0 -0
- {kronos → krons}/utils/fuzzy/_extract_json.py +0 -0
- {kronos → krons}/utils/fuzzy/_fuzzy_json.py +0 -0
- {kronos → krons}/utils/sql/__init__.py +0 -0
- {kronos/enforcement → krons/work/rules}/common/__init__.py +0 -0
- {kronos/enforcement → krons/work/rules}/common/mapping.py +0 -0
- {kronos/enforcement → krons/work/rules}/common/model.py +0 -0
- {kronos/enforcement → krons/work/rules}/registry.py +0 -0
- {krons-0.1.0.dist-info → krons-0.2.0.dist-info}/WHEEL +0 -0
- {krons-0.1.0.dist-info → krons-0.2.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -11,11 +11,11 @@ from datetime import datetime
|
|
|
11
11
|
from typing import TYPE_CHECKING, Any, Protocol
|
|
12
12
|
from uuid import UUID, uuid4
|
|
13
13
|
|
|
14
|
-
from
|
|
15
|
-
from
|
|
14
|
+
from krons.core.types.base import DataClass
|
|
15
|
+
from krons.core.types.identity import ID
|
|
16
16
|
|
|
17
17
|
if TYPE_CHECKING:
|
|
18
|
-
from
|
|
18
|
+
from krons.session import Branch, Session
|
|
19
19
|
|
|
20
20
|
__all__ = ("QueryFn", "RequestContext")
|
|
21
21
|
|
|
@@ -85,7 +85,7 @@ class RequestContext(DataClass):
|
|
|
85
85
|
name: str
|
|
86
86
|
id: UUID = field(default_factory=uuid4)
|
|
87
87
|
session_id: ID[Session] | None = None
|
|
88
|
-
|
|
88
|
+
branch: ID[Branch] | str | None = None
|
|
89
89
|
metadata: dict[str, Any] = field(default_factory=dict)
|
|
90
90
|
conn: Any | None = None
|
|
91
91
|
query_fn: QueryFn | None = None
|
|
@@ -95,7 +95,7 @@ class RequestContext(DataClass):
|
|
|
95
95
|
self,
|
|
96
96
|
name: str,
|
|
97
97
|
session_id: ID[Session] | None = None,
|
|
98
|
-
|
|
98
|
+
branch: ID[Branch] | str | None = None,
|
|
99
99
|
id: UUID | None = None,
|
|
100
100
|
conn: Any | None = None,
|
|
101
101
|
query_fn: QueryFn | None = None,
|
|
@@ -105,7 +105,7 @@ class RequestContext(DataClass):
|
|
|
105
105
|
self.name = name
|
|
106
106
|
self.id = id or uuid4()
|
|
107
107
|
self.session_id = session_id
|
|
108
|
-
self.
|
|
108
|
+
self.branch = branch
|
|
109
109
|
self.conn = conn
|
|
110
110
|
self.query_fn = query_fn
|
|
111
111
|
self.now = now
|
|
@@ -127,3 +127,34 @@ class RequestContext(DataClass):
|
|
|
127
127
|
raise AttributeError(
|
|
128
128
|
f"'{type(self).__name__}' has no attribute '{name}'"
|
|
129
129
|
) from None
|
|
130
|
+
|
|
131
|
+
async def get_session(self) -> Session | None:
|
|
132
|
+
"""Get the Session object.
|
|
133
|
+
|
|
134
|
+
Checks bound reference first (set by Operation.invoke),
|
|
135
|
+
then falls back to global registry lookup via session_id.
|
|
136
|
+
|
|
137
|
+
Returns None if no session available.
|
|
138
|
+
"""
|
|
139
|
+
if "_bound_session" in self.metadata:
|
|
140
|
+
return self.metadata["_bound_session"]
|
|
141
|
+
if self.session_id is None:
|
|
142
|
+
return None
|
|
143
|
+
from krons.session.registry import get_session
|
|
144
|
+
|
|
145
|
+
return await get_session(self.session_id)
|
|
146
|
+
|
|
147
|
+
async def get_branch(self) -> Branch | None:
|
|
148
|
+
"""Get the Branch object.
|
|
149
|
+
|
|
150
|
+
Checks bound reference first (set by Operation.invoke),
|
|
151
|
+
then falls back to session lookup via branch identifier.
|
|
152
|
+
|
|
153
|
+
Returns None if no branch available.
|
|
154
|
+
"""
|
|
155
|
+
if "_bound_branch" in self.metadata:
|
|
156
|
+
return self.metadata["_bound_branch"]
|
|
157
|
+
session = await self.get_session()
|
|
158
|
+
if session is None or self.branch is None:
|
|
159
|
+
return None
|
|
160
|
+
return session.branches.get(self.branch)
|
|
@@ -15,15 +15,15 @@ from dataclasses import dataclass
|
|
|
15
15
|
from typing import TYPE_CHECKING, Any
|
|
16
16
|
from uuid import UUID
|
|
17
17
|
|
|
18
|
-
from
|
|
19
|
-
from
|
|
20
|
-
from
|
|
21
|
-
from
|
|
18
|
+
from krons.core import EventStatus, Graph
|
|
19
|
+
from krons.core.types import Undefined, UndefinedType, is_sentinel
|
|
20
|
+
from krons.utils import concurrency
|
|
21
|
+
from krons.utils.concurrency import CapacityLimiter, CompletionStream
|
|
22
22
|
|
|
23
23
|
from .node import Operation
|
|
24
24
|
|
|
25
25
|
if TYPE_CHECKING:
|
|
26
|
-
from
|
|
26
|
+
from krons.session import Branch, Session
|
|
27
27
|
|
|
28
28
|
logger = logging.getLogger(__name__)
|
|
29
29
|
|
|
@@ -94,8 +94,12 @@ class DependencyAwareExecutor:
|
|
|
94
94
|
"""
|
|
95
95
|
self.session = session
|
|
96
96
|
self.graph = graph
|
|
97
|
-
resolved_max_concurrent =
|
|
98
|
-
|
|
97
|
+
resolved_max_concurrent = (
|
|
98
|
+
None if is_sentinel(max_concurrent) else max_concurrent
|
|
99
|
+
)
|
|
100
|
+
resolved_default_branch = (
|
|
101
|
+
None if is_sentinel(default_branch) else default_branch
|
|
102
|
+
)
|
|
99
103
|
self.max_concurrent = resolved_max_concurrent
|
|
100
104
|
self.stop_on_error = stop_on_error
|
|
101
105
|
self.verbose = verbose
|
|
@@ -107,7 +111,9 @@ class DependencyAwareExecutor:
|
|
|
107
111
|
self.operation_branches: dict[UUID, Branch | None] = {}
|
|
108
112
|
|
|
109
113
|
self._limiter: CapacityLimiter | None = (
|
|
110
|
-
CapacityLimiter(resolved_max_concurrent)
|
|
114
|
+
CapacityLimiter(resolved_max_concurrent)
|
|
115
|
+
if resolved_max_concurrent
|
|
116
|
+
else None
|
|
111
117
|
)
|
|
112
118
|
|
|
113
119
|
for node in graph.nodes:
|
|
@@ -258,7 +264,9 @@ class DependencyAwareExecutor:
|
|
|
258
264
|
self.operation_branches[node.id] = default_branch
|
|
259
265
|
|
|
260
266
|
if self.verbose:
|
|
261
|
-
logger.debug(
|
|
267
|
+
logger.debug(
|
|
268
|
+
"Pre-allocated branches for %d operations", len(self.operation_branches)
|
|
269
|
+
)
|
|
262
270
|
|
|
263
271
|
async def _execute_operation(self, operation: Operation) -> Operation:
|
|
264
272
|
"""Execute single operation: wait deps -> acquire slot -> invoke -> signal."""
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Copyright (c) 2025 - 2026, HaiyangLi <quantocean.li at gmail dot com>
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""Operation: executable graph node bridging session to handler.
|
|
5
|
+
|
|
6
|
+
Operation.invoke() creates a RequestContext from the bound session/branch
|
|
7
|
+
and calls the registered handler with (params, ctx). Handlers never need
|
|
8
|
+
to know about the factory pattern — they receive a clean RequestContext.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import TYPE_CHECKING, Any
|
|
14
|
+
|
|
15
|
+
from pydantic import Field, PrivateAttr
|
|
16
|
+
|
|
17
|
+
from krons.core import Event, Node
|
|
18
|
+
|
|
19
|
+
from .context import RequestContext
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from krons.session import Branch, Session
|
|
23
|
+
|
|
24
|
+
__all__ = ("Operation",)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Operation(Node, Event):
|
|
28
|
+
"""Executable operation node.
|
|
29
|
+
|
|
30
|
+
Bridges session.conduct() to handler(params, ctx) by:
|
|
31
|
+
1. Storing bound session/branch references
|
|
32
|
+
2. Creating RequestContext with those references
|
|
33
|
+
3. Looking up the handler from session.operations registry
|
|
34
|
+
4. Calling handler(params, ctx)
|
|
35
|
+
|
|
36
|
+
The result is stored in execution.response (via Event.invoke).
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
operation_type: str
|
|
40
|
+
parameters: dict[str, Any] | Any = Field(
|
|
41
|
+
default_factory=dict,
|
|
42
|
+
description="Operation parameters (Params dataclass, dict, or model)",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
_session: Any = PrivateAttr(default=None)
|
|
46
|
+
_branch: Any = PrivateAttr(default=None)
|
|
47
|
+
_verbose: bool = PrivateAttr(default=False)
|
|
48
|
+
|
|
49
|
+
def bind(self, session: Session, branch: Branch) -> Operation:
|
|
50
|
+
"""Bind session and branch for execution.
|
|
51
|
+
|
|
52
|
+
Must be called before invoke() if not using Session.conduct().
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
session: Session with operations registry and services.
|
|
56
|
+
branch: Branch for message context.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
Self for chaining.
|
|
60
|
+
"""
|
|
61
|
+
self._session = session
|
|
62
|
+
self._branch = branch
|
|
63
|
+
return self
|
|
64
|
+
|
|
65
|
+
def _require_binding(self) -> tuple[Session, Branch]:
|
|
66
|
+
"""Return bound (session, branch) tuple or raise RuntimeError if unbound."""
|
|
67
|
+
if self._session is None or self._branch is None:
|
|
68
|
+
raise RuntimeError(
|
|
69
|
+
"Operation not bound to session/branch. "
|
|
70
|
+
"Use operation.bind(session, branch) or session.conduct(...)"
|
|
71
|
+
)
|
|
72
|
+
return self._session, self._branch
|
|
73
|
+
|
|
74
|
+
async def _invoke(self) -> Any:
|
|
75
|
+
"""Execute handler via session's operation registry.
|
|
76
|
+
|
|
77
|
+
Creates a RequestContext with bound session/branch references
|
|
78
|
+
and calls handler(params, ctx). Called by Event.invoke().
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Handler result (stored in execution.response).
|
|
82
|
+
|
|
83
|
+
Raises:
|
|
84
|
+
RuntimeError: If not bound.
|
|
85
|
+
KeyError: If operation_type not registered.
|
|
86
|
+
"""
|
|
87
|
+
session, branch = self._require_binding()
|
|
88
|
+
handler = session.operations.get(self.operation_type)
|
|
89
|
+
|
|
90
|
+
ctx = RequestContext(
|
|
91
|
+
name=self.operation_type,
|
|
92
|
+
session_id=session.id,
|
|
93
|
+
branch=branch.name or str(branch.id),
|
|
94
|
+
_bound_session=session,
|
|
95
|
+
_bound_branch=branch,
|
|
96
|
+
_verbose=self._verbose,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
return await handler(self.parameters, ctx)
|
|
100
|
+
|
|
101
|
+
def __repr__(self) -> str:
|
|
102
|
+
bound = "bound" if self._session is not None else "unbound"
|
|
103
|
+
return f"Operation(type={self.operation_type}, status={self.execution.status.value}, {bound})"
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Copyright (c) 2025 - 2026, HaiyangLi <quantocean.li at gmail dot com>
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""Per-session operation handler registry.
|
|
5
|
+
|
|
6
|
+
Maps operation names to async handlers. Instantiated per-Session
|
|
7
|
+
for isolation, testability, and per-session customization.
|
|
8
|
+
|
|
9
|
+
Handler signature: async handler(params, ctx: RequestContext) -> result
|
|
10
|
+
Operation._invoke() creates the RequestContext from bound session/branch.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from collections.abc import Awaitable, Callable
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
__all__ = ("OperationHandler", "OperationRegistry")
|
|
19
|
+
|
|
20
|
+
OperationHandler = Callable[..., Awaitable[Any]]
|
|
21
|
+
"""Handler signature: async (params, ctx: RequestContext) -> result"""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class OperationRegistry:
|
|
25
|
+
"""Map operation names to async handler functions.
|
|
26
|
+
|
|
27
|
+
Per-session registry (not global) for isolation and testability.
|
|
28
|
+
|
|
29
|
+
Example:
|
|
30
|
+
from krons.agent.operations import generate, structure, operate
|
|
31
|
+
|
|
32
|
+
registry = OperationRegistry()
|
|
33
|
+
registry.register("generate", generate)
|
|
34
|
+
registry.register("structure", structure)
|
|
35
|
+
registry.register("operate", operate)
|
|
36
|
+
|
|
37
|
+
# Called by Operation._invoke() — users call session.conduct()
|
|
38
|
+
handler = registry.get("generate")
|
|
39
|
+
result = await handler(params, ctx)
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(self):
|
|
43
|
+
"""Initialize empty registry."""
|
|
44
|
+
self._handlers: dict[str, OperationHandler] = {}
|
|
45
|
+
|
|
46
|
+
def register(
|
|
47
|
+
self,
|
|
48
|
+
operation_name: str,
|
|
49
|
+
handler: OperationHandler,
|
|
50
|
+
*,
|
|
51
|
+
override: bool = False,
|
|
52
|
+
) -> None:
|
|
53
|
+
"""Register handler for operation name.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
operation_name: Lookup key (e.g. "generate", "operate").
|
|
57
|
+
handler: Async (params, ctx) -> result.
|
|
58
|
+
override: Allow replacing existing. Default False.
|
|
59
|
+
|
|
60
|
+
Raises:
|
|
61
|
+
ValueError: If name exists and override=False.
|
|
62
|
+
"""
|
|
63
|
+
if operation_name in self._handlers and not override:
|
|
64
|
+
raise ValueError(
|
|
65
|
+
f"Operation '{operation_name}' already registered. "
|
|
66
|
+
"Use override=True to replace."
|
|
67
|
+
)
|
|
68
|
+
self._handlers[operation_name] = handler
|
|
69
|
+
|
|
70
|
+
def get(self, operation_name: str) -> OperationHandler:
|
|
71
|
+
"""Get handler by name. Raises KeyError with available names if not found."""
|
|
72
|
+
if operation_name not in self._handlers:
|
|
73
|
+
raise KeyError(
|
|
74
|
+
f"Operation '{operation_name}' not registered. "
|
|
75
|
+
f"Available: {self.list_names()}"
|
|
76
|
+
)
|
|
77
|
+
return self._handlers[operation_name]
|
|
78
|
+
|
|
79
|
+
def has(self, operation_name: str) -> bool:
|
|
80
|
+
"""Check if name is registered."""
|
|
81
|
+
return operation_name in self._handlers
|
|
82
|
+
|
|
83
|
+
def unregister(self, operation_name: str) -> bool:
|
|
84
|
+
"""Remove registration. Returns True if existed."""
|
|
85
|
+
if operation_name in self._handlers:
|
|
86
|
+
del self._handlers[operation_name]
|
|
87
|
+
return True
|
|
88
|
+
return False
|
|
89
|
+
|
|
90
|
+
def list_names(self) -> list[str]:
|
|
91
|
+
"""Return all registered operation names."""
|
|
92
|
+
return list(self._handlers.keys())
|
|
93
|
+
|
|
94
|
+
def __contains__(self, operation_name: str) -> bool:
|
|
95
|
+
"""Support 'name in registry' syntax."""
|
|
96
|
+
return operation_name in self._handlers
|
|
97
|
+
|
|
98
|
+
def __len__(self) -> int:
|
|
99
|
+
"""Count of registered operations."""
|
|
100
|
+
return len(self._handlers)
|
|
101
|
+
|
|
102
|
+
def __repr__(self) -> str:
|
|
103
|
+
return f"OperationRegistry(operations={self.list_names()})"
|
|
@@ -6,7 +6,7 @@ A Phrase wraps an async handler with:
|
|
|
6
6
|
- Validation via Operable
|
|
7
7
|
|
|
8
8
|
Usage with decorator (custom handler):
|
|
9
|
-
from
|
|
9
|
+
from krons.core.specs import Operable, phrase
|
|
10
10
|
|
|
11
11
|
consent_operable = Operable([
|
|
12
12
|
Spec("subject_id", UUID),
|
|
@@ -25,7 +25,7 @@ Usage with decorator (custom handler):
|
|
|
25
25
|
result = await verify_consent({"subject_id": id, "scope": "background"}, ctx)
|
|
26
26
|
|
|
27
27
|
Usage with CrudPattern (declarative):
|
|
28
|
-
from
|
|
28
|
+
from krons.core.specs import Operable, phrase, CrudPattern
|
|
29
29
|
|
|
30
30
|
def check_has_consent(row):
|
|
31
31
|
return {"has_consent": row["status"] in {"active"} if row else False}
|
|
@@ -48,12 +48,14 @@ from collections.abc import Awaitable, Callable, Mapping
|
|
|
48
48
|
from dataclasses import dataclass
|
|
49
49
|
from enum import Enum
|
|
50
50
|
from types import MappingProxyType
|
|
51
|
-
from typing import Any
|
|
51
|
+
from typing import TYPE_CHECKING, Any
|
|
52
52
|
|
|
53
|
-
from
|
|
54
|
-
from
|
|
53
|
+
from krons.core.specs.operable import Operable
|
|
54
|
+
from krons.core.types import Unset, is_unset
|
|
55
|
+
from krons.utils.sql import validate_identifier
|
|
55
56
|
|
|
56
|
-
|
|
57
|
+
if TYPE_CHECKING:
|
|
58
|
+
from krons.work.operations.node import Operation
|
|
57
59
|
|
|
58
60
|
__all__ = ("CrudPattern", "CrudOperation", "Phrase", "phrase")
|
|
59
61
|
|
|
@@ -111,18 +113,31 @@ class CrudPattern:
|
|
|
111
113
|
object.__setattr__(self, "lookup", frozenset(self.lookup))
|
|
112
114
|
# Normalize None mappings to immutable empty maps; freeze mutable dicts
|
|
113
115
|
object.__setattr__(
|
|
114
|
-
self,
|
|
115
|
-
|
|
116
|
+
self,
|
|
117
|
+
"filters",
|
|
118
|
+
(
|
|
119
|
+
_EMPTY_MAP
|
|
120
|
+
if self.filters is None
|
|
121
|
+
else MappingProxyType(dict(self.filters))
|
|
122
|
+
),
|
|
116
123
|
)
|
|
117
124
|
object.__setattr__(
|
|
118
|
-
self,
|
|
119
|
-
|
|
120
|
-
|
|
125
|
+
self,
|
|
126
|
+
"set_fields",
|
|
127
|
+
(
|
|
128
|
+
_EMPTY_MAP
|
|
129
|
+
if self.set_fields is None
|
|
130
|
+
else MappingProxyType(dict(self.set_fields))
|
|
131
|
+
),
|
|
121
132
|
)
|
|
122
133
|
object.__setattr__(
|
|
123
|
-
self,
|
|
124
|
-
|
|
125
|
-
|
|
134
|
+
self,
|
|
135
|
+
"defaults",
|
|
136
|
+
(
|
|
137
|
+
_EMPTY_MAP
|
|
138
|
+
if self.defaults is None
|
|
139
|
+
else MappingProxyType(dict(self.defaults))
|
|
140
|
+
),
|
|
126
141
|
)
|
|
127
142
|
|
|
128
143
|
|
|
@@ -307,6 +322,108 @@ class Phrase:
|
|
|
307
322
|
)
|
|
308
323
|
return self._result_type
|
|
309
324
|
|
|
325
|
+
# --- Form-like interface ---
|
|
326
|
+
|
|
327
|
+
@property
|
|
328
|
+
def input_fields(self) -> list[str]:
|
|
329
|
+
"""Input field names (Form-compatible interface)."""
|
|
330
|
+
return list(self.inputs)
|
|
331
|
+
|
|
332
|
+
@property
|
|
333
|
+
def output_fields(self) -> list[str]:
|
|
334
|
+
"""Output field names (Form-compatible interface)."""
|
|
335
|
+
return list(self.outputs)
|
|
336
|
+
|
|
337
|
+
def is_workable(self, available_data: dict[str, Any]) -> bool:
|
|
338
|
+
"""Check if all inputs are available in data dict.
|
|
339
|
+
|
|
340
|
+
Enables Form/Report-style scheduling based on data availability.
|
|
341
|
+
"""
|
|
342
|
+
return all(field in available_data for field in self.inputs)
|
|
343
|
+
|
|
344
|
+
def extract_inputs(self, available_data: dict[str, Any]) -> dict[str, Any]:
|
|
345
|
+
"""Extract input values from available data.
|
|
346
|
+
|
|
347
|
+
Returns:
|
|
348
|
+
Dict with only the fields declared as inputs.
|
|
349
|
+
|
|
350
|
+
Raises:
|
|
351
|
+
KeyError: If any required input is missing.
|
|
352
|
+
"""
|
|
353
|
+
return {field: available_data[field] for field in self.inputs}
|
|
354
|
+
|
|
355
|
+
# --- Operation bridge ---
|
|
356
|
+
|
|
357
|
+
def as_operation(
|
|
358
|
+
self,
|
|
359
|
+
options: dict[str, Any] | None = None,
|
|
360
|
+
*,
|
|
361
|
+
available_data: dict[str, Any] | None = None,
|
|
362
|
+
ctx: Any = None,
|
|
363
|
+
**metadata,
|
|
364
|
+
) -> "Operation":
|
|
365
|
+
"""Create an Operation that invokes this phrase.
|
|
366
|
+
|
|
367
|
+
The Operation can participate in DAG flow while preserving
|
|
368
|
+
the phrase's typed I/O semantics.
|
|
369
|
+
|
|
370
|
+
Args:
|
|
371
|
+
options: Direct input values (takes precedence)
|
|
372
|
+
available_data: Data dict to extract inputs from (if options not given)
|
|
373
|
+
ctx: Execution context passed to phrase handler
|
|
374
|
+
**metadata: Additional metadata for Operation
|
|
375
|
+
|
|
376
|
+
Returns:
|
|
377
|
+
PhraseOperation instance ready for DAG execution.
|
|
378
|
+
|
|
379
|
+
Example:
|
|
380
|
+
# Direct options
|
|
381
|
+
op = phrase.as_operation({"subject_id": id, "scope": "read"})
|
|
382
|
+
|
|
383
|
+
# From available data (Form/Report pattern)
|
|
384
|
+
op = phrase.as_operation(available_data=report.available_data)
|
|
385
|
+
|
|
386
|
+
# Build DAG
|
|
387
|
+
graph = Graph()
|
|
388
|
+
graph.add_node(phrase1.as_operation({"input": "x"}))
|
|
389
|
+
await flow(session, graph)
|
|
390
|
+
"""
|
|
391
|
+
from krons.work.operations.node import Operation
|
|
392
|
+
|
|
393
|
+
# Resolve options
|
|
394
|
+
if options is not None:
|
|
395
|
+
resolved_options = dict(options)
|
|
396
|
+
elif available_data is not None:
|
|
397
|
+
if not self.is_workable(available_data):
|
|
398
|
+
missing = [f for f in self.inputs if f not in available_data]
|
|
399
|
+
raise ValueError(f"Missing required inputs: {missing}")
|
|
400
|
+
resolved_options = self.extract_inputs(available_data)
|
|
401
|
+
else:
|
|
402
|
+
raise ValueError("Either options or available_data must be provided")
|
|
403
|
+
|
|
404
|
+
# Create closure-based operation that bypasses registry
|
|
405
|
+
phrase = self
|
|
406
|
+
bound_ctx = ctx
|
|
407
|
+
|
|
408
|
+
class PhraseOperation(Operation):
|
|
409
|
+
"""Operation wrapping a Phrase for direct invocation."""
|
|
410
|
+
|
|
411
|
+
async def _invoke(self) -> Any:
|
|
412
|
+
# Direct phrase invocation - no registry lookup
|
|
413
|
+
return await phrase(self.parameters, bound_ctx)
|
|
414
|
+
|
|
415
|
+
return PhraseOperation(
|
|
416
|
+
operation_type=self.name,
|
|
417
|
+
parameters=resolved_options,
|
|
418
|
+
metadata={
|
|
419
|
+
"name": self.name,
|
|
420
|
+
"phrase": True,
|
|
421
|
+
"input_fields": self.input_fields,
|
|
422
|
+
"output_fields": self.output_fields,
|
|
423
|
+
**metadata,
|
|
424
|
+
},
|
|
425
|
+
)
|
|
426
|
+
|
|
310
427
|
|
|
311
428
|
def _to_pascal(snake_name: str) -> str:
|
|
312
429
|
"""Convert snake_case name to PascalCase.
|
|
@@ -13,9 +13,9 @@ from collections.abc import Sequence
|
|
|
13
13
|
from dataclasses import dataclass, field
|
|
14
14
|
from typing import Any, Protocol, runtime_checkable
|
|
15
15
|
|
|
16
|
-
from
|
|
17
|
-
from
|
|
18
|
-
from
|
|
16
|
+
from krons.core.specs.catalog._enforcement import EnforcementLevel
|
|
17
|
+
from krons.core.types.base import DataClass
|
|
18
|
+
from krons.work.operations.context import RequestContext
|
|
19
19
|
|
|
20
20
|
__all__ = (
|
|
21
21
|
"EnforcementLevel",
|