tba-concrete-capability 0.4.0.dev2__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.
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
import time
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from dataclasses import asdict, replace
|
|
7
|
+
|
|
8
|
+
from tba_bases_capability import CapabilityBase
|
|
9
|
+
from tba_primitives_identity.clock import utc_now_iso
|
|
10
|
+
from tba_primitives_identity.primitives import new_opaque_id, new_prefixed_id
|
|
11
|
+
from tba_contracts_identity.capabilities import (
|
|
12
|
+
CapabilityCallContext,
|
|
13
|
+
CapabilityCallable,
|
|
14
|
+
CapabilityCallResult,
|
|
15
|
+
CapabilityContextError,
|
|
16
|
+
CapabilityDeadlineExceededError,
|
|
17
|
+
CapabilityDefinition,
|
|
18
|
+
CapabilityDelegationCycleError,
|
|
19
|
+
CapabilityOperation,
|
|
20
|
+
CapabilityOperationNotFoundError,
|
|
21
|
+
CapabilityOperationUnavailableError,
|
|
22
|
+
CapabilityState,
|
|
23
|
+
CapabilityStateProvider,
|
|
24
|
+
ICapability,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Capability(CapabilityBase):
|
|
29
|
+
"""Neutral generic capability with only explicitly supplied configuration."""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
definition: CapabilityDefinition,
|
|
34
|
+
/,
|
|
35
|
+
*,
|
|
36
|
+
operations: Mapping[str, CapabilityOperation],
|
|
37
|
+
attributes: Mapping[str, object] | None = None,
|
|
38
|
+
state: CapabilityState | CapabilityStateProvider | None = None,
|
|
39
|
+
) -> None:
|
|
40
|
+
if not definition.capability_id.strip():
|
|
41
|
+
raise ValueError("capability_id is required")
|
|
42
|
+
if not definition.version.strip():
|
|
43
|
+
raise ValueError("capability version is required")
|
|
44
|
+
if not operations:
|
|
45
|
+
raise ValueError("at least one capability operation is required")
|
|
46
|
+
normalized_operations: dict[str, CapabilityOperation] = {}
|
|
47
|
+
for name, operation in operations.items():
|
|
48
|
+
if not name.strip():
|
|
49
|
+
raise ValueError("capability operation name is required")
|
|
50
|
+
if not isinstance(operation, CapabilityOperation):
|
|
51
|
+
raise TypeError(f"invalid capability operation: {name}")
|
|
52
|
+
if operation.target is not None and not callable(operation.target):
|
|
53
|
+
raise TypeError(f"capability operation target must be callable: {name}")
|
|
54
|
+
if operation.required and operation.target is None:
|
|
55
|
+
raise NotImplementedError(
|
|
56
|
+
f"required capability operation is not implemented: {name}"
|
|
57
|
+
)
|
|
58
|
+
normalized_operations[name] = operation
|
|
59
|
+
self._capability_definition = definition
|
|
60
|
+
self._capability_operations = normalized_operations
|
|
61
|
+
self._capability_attributes = dict(attributes or {})
|
|
62
|
+
if state is None:
|
|
63
|
+
self._capability_state_provider: CapabilityStateProvider = CapabilityState
|
|
64
|
+
elif isinstance(state, CapabilityState):
|
|
65
|
+
self._capability_state_provider = lambda state=state: state
|
|
66
|
+
elif callable(state):
|
|
67
|
+
self._capability_state_provider = state
|
|
68
|
+
else:
|
|
69
|
+
raise TypeError("capability state must be a CapabilityState or state provider")
|
|
70
|
+
|
|
71
|
+
def definition(self) -> CapabilityDefinition:
|
|
72
|
+
return self._capability_definition
|
|
73
|
+
|
|
74
|
+
def operations(self) -> Mapping[str, CapabilityOperation]:
|
|
75
|
+
return dict(self._capability_operations)
|
|
76
|
+
|
|
77
|
+
def attributes(self) -> Mapping[str, object]:
|
|
78
|
+
return dict(self._capability_attributes)
|
|
79
|
+
|
|
80
|
+
def callables(self) -> Mapping[str, CapabilityCallable]:
|
|
81
|
+
return {
|
|
82
|
+
name: operation.target
|
|
83
|
+
for name, operation in self._capability_operations.items()
|
|
84
|
+
if operation.target is not None
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
def state(self) -> CapabilityState:
|
|
88
|
+
state = self._capability_state_provider()
|
|
89
|
+
if not isinstance(state, CapabilityState):
|
|
90
|
+
raise TypeError("capability state provider must return CapabilityState")
|
|
91
|
+
return state
|
|
92
|
+
|
|
93
|
+
def capability_report(self) -> dict[str, object]:
|
|
94
|
+
"""Return the complete, operator-inspectable effective capability set."""
|
|
95
|
+
report: dict[str, object] = dict(asdict(self._capability_definition))
|
|
96
|
+
report["operations"] = tuple(self._capability_operations)
|
|
97
|
+
report["bound_operations"] = tuple(sorted(self.callables()))
|
|
98
|
+
report["delegated_operations"] = tuple(
|
|
99
|
+
sorted(
|
|
100
|
+
name
|
|
101
|
+
for name, operation in self._capability_operations.items()
|
|
102
|
+
if operation.target is not None and operation.delegated
|
|
103
|
+
)
|
|
104
|
+
)
|
|
105
|
+
report["optional_operations"] = tuple(
|
|
106
|
+
sorted(
|
|
107
|
+
name
|
|
108
|
+
for name, operation in self._capability_operations.items()
|
|
109
|
+
if not operation.required
|
|
110
|
+
)
|
|
111
|
+
)
|
|
112
|
+
report["unavailable_optional_operations"] = tuple(
|
|
113
|
+
sorted(
|
|
114
|
+
name
|
|
115
|
+
for name, operation in self._capability_operations.items()
|
|
116
|
+
if not operation.required and operation.target is None
|
|
117
|
+
)
|
|
118
|
+
)
|
|
119
|
+
report["state"] = asdict(self.state())
|
|
120
|
+
return report
|
|
121
|
+
|
|
122
|
+
async def call(
|
|
123
|
+
self,
|
|
124
|
+
operation: str,
|
|
125
|
+
*args: object,
|
|
126
|
+
context: CapabilityCallContext | None = None,
|
|
127
|
+
**kwargs: object,
|
|
128
|
+
) -> CapabilityCallResult:
|
|
129
|
+
try:
|
|
130
|
+
operation_definition = self._capability_operations[operation]
|
|
131
|
+
except KeyError as exc:
|
|
132
|
+
raise CapabilityOperationNotFoundError(
|
|
133
|
+
f"unknown capability operation: {operation}"
|
|
134
|
+
) from exc
|
|
135
|
+
target = operation_definition.target
|
|
136
|
+
if target is None:
|
|
137
|
+
raise CapabilityOperationUnavailableError(
|
|
138
|
+
f"unbound optional capability operation: {operation}"
|
|
139
|
+
)
|
|
140
|
+
active = context or CapabilityCallContext(call_id=new_opaque_id())
|
|
141
|
+
capability_id = self._capability_definition.capability_id
|
|
142
|
+
if active.capability_id not in {None, capability_id}:
|
|
143
|
+
raise CapabilityContextError(
|
|
144
|
+
"call context capability_id does not match target capability"
|
|
145
|
+
)
|
|
146
|
+
if active.operation not in {None, operation}:
|
|
147
|
+
raise CapabilityContextError(
|
|
148
|
+
"call context operation does not match target operation"
|
|
149
|
+
)
|
|
150
|
+
active = replace(
|
|
151
|
+
active,
|
|
152
|
+
capability_id=capability_id,
|
|
153
|
+
operation=operation,
|
|
154
|
+
)
|
|
155
|
+
if active.deadline is not None and time.time() > active.deadline:
|
|
156
|
+
raise CapabilityDeadlineExceededError(
|
|
157
|
+
"capability call deadline exceeded"
|
|
158
|
+
)
|
|
159
|
+
value = target(*args, **kwargs)
|
|
160
|
+
if inspect.isawaitable(value):
|
|
161
|
+
value = await value
|
|
162
|
+
return CapabilityCallResult(
|
|
163
|
+
value=value,
|
|
164
|
+
capability_id=self._capability_definition.capability_id,
|
|
165
|
+
operation=operation,
|
|
166
|
+
call_id=active.call_id,
|
|
167
|
+
delegated=operation_definition.delegated,
|
|
168
|
+
context=active,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
async def subcall(
|
|
172
|
+
self,
|
|
173
|
+
capability: ICapability,
|
|
174
|
+
operation: str,
|
|
175
|
+
*args: object,
|
|
176
|
+
context: CapabilityCallContext,
|
|
177
|
+
authority: tuple[str, ...] | None = None,
|
|
178
|
+
**kwargs: object,
|
|
179
|
+
) -> CapabilityCallResult:
|
|
180
|
+
target_capability_id = capability.definition().capability_id
|
|
181
|
+
delegation_chain = context.delegation_chain + (
|
|
182
|
+
self._capability_definition.capability_id,
|
|
183
|
+
)
|
|
184
|
+
if target_capability_id in delegation_chain:
|
|
185
|
+
raise CapabilityDelegationCycleError(
|
|
186
|
+
"capability delegation cycle detected"
|
|
187
|
+
)
|
|
188
|
+
child_authority = context.authority if authority is None else authority
|
|
189
|
+
if not set(child_authority).issubset(context.authority):
|
|
190
|
+
raise CapabilityContextError(
|
|
191
|
+
"delegated authority must narrow or preserve parent authority"
|
|
192
|
+
)
|
|
193
|
+
child = replace(
|
|
194
|
+
context,
|
|
195
|
+
parent_call_id=context.call_id,
|
|
196
|
+
call_id=new_opaque_id(),
|
|
197
|
+
capability_id=target_capability_id,
|
|
198
|
+
operation=operation,
|
|
199
|
+
delegation_chain=delegation_chain,
|
|
200
|
+
authority=tuple(child_authority),
|
|
201
|
+
)
|
|
202
|
+
return await capability.call(operation, *args, context=child, **kwargs)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
__all__ = ["Capability", "new_prefixed_id", "utc_now_iso"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tba-concrete-capability
|
|
3
|
+
Version: 0.4.0.dev2
|
|
4
|
+
Summary: Neutral generic capability implementation for TBA identity.
|
|
5
|
+
License-Expression: Apache-2.0
|
|
6
|
+
Requires-Python: >=3.10,<3.15
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
13
|
+
Requires-Dist: tba-bases-capability (==0.4.0.dev2)
|
|
14
|
+
Requires-Dist: tba-contracts-identity (==0.4.0.dev2)
|
|
15
|
+
Requires-Dist: tba-primitives-identity (==0.4.0.dev2)
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# tba-concrete-capability
|
|
19
|
+
|
|
20
|
+
Layer-10 neutral `Capability`. Authors provide a `CapabilityDefinition` and one
|
|
21
|
+
mapping of names to `CapabilityOperation` objects. Required operations must have
|
|
22
|
+
callable targets at construction. Optional operations may be present without a
|
|
23
|
+
target and are reported as unavailable. State is supplied as a fixed
|
|
24
|
+
`CapabilityState` or a live state provider.
|
|
25
|
+
|
|
26
|
+
The package has no implicit providers, persistence, policies, retries, timeouts,
|
|
27
|
+
namespaces, or environment-derived settings.
|
|
28
|
+
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
tba_concrete_capability/__init__.py,sha256=83wZXpKStQM6pieqfD_ZYKStgWUPzwVFvmhUKTBN9kU,8091
|
|
2
|
+
tba_concrete_capability/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
3
|
+
tba_concrete_capability-0.4.0.dev2.dist-info/METADATA,sha256=yWixd6wipc153Nab3olUGeUsmhKGZ_dyFErno218i2Y,1212
|
|
4
|
+
tba_concrete_capability-0.4.0.dev2.dist-info/WHEEL,sha256=L-WvLdSBvJWHXv5zrQxDvNRnIgoBzLUpcbuiw19KCuQ,88
|
|
5
|
+
tba_concrete_capability-0.4.0.dev2.dist-info/RECORD,,
|