tigrbl 0.0.1.dev1__py3-none-any.whl → 0.3.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.
- tigrbl/README.md +94 -0
- tigrbl/__init__.py +139 -14
- tigrbl/api/__init__.py +6 -0
- tigrbl/api/_api.py +97 -0
- tigrbl/api/api_spec.py +30 -0
- tigrbl/api/mro_collect.py +43 -0
- tigrbl/api/shortcuts.py +56 -0
- tigrbl/api/tigrbl_api.py +291 -0
- tigrbl/app/__init__.py +0 -0
- tigrbl/app/_app.py +86 -0
- tigrbl/app/_model_registry.py +41 -0
- tigrbl/app/app_spec.py +42 -0
- tigrbl/app/mro_collect.py +67 -0
- tigrbl/app/shortcuts.py +65 -0
- tigrbl/app/tigrbl_app.py +319 -0
- tigrbl/bindings/__init__.py +73 -0
- tigrbl/bindings/api/__init__.py +12 -0
- tigrbl/bindings/api/common.py +109 -0
- tigrbl/bindings/api/include.py +256 -0
- tigrbl/bindings/api/resource_proxy.py +149 -0
- tigrbl/bindings/api/rpc.py +111 -0
- tigrbl/bindings/columns.py +49 -0
- tigrbl/bindings/handlers/__init__.py +11 -0
- tigrbl/bindings/handlers/builder.py +119 -0
- tigrbl/bindings/handlers/ctx.py +74 -0
- tigrbl/bindings/handlers/identifiers.py +228 -0
- tigrbl/bindings/handlers/namespaces.py +51 -0
- tigrbl/bindings/handlers/steps.py +276 -0
- tigrbl/bindings/hooks.py +311 -0
- tigrbl/bindings/model.py +194 -0
- tigrbl/bindings/model_helpers.py +139 -0
- tigrbl/bindings/model_registry.py +77 -0
- tigrbl/bindings/rest/__init__.py +7 -0
- tigrbl/bindings/rest/attach.py +34 -0
- tigrbl/bindings/rest/collection.py +286 -0
- tigrbl/bindings/rest/common.py +120 -0
- tigrbl/bindings/rest/fastapi.py +76 -0
- tigrbl/bindings/rest/helpers.py +119 -0
- tigrbl/bindings/rest/io.py +317 -0
- tigrbl/bindings/rest/io_headers.py +49 -0
- tigrbl/bindings/rest/member.py +386 -0
- tigrbl/bindings/rest/router.py +296 -0
- tigrbl/bindings/rest/routing.py +153 -0
- tigrbl/bindings/rpc.py +364 -0
- tigrbl/bindings/schemas/__init__.py +11 -0
- tigrbl/bindings/schemas/builder.py +348 -0
- tigrbl/bindings/schemas/defaults.py +260 -0
- tigrbl/bindings/schemas/utils.py +193 -0
- tigrbl/column/README.md +62 -0
- tigrbl/column/__init__.py +72 -0
- tigrbl/column/_column.py +96 -0
- tigrbl/column/column_spec.py +40 -0
- tigrbl/column/field_spec.py +31 -0
- tigrbl/column/infer/__init__.py +25 -0
- tigrbl/column/infer/core.py +92 -0
- tigrbl/column/infer/jsonhints.py +44 -0
- tigrbl/column/infer/planning.py +133 -0
- tigrbl/column/infer/types.py +102 -0
- tigrbl/column/infer/utils.py +59 -0
- tigrbl/column/io_spec.py +136 -0
- tigrbl/column/mro_collect.py +59 -0
- tigrbl/column/shortcuts.py +89 -0
- tigrbl/column/storage_spec.py +65 -0
- tigrbl/config/__init__.py +19 -0
- tigrbl/config/constants.py +224 -0
- tigrbl/config/defaults.py +29 -0
- tigrbl/config/resolver.py +295 -0
- tigrbl/core/__init__.py +47 -0
- tigrbl/core/crud/__init__.py +36 -0
- tigrbl/core/crud/bulk.py +168 -0
- tigrbl/core/crud/helpers/__init__.py +76 -0
- tigrbl/core/crud/helpers/db.py +92 -0
- tigrbl/core/crud/helpers/enum.py +86 -0
- tigrbl/core/crud/helpers/filters.py +162 -0
- tigrbl/core/crud/helpers/model.py +123 -0
- tigrbl/core/crud/helpers/normalize.py +99 -0
- tigrbl/core/crud/ops.py +235 -0
- tigrbl/ddl/__init__.py +344 -0
- tigrbl/decorators.py +17 -0
- tigrbl/deps/__init__.py +20 -0
- tigrbl/deps/fastapi.py +45 -0
- tigrbl/deps/favicon.svg +4 -0
- tigrbl/deps/jinja.py +27 -0
- tigrbl/deps/pydantic.py +10 -0
- tigrbl/deps/sqlalchemy.py +94 -0
- tigrbl/deps/starlette.py +36 -0
- tigrbl/engine/__init__.py +45 -0
- tigrbl/engine/_engine.py +144 -0
- tigrbl/engine/bind.py +33 -0
- tigrbl/engine/builders.py +236 -0
- tigrbl/engine/capabilities.py +29 -0
- tigrbl/engine/collect.py +111 -0
- tigrbl/engine/decorators.py +110 -0
- tigrbl/engine/docs/PLUGINS.md +49 -0
- tigrbl/engine/engine_spec.py +355 -0
- tigrbl/engine/plugins.py +52 -0
- tigrbl/engine/registry.py +36 -0
- tigrbl/engine/resolver.py +224 -0
- tigrbl/engine/shortcuts.py +216 -0
- tigrbl/hook/__init__.py +21 -0
- tigrbl/hook/_hook.py +22 -0
- tigrbl/hook/decorators.py +28 -0
- tigrbl/hook/hook_spec.py +24 -0
- tigrbl/hook/mro_collect.py +98 -0
- tigrbl/hook/shortcuts.py +44 -0
- tigrbl/hook/types.py +76 -0
- tigrbl/op/__init__.py +50 -0
- tigrbl/op/_op.py +31 -0
- tigrbl/op/canonical.py +31 -0
- tigrbl/op/collect.py +11 -0
- tigrbl/op/decorators.py +238 -0
- tigrbl/op/model_registry.py +301 -0
- tigrbl/op/mro_collect.py +99 -0
- tigrbl/op/resolver.py +216 -0
- tigrbl/op/types.py +136 -0
- tigrbl/orm/__init__.py +1 -0
- tigrbl/orm/mixins/_RowBound.py +83 -0
- tigrbl/orm/mixins/__init__.py +95 -0
- tigrbl/orm/mixins/bootstrappable.py +113 -0
- tigrbl/orm/mixins/bound.py +47 -0
- tigrbl/orm/mixins/edges.py +40 -0
- tigrbl/orm/mixins/fields.py +165 -0
- tigrbl/orm/mixins/hierarchy.py +54 -0
- tigrbl/orm/mixins/key_digest.py +44 -0
- tigrbl/orm/mixins/lifecycle.py +115 -0
- tigrbl/orm/mixins/locks.py +51 -0
- tigrbl/orm/mixins/markers.py +16 -0
- tigrbl/orm/mixins/operations.py +57 -0
- tigrbl/orm/mixins/ownable.py +337 -0
- tigrbl/orm/mixins/principals.py +98 -0
- tigrbl/orm/mixins/tenant_bound.py +301 -0
- tigrbl/orm/mixins/upsertable.py +118 -0
- tigrbl/orm/mixins/utils.py +49 -0
- tigrbl/orm/tables/__init__.py +72 -0
- tigrbl/orm/tables/_base.py +8 -0
- tigrbl/orm/tables/audit.py +56 -0
- tigrbl/orm/tables/client.py +25 -0
- tigrbl/orm/tables/group.py +29 -0
- tigrbl/orm/tables/org.py +30 -0
- tigrbl/orm/tables/rbac.py +76 -0
- tigrbl/orm/tables/status.py +106 -0
- tigrbl/orm/tables/tenant.py +22 -0
- tigrbl/orm/tables/user.py +39 -0
- tigrbl/response/README.md +34 -0
- tigrbl/response/__init__.py +33 -0
- tigrbl/response/bind.py +12 -0
- tigrbl/response/decorators.py +37 -0
- tigrbl/response/resolver.py +83 -0
- tigrbl/response/shortcuts.py +171 -0
- tigrbl/response/types.py +49 -0
- tigrbl/rest/__init__.py +27 -0
- tigrbl/runtime/README.md +129 -0
- tigrbl/runtime/__init__.py +20 -0
- tigrbl/runtime/atoms/__init__.py +102 -0
- tigrbl/runtime/atoms/emit/__init__.py +42 -0
- tigrbl/runtime/atoms/emit/paired_post.py +158 -0
- tigrbl/runtime/atoms/emit/paired_pre.py +106 -0
- tigrbl/runtime/atoms/emit/readtime_alias.py +120 -0
- tigrbl/runtime/atoms/out/__init__.py +38 -0
- tigrbl/runtime/atoms/out/masking.py +135 -0
- tigrbl/runtime/atoms/refresh/__init__.py +38 -0
- tigrbl/runtime/atoms/refresh/demand.py +130 -0
- tigrbl/runtime/atoms/resolve/__init__.py +40 -0
- tigrbl/runtime/atoms/resolve/assemble.py +167 -0
- tigrbl/runtime/atoms/resolve/paired_gen.py +147 -0
- tigrbl/runtime/atoms/response/__init__.py +19 -0
- tigrbl/runtime/atoms/response/headers_from_payload.py +57 -0
- tigrbl/runtime/atoms/response/negotiate.py +30 -0
- tigrbl/runtime/atoms/response/negotiation.py +43 -0
- tigrbl/runtime/atoms/response/render.py +36 -0
- tigrbl/runtime/atoms/response/renderer.py +116 -0
- tigrbl/runtime/atoms/response/template.py +44 -0
- tigrbl/runtime/atoms/response/templates.py +88 -0
- tigrbl/runtime/atoms/schema/__init__.py +40 -0
- tigrbl/runtime/atoms/schema/collect_in.py +21 -0
- tigrbl/runtime/atoms/schema/collect_out.py +21 -0
- tigrbl/runtime/atoms/storage/__init__.py +38 -0
- tigrbl/runtime/atoms/storage/to_stored.py +167 -0
- tigrbl/runtime/atoms/wire/__init__.py +45 -0
- tigrbl/runtime/atoms/wire/build_in.py +166 -0
- tigrbl/runtime/atoms/wire/build_out.py +87 -0
- tigrbl/runtime/atoms/wire/dump.py +206 -0
- tigrbl/runtime/atoms/wire/validate_in.py +227 -0
- tigrbl/runtime/context.py +206 -0
- tigrbl/runtime/errors/__init__.py +61 -0
- tigrbl/runtime/errors/converters.py +214 -0
- tigrbl/runtime/errors/exceptions.py +124 -0
- tigrbl/runtime/errors/mappings.py +71 -0
- tigrbl/runtime/errors/utils.py +150 -0
- tigrbl/runtime/events.py +209 -0
- tigrbl/runtime/executor/__init__.py +6 -0
- tigrbl/runtime/executor/guards.py +132 -0
- tigrbl/runtime/executor/helpers.py +88 -0
- tigrbl/runtime/executor/invoke.py +150 -0
- tigrbl/runtime/executor/types.py +84 -0
- tigrbl/runtime/kernel.py +644 -0
- tigrbl/runtime/labels.py +353 -0
- tigrbl/runtime/opview.py +89 -0
- tigrbl/runtime/ordering.py +256 -0
- tigrbl/runtime/system.py +279 -0
- tigrbl/runtime/trace.py +330 -0
- tigrbl/schema/__init__.py +38 -0
- tigrbl/schema/_schema.py +27 -0
- tigrbl/schema/builder/__init__.py +17 -0
- tigrbl/schema/builder/build_schema.py +209 -0
- tigrbl/schema/builder/cache.py +24 -0
- tigrbl/schema/builder/compat.py +16 -0
- tigrbl/schema/builder/extras.py +85 -0
- tigrbl/schema/builder/helpers.py +51 -0
- tigrbl/schema/builder/list_params.py +117 -0
- tigrbl/schema/builder/strip_parent_fields.py +70 -0
- tigrbl/schema/collect.py +79 -0
- tigrbl/schema/decorators.py +68 -0
- tigrbl/schema/get_schema.py +86 -0
- tigrbl/schema/schema_spec.py +20 -0
- tigrbl/schema/shortcuts.py +42 -0
- tigrbl/schema/types.py +34 -0
- tigrbl/schema/utils.py +143 -0
- tigrbl/session/README.md +14 -0
- tigrbl/session/__init__.py +28 -0
- tigrbl/session/abc.py +76 -0
- tigrbl/session/base.py +151 -0
- tigrbl/session/decorators.py +43 -0
- tigrbl/session/default.py +118 -0
- tigrbl/session/shortcuts.py +50 -0
- tigrbl/session/spec.py +112 -0
- tigrbl/shortcuts.py +22 -0
- tigrbl/specs.py +44 -0
- tigrbl/system/__init__.py +13 -0
- tigrbl/system/diagnostics/__init__.py +24 -0
- tigrbl/system/diagnostics/compat.py +31 -0
- tigrbl/system/diagnostics/healthz.py +41 -0
- tigrbl/system/diagnostics/hookz.py +51 -0
- tigrbl/system/diagnostics/kernelz.py +20 -0
- tigrbl/system/diagnostics/methodz.py +43 -0
- tigrbl/system/diagnostics/router.py +73 -0
- tigrbl/system/diagnostics/utils.py +43 -0
- tigrbl/system/uvicorn.py +60 -0
- tigrbl/table/__init__.py +9 -0
- tigrbl/table/_base.py +260 -0
- tigrbl/table/_table.py +54 -0
- tigrbl/table/mro_collect.py +69 -0
- tigrbl/table/shortcuts.py +57 -0
- tigrbl/table/table_spec.py +28 -0
- tigrbl/transport/__init__.py +74 -0
- tigrbl/transport/jsonrpc/__init__.py +19 -0
- tigrbl/transport/jsonrpc/dispatcher.py +352 -0
- tigrbl/transport/jsonrpc/helpers.py +115 -0
- tigrbl/transport/jsonrpc/models.py +41 -0
- tigrbl/transport/rest/__init__.py +25 -0
- tigrbl/transport/rest/aggregator.py +132 -0
- tigrbl/types/__init__.py +170 -0
- tigrbl/types/allow_anon_provider.py +19 -0
- tigrbl/types/authn_abc.py +30 -0
- tigrbl/types/nested_path_provider.py +22 -0
- tigrbl/types/op.py +35 -0
- tigrbl/types/op_config_provider.py +17 -0
- tigrbl/types/op_verb_alias_provider.py +33 -0
- tigrbl/types/request_extras_provider.py +22 -0
- tigrbl/types/response_extras_provider.py +22 -0
- tigrbl/types/table_config_provider.py +13 -0
- tigrbl/types/uuid.py +55 -0
- tigrbl-0.3.0.dist-info/METADATA +516 -0
- tigrbl-0.3.0.dist-info/RECORD +266 -0
- {tigrbl-0.0.1.dev1.dist-info → tigrbl-0.3.0.dist-info}/WHEEL +1 -1
- tigrbl-0.3.0.dist-info/licenses/LICENSE +201 -0
- tigrbl/ExampleAgent.py +0 -1
- tigrbl-0.0.1.dev1.dist-info/METADATA +0 -18
- tigrbl-0.0.1.dev1.dist-info/RECORD +0 -5
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
# tigrbl/v3/ops/model_registry.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import replace
|
|
5
|
+
from threading import RLock
|
|
6
|
+
from typing import (
|
|
7
|
+
Any,
|
|
8
|
+
Callable,
|
|
9
|
+
Dict,
|
|
10
|
+
Iterable,
|
|
11
|
+
Iterator,
|
|
12
|
+
List,
|
|
13
|
+
Mapping,
|
|
14
|
+
Optional,
|
|
15
|
+
Tuple,
|
|
16
|
+
)
|
|
17
|
+
import weakref
|
|
18
|
+
|
|
19
|
+
from .types import OpSpec, TargetOp
|
|
20
|
+
|
|
21
|
+
# Listener signature: (registry, changed_keys) -> None
|
|
22
|
+
# where changed_keys is a set of (alias, target) tuples indicating what changed.
|
|
23
|
+
Listener = Callable[["OpspecRegistry", set[Tuple[str, TargetOp]]], None]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _spec_key(sp: OpSpec) -> Tuple[str, TargetOp]:
|
|
27
|
+
return (sp.alias, sp.target)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _ensure_table(sp: OpSpec, table: type) -> OpSpec:
|
|
31
|
+
return sp if sp.table is table else replace(sp, table=table)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _coerce_to_specs(value: Any, table: type) -> List[OpSpec]:
|
|
35
|
+
"""
|
|
36
|
+
Accept flexible inputs (for back-compat with v2):
|
|
37
|
+
• OpSpec
|
|
38
|
+
• Iterable[OpSpec]
|
|
39
|
+
• Mapping[str, dict] (alias -> kwargs for OpSpec)
|
|
40
|
+
• Iterable[Mapping[str, Any]] (each includes 'alias' & 'target')
|
|
41
|
+
"""
|
|
42
|
+
specs: List[OpSpec] = []
|
|
43
|
+
if value is None:
|
|
44
|
+
return specs
|
|
45
|
+
|
|
46
|
+
def _from_kwargs(kwargs: Mapping[str, Any]) -> Optional[OpSpec]:
|
|
47
|
+
if "alias" not in kwargs or "target" not in kwargs:
|
|
48
|
+
return None
|
|
49
|
+
try:
|
|
50
|
+
return OpSpec(table=table, **dict(kwargs)) # type: ignore[arg-type]
|
|
51
|
+
except TypeError:
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
if isinstance(value, OpSpec):
|
|
55
|
+
specs.append(_ensure_table(value, table))
|
|
56
|
+
elif isinstance(value, Mapping):
|
|
57
|
+
for maybe_alias, cfg in value.items():
|
|
58
|
+
if isinstance(cfg, OpSpec):
|
|
59
|
+
specs.append(_ensure_table(cfg, table))
|
|
60
|
+
elif isinstance(cfg, Mapping):
|
|
61
|
+
kw = dict(cfg)
|
|
62
|
+
kw.setdefault("alias", maybe_alias)
|
|
63
|
+
sp = _from_kwargs(kw)
|
|
64
|
+
if sp:
|
|
65
|
+
specs.append(sp)
|
|
66
|
+
elif isinstance(value, Iterable):
|
|
67
|
+
for item in value:
|
|
68
|
+
if isinstance(item, OpSpec):
|
|
69
|
+
specs.append(_ensure_table(item, table))
|
|
70
|
+
elif isinstance(item, Mapping):
|
|
71
|
+
sp = _from_kwargs(item)
|
|
72
|
+
if sp:
|
|
73
|
+
specs.append(sp)
|
|
74
|
+
return specs
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class OpspecRegistry:
|
|
78
|
+
"""
|
|
79
|
+
Per-model OpSpec registry with change notifications.
|
|
80
|
+
|
|
81
|
+
- Stores specs keyed by (alias, target).
|
|
82
|
+
- Adds/sets/removes specs and notifies listeners with the changed keys.
|
|
83
|
+
- Binder should call `subscribe(...)` to rebuild a model's namespaces when the
|
|
84
|
+
registry changes (partial rebuild is possible based on changed keys).
|
|
85
|
+
|
|
86
|
+
Thread-safe via an instance-level RLock.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
__slots__ = ("_table", "_items", "_lock", "_listeners", "_version")
|
|
90
|
+
|
|
91
|
+
def __init__(self, table: type) -> None:
|
|
92
|
+
self._table: type = table
|
|
93
|
+
self._items: Dict[Tuple[str, TargetOp], OpSpec] = {}
|
|
94
|
+
self._lock = RLock()
|
|
95
|
+
# store weakrefs to listener callables where possible; fallback to strong refs
|
|
96
|
+
self._listeners: List[
|
|
97
|
+
Callable[[OpspecRegistry, set[Tuple[str, TargetOp]]], None]
|
|
98
|
+
] = []
|
|
99
|
+
self._version: int = 0
|
|
100
|
+
|
|
101
|
+
# ---------------------------- Introspection ---------------------------- #
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def table(self) -> type:
|
|
105
|
+
return self._table
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def version(self) -> int:
|
|
109
|
+
return self._version
|
|
110
|
+
|
|
111
|
+
def keys(self) -> Iterator[Tuple[str, TargetOp]]:
|
|
112
|
+
with self._lock:
|
|
113
|
+
return iter(tuple(self._items.keys()))
|
|
114
|
+
|
|
115
|
+
def items(self) -> Iterator[Tuple[Tuple[str, TargetOp], OpSpec]]:
|
|
116
|
+
with self._lock:
|
|
117
|
+
return iter(tuple(self._items.items()))
|
|
118
|
+
|
|
119
|
+
def values(self) -> Iterator[OpSpec]:
|
|
120
|
+
with self._lock:
|
|
121
|
+
return iter(tuple(self._items.values()))
|
|
122
|
+
|
|
123
|
+
def get_all(self) -> Tuple[OpSpec, ...]:
|
|
124
|
+
"""Stable snapshot of all specs."""
|
|
125
|
+
with self._lock:
|
|
126
|
+
return tuple(self._items.values())
|
|
127
|
+
|
|
128
|
+
# ------------------------------ Listeners ------------------------------ #
|
|
129
|
+
|
|
130
|
+
def subscribe(self, fn: Listener) -> None:
|
|
131
|
+
"""
|
|
132
|
+
Register a listener to be called on changes.
|
|
133
|
+
NOTE: The listener should be idempotent. It receives (registry, changed_keys).
|
|
134
|
+
"""
|
|
135
|
+
with self._lock:
|
|
136
|
+
# Avoid duplicate subscriptions
|
|
137
|
+
if fn not in self._listeners:
|
|
138
|
+
self._listeners.append(fn)
|
|
139
|
+
|
|
140
|
+
def unsubscribe(self, fn: Listener) -> None:
|
|
141
|
+
with self._lock:
|
|
142
|
+
try:
|
|
143
|
+
self._listeners.remove(fn)
|
|
144
|
+
except ValueError:
|
|
145
|
+
pass
|
|
146
|
+
|
|
147
|
+
def _notify(self, changed: set[Tuple[str, TargetOp]]) -> None:
|
|
148
|
+
# Snapshot listeners to avoid mutation issues during callbacks
|
|
149
|
+
listeners: Tuple[Listener, ...]
|
|
150
|
+
with self._lock:
|
|
151
|
+
listeners = tuple(self._listeners)
|
|
152
|
+
for fn in listeners:
|
|
153
|
+
try:
|
|
154
|
+
fn(self, changed)
|
|
155
|
+
except Exception:
|
|
156
|
+
# Never let a listener error break the registry
|
|
157
|
+
pass
|
|
158
|
+
|
|
159
|
+
# ------------------------------- Mutators ------------------------------ #
|
|
160
|
+
|
|
161
|
+
def add(self, specs: Iterable[OpSpec] | OpSpec) -> set[Tuple[str, TargetOp]]:
|
|
162
|
+
"""
|
|
163
|
+
Add or overwrite one or more specs.
|
|
164
|
+
Returns the set of changed keys.
|
|
165
|
+
"""
|
|
166
|
+
if isinstance(specs, OpSpec):
|
|
167
|
+
specs = (specs,)
|
|
168
|
+
|
|
169
|
+
changed: set[Tuple[str, TargetOp]] = set()
|
|
170
|
+
with self._lock:
|
|
171
|
+
for sp in specs:
|
|
172
|
+
sp = _ensure_table(sp, self._table)
|
|
173
|
+
k = _spec_key(sp)
|
|
174
|
+
if self._items.get(k) is sp:
|
|
175
|
+
continue # exact object already present
|
|
176
|
+
self._items[k] = sp
|
|
177
|
+
changed.add(k)
|
|
178
|
+
if changed:
|
|
179
|
+
self._version += 1
|
|
180
|
+
if changed:
|
|
181
|
+
self._notify(changed)
|
|
182
|
+
return changed
|
|
183
|
+
|
|
184
|
+
def set(self, specs: Iterable[OpSpec]) -> set[Tuple[str, TargetOp]]:
|
|
185
|
+
"""
|
|
186
|
+
Replace all specs with the provided iterable.
|
|
187
|
+
Returns the set of changed keys (union of removed + added/updated).
|
|
188
|
+
"""
|
|
189
|
+
new_map: Dict[Tuple[str, TargetOp], OpSpec] = {}
|
|
190
|
+
for sp in specs:
|
|
191
|
+
sp = _ensure_table(sp, self._table)
|
|
192
|
+
new_map[_spec_key(sp)] = sp
|
|
193
|
+
|
|
194
|
+
with self._lock:
|
|
195
|
+
old_keys = set(self._items.keys())
|
|
196
|
+
new_keys = set(new_map.keys())
|
|
197
|
+
|
|
198
|
+
removed = old_keys - new_keys
|
|
199
|
+
added_or_updated = {
|
|
200
|
+
k for k in new_keys if self._items.get(k) is not new_map[k]
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
changed = removed | added_or_updated
|
|
204
|
+
self._items = new_map
|
|
205
|
+
if changed:
|
|
206
|
+
self._version += 1
|
|
207
|
+
|
|
208
|
+
if changed:
|
|
209
|
+
self._notify(changed)
|
|
210
|
+
return changed
|
|
211
|
+
|
|
212
|
+
def remove(
|
|
213
|
+
self, alias: str, target: TargetOp | None = None
|
|
214
|
+
) -> set[Tuple[str, TargetOp]]:
|
|
215
|
+
"""
|
|
216
|
+
Remove specs by alias (optionally constrain to a specific target).
|
|
217
|
+
Returns the set of removed keys.
|
|
218
|
+
"""
|
|
219
|
+
removed: set[Tuple[str, TargetOp]] = set()
|
|
220
|
+
with self._lock:
|
|
221
|
+
if target is None:
|
|
222
|
+
# remove all targets under this alias
|
|
223
|
+
for k in list(self._items.keys()):
|
|
224
|
+
if k[0] == alias:
|
|
225
|
+
self._items.pop(k, None)
|
|
226
|
+
removed.add(k)
|
|
227
|
+
else:
|
|
228
|
+
k = (alias, target)
|
|
229
|
+
if k in self._items:
|
|
230
|
+
self._items.pop(k, None)
|
|
231
|
+
removed.add(k)
|
|
232
|
+
|
|
233
|
+
if removed:
|
|
234
|
+
self._version += 1
|
|
235
|
+
|
|
236
|
+
if removed:
|
|
237
|
+
self._notify(removed)
|
|
238
|
+
return removed
|
|
239
|
+
|
|
240
|
+
def clear(self) -> None:
|
|
241
|
+
with self._lock:
|
|
242
|
+
if not self._items:
|
|
243
|
+
return
|
|
244
|
+
self._items.clear()
|
|
245
|
+
self._version += 1
|
|
246
|
+
self._notify(set())
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
# ------------------------------------------------------------------------------
|
|
250
|
+
# Per-model registry storage (weak keys so classes can be GC'd)
|
|
251
|
+
# ------------------------------------------------------------------------------
|
|
252
|
+
|
|
253
|
+
_REGISTRIES: "weakref.WeakKeyDictionary[type, OpspecRegistry]" = (
|
|
254
|
+
weakref.WeakKeyDictionary()
|
|
255
|
+
)
|
|
256
|
+
_REG_LOCK = RLock()
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def get_registry(table: type) -> OpspecRegistry:
|
|
260
|
+
with _REG_LOCK:
|
|
261
|
+
reg = _REGISTRIES.get(table)
|
|
262
|
+
if reg is None:
|
|
263
|
+
reg = OpspecRegistry(table)
|
|
264
|
+
_REGISTRIES[table] = reg
|
|
265
|
+
return reg
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
# ------------------------------------------------------------------------------
|
|
269
|
+
# Back-compat helpers (v2-style imperative API)
|
|
270
|
+
# ------------------------------------------------------------------------------
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def register_ops(table: type, specs: Any) -> set[Tuple[str, TargetOp]]:
|
|
274
|
+
"""
|
|
275
|
+
Imperative registration (back-compat).
|
|
276
|
+
Accepts OpSpec, iterable of OpSpec, mapping forms, etc.
|
|
277
|
+
Triggers listeners (i.e., binder refresh) on change.
|
|
278
|
+
"""
|
|
279
|
+
reg = get_registry(table)
|
|
280
|
+
coerced = _coerce_to_specs(specs, table)
|
|
281
|
+
return reg.add(coerced)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def get_registered_ops(table: type) -> Tuple[OpSpec, ...]:
|
|
285
|
+
"""
|
|
286
|
+
Back-compat reader used by the collector.
|
|
287
|
+
"""
|
|
288
|
+
return get_registry(table).get_all()
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def clear_registry(table: type) -> None:
|
|
292
|
+
get_registry(table).clear()
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
__all__ = [
|
|
296
|
+
"OpspecRegistry",
|
|
297
|
+
"get_registry",
|
|
298
|
+
"register_ops",
|
|
299
|
+
"get_registered_ops",
|
|
300
|
+
"clear_registry",
|
|
301
|
+
]
|
tigrbl/op/mro_collect.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from functools import lru_cache
|
|
5
|
+
from typing import Any, Callable, Dict
|
|
6
|
+
|
|
7
|
+
from .types import OpSpec
|
|
8
|
+
from .decorators import _maybe_await, _OpDecl, _infer_arity, _normalize_persist, _unwrap
|
|
9
|
+
from ..runtime.executor import _Ctx
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger("uvicorn")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _merge_mro_dict(cls: type, attr: str) -> Dict[str, Any]:
|
|
15
|
+
merged: Dict[str, Any] = {}
|
|
16
|
+
for base in reversed(cls.__mro__):
|
|
17
|
+
merged.update(getattr(base, attr, {}) or {})
|
|
18
|
+
return merged
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@lru_cache(maxsize=None)
|
|
22
|
+
def mro_alias_map_for(table: type) -> Dict[str, str]:
|
|
23
|
+
"""Collect alias overrides across the table's MRO."""
|
|
24
|
+
return _merge_mro_dict(table, "__tigrbl_aliases__")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _wrap_ctx_core(table: type, func: Callable[..., Any]) -> Callable[..., Any]:
|
|
28
|
+
"""Adapt `(cls, ctx)` op to `(p, *, db, request, ctx)` handler signature."""
|
|
29
|
+
|
|
30
|
+
async def core(p=None, *, db=None, request=None, ctx: Dict[str, Any] | None = None):
|
|
31
|
+
ctx = _Ctx.ensure(request=request, db=db, seed=ctx)
|
|
32
|
+
if p is not None:
|
|
33
|
+
ctx["payload"] = p
|
|
34
|
+
bound = func.__get__(table, table)
|
|
35
|
+
res = await _maybe_await(bound(ctx))
|
|
36
|
+
return res if res is not None else ctx.get("result")
|
|
37
|
+
|
|
38
|
+
core.__name__ = getattr(func, "__name__", "core")
|
|
39
|
+
core.__qualname__ = getattr(func, "__qualname__", core.__name__)
|
|
40
|
+
return core
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@lru_cache(maxsize=None)
|
|
44
|
+
def mro_collect_decorated_ops(table: type) -> list[OpSpec]:
|
|
45
|
+
"""Collect ctx-only op declarations across the table's MRO."""
|
|
46
|
+
|
|
47
|
+
logger.info("Collecting decorated ops for %s", table.__name__)
|
|
48
|
+
out: list[OpSpec] = []
|
|
49
|
+
seen: set[str] = set()
|
|
50
|
+
|
|
51
|
+
for base in table.__mro__:
|
|
52
|
+
for name, attr in vars(base).items():
|
|
53
|
+
if name in seen:
|
|
54
|
+
continue
|
|
55
|
+
func = _unwrap(attr)
|
|
56
|
+
decl: _OpDecl | None = getattr(func, "__tigrbl_op_decl__", None)
|
|
57
|
+
if not decl:
|
|
58
|
+
continue
|
|
59
|
+
|
|
60
|
+
target = decl.target or "custom"
|
|
61
|
+
arity = decl.arity or _infer_arity(target)
|
|
62
|
+
persist = _normalize_persist(decl.persist)
|
|
63
|
+
alias = decl.alias or name
|
|
64
|
+
|
|
65
|
+
expose_kwargs: dict[str, Any] = {}
|
|
66
|
+
extra: dict[str, Any] = {}
|
|
67
|
+
if decl.rest is not None:
|
|
68
|
+
expose_kwargs["expose_routes"] = bool(decl.rest)
|
|
69
|
+
elif alias != target and target in {
|
|
70
|
+
"read",
|
|
71
|
+
"update",
|
|
72
|
+
"delete",
|
|
73
|
+
"list",
|
|
74
|
+
"clear",
|
|
75
|
+
}:
|
|
76
|
+
expose_kwargs["expose_routes"] = False
|
|
77
|
+
|
|
78
|
+
spec = OpSpec(
|
|
79
|
+
table=table,
|
|
80
|
+
alias=alias,
|
|
81
|
+
target=target,
|
|
82
|
+
arity=arity,
|
|
83
|
+
persist=persist,
|
|
84
|
+
handler=_wrap_ctx_core(table, func),
|
|
85
|
+
request_model=decl.request_schema,
|
|
86
|
+
response_model=decl.response_schema,
|
|
87
|
+
hooks=(),
|
|
88
|
+
status_code=decl.status_code,
|
|
89
|
+
extra=extra,
|
|
90
|
+
**expose_kwargs,
|
|
91
|
+
)
|
|
92
|
+
out.append(spec)
|
|
93
|
+
seen.add(name)
|
|
94
|
+
|
|
95
|
+
logger.debug("Collected %d ops for %s", len(out), table.__name__)
|
|
96
|
+
return out
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
__all__ = ["mro_alias_map_for", "mro_collect_decorated_ops"]
|
tigrbl/op/resolver.py
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import replace
|
|
4
|
+
import logging
|
|
5
|
+
import re
|
|
6
|
+
from typing import Any, Dict, Iterable, List, Mapping, Optional, Sequence, Tuple
|
|
7
|
+
|
|
8
|
+
from .types import OpSpec, TargetOp
|
|
9
|
+
from ..config.constants import TIGRBL_OPS_ATTR
|
|
10
|
+
from .canonical import should_wire_canonical
|
|
11
|
+
from .mro_collect import mro_alias_map_for
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
# Per-model registry (observable, triggers rebind elsewhere)
|
|
15
|
+
from .model_registry import get_registered_ops # type: ignore
|
|
16
|
+
except Exception: # pragma: no cover
|
|
17
|
+
|
|
18
|
+
def get_registered_ops(model: type) -> Sequence[OpSpec]: # shim
|
|
19
|
+
return ()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger("uvicorn")
|
|
23
|
+
|
|
24
|
+
_ALIAS_RE = re.compile(r"^[a-z][a-z0-9_]*$")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _ensure_spec_table(spec: OpSpec, table: type) -> OpSpec:
|
|
28
|
+
if spec.table is table:
|
|
29
|
+
return spec
|
|
30
|
+
return replace(spec, table=table)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _as_specs(value: Any, table: type) -> List[OpSpec]:
|
|
34
|
+
"""Normalize various `__tigrbl_ops__` shapes to a list of OpSpec."""
|
|
35
|
+
specs: List[OpSpec] = []
|
|
36
|
+
if value is None:
|
|
37
|
+
return specs
|
|
38
|
+
|
|
39
|
+
def _from_kwargs(kwargs: Mapping[str, Any]) -> Optional[OpSpec]:
|
|
40
|
+
if "alias" not in kwargs or "target" not in kwargs:
|
|
41
|
+
return None
|
|
42
|
+
try:
|
|
43
|
+
spec = OpSpec(table=table, hooks=(), extra={}, **kwargs)
|
|
44
|
+
return spec
|
|
45
|
+
except Exception: # pragma: no cover - defensive
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
if isinstance(value, OpSpec):
|
|
49
|
+
specs.append(_ensure_spec_table(value, table))
|
|
50
|
+
elif isinstance(value, Mapping):
|
|
51
|
+
for alias, cfg in value.items():
|
|
52
|
+
if isinstance(cfg, Mapping):
|
|
53
|
+
spec = _from_kwargs({"alias": alias, **cfg})
|
|
54
|
+
if spec:
|
|
55
|
+
specs.append(spec)
|
|
56
|
+
elif isinstance(value, Iterable):
|
|
57
|
+
for item in value:
|
|
58
|
+
if isinstance(item, OpSpec):
|
|
59
|
+
specs.append(_ensure_spec_table(item, table))
|
|
60
|
+
elif isinstance(item, Mapping):
|
|
61
|
+
spec = _from_kwargs(item)
|
|
62
|
+
if spec:
|
|
63
|
+
specs.append(spec)
|
|
64
|
+
else:
|
|
65
|
+
spec = _from_kwargs(
|
|
66
|
+
{
|
|
67
|
+
"alias": getattr(value, "alias", None),
|
|
68
|
+
"target": getattr(value, "target", None),
|
|
69
|
+
}
|
|
70
|
+
)
|
|
71
|
+
if spec:
|
|
72
|
+
specs.append(spec)
|
|
73
|
+
return specs
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _generate_canonical(table: type) -> List[OpSpec]:
|
|
77
|
+
"""Generate canonical CRUD specs based on model attributes."""
|
|
78
|
+
specs: List[OpSpec] = []
|
|
79
|
+
targets: List[Tuple[str, TargetOp]] = [
|
|
80
|
+
("create", "create"),
|
|
81
|
+
("read", "read"),
|
|
82
|
+
("update", "update"),
|
|
83
|
+
# Include canonical "replace" so RPC callers get full CRUD semantics
|
|
84
|
+
# without opting into the Replaceable mixin.
|
|
85
|
+
("replace", "replace"),
|
|
86
|
+
("merge", "merge"),
|
|
87
|
+
("delete", "delete"),
|
|
88
|
+
("list", "list"),
|
|
89
|
+
("clear", "clear"),
|
|
90
|
+
("bulk_create", "bulk_create"),
|
|
91
|
+
("bulk_update", "bulk_update"),
|
|
92
|
+
("bulk_replace", "bulk_replace"),
|
|
93
|
+
("bulk_merge", "bulk_merge"),
|
|
94
|
+
("bulk_delete", "bulk_delete"),
|
|
95
|
+
]
|
|
96
|
+
collection_targets = {
|
|
97
|
+
"create",
|
|
98
|
+
"list",
|
|
99
|
+
"clear",
|
|
100
|
+
"bulk_create",
|
|
101
|
+
"bulk_update",
|
|
102
|
+
"bulk_replace",
|
|
103
|
+
"bulk_merge",
|
|
104
|
+
"bulk_delete",
|
|
105
|
+
}
|
|
106
|
+
for alias, target in targets:
|
|
107
|
+
if not should_wire_canonical(table, target):
|
|
108
|
+
continue
|
|
109
|
+
specs.append(
|
|
110
|
+
OpSpec(
|
|
111
|
+
table=table,
|
|
112
|
+
alias=alias,
|
|
113
|
+
target=target,
|
|
114
|
+
arity="collection" if target in collection_targets else "member",
|
|
115
|
+
persist="default",
|
|
116
|
+
handler=None,
|
|
117
|
+
request_model=None,
|
|
118
|
+
response_model=None,
|
|
119
|
+
hooks=(),
|
|
120
|
+
status_code=None,
|
|
121
|
+
extra={},
|
|
122
|
+
)
|
|
123
|
+
)
|
|
124
|
+
return specs
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _collect_class_declared(model: type) -> List[OpSpec]:
|
|
128
|
+
out: List[OpSpec] = []
|
|
129
|
+
raw = getattr(model, TIGRBL_OPS_ATTR, None)
|
|
130
|
+
if isinstance(raw, Mapping) or isinstance(raw, Iterable):
|
|
131
|
+
out.extend(_as_specs(raw, model))
|
|
132
|
+
return out
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _collect_registry(model: type) -> List[OpSpec]:
|
|
136
|
+
return list(get_registered_ops(model))
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _dedupe(
|
|
140
|
+
existing: Dict[Tuple[str, str], OpSpec], incoming: Iterable[OpSpec]
|
|
141
|
+
) -> None:
|
|
142
|
+
for sp in incoming:
|
|
143
|
+
if not isinstance(sp, OpSpec):
|
|
144
|
+
continue
|
|
145
|
+
if not sp.alias or not sp.target:
|
|
146
|
+
continue
|
|
147
|
+
existing[(sp.alias, sp.target)] = sp
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _apply_alias_ctx_to_canon(specs: List[OpSpec], model: type) -> List[OpSpec]:
|
|
151
|
+
aliases = mro_alias_map_for(model)
|
|
152
|
+
overrides: Mapping[str, Mapping[str, Any]] = (
|
|
153
|
+
getattr(model, "__tigrbl_alias_overrides__", {}) or {}
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
if not aliases and not overrides:
|
|
157
|
+
return specs
|
|
158
|
+
|
|
159
|
+
out: List[OpSpec] = []
|
|
160
|
+
for sp in specs:
|
|
161
|
+
canon = sp.target
|
|
162
|
+
new_alias = aliases.get(canon, sp.alias)
|
|
163
|
+
mutated = sp
|
|
164
|
+
if new_alias != sp.alias:
|
|
165
|
+
if not isinstance(new_alias, str) or not _ALIAS_RE.match(new_alias):
|
|
166
|
+
logger.warning(
|
|
167
|
+
"Invalid alias %r for verb %r on %s; keeping %r",
|
|
168
|
+
new_alias,
|
|
169
|
+
sp.target,
|
|
170
|
+
model.__name__,
|
|
171
|
+
sp.alias,
|
|
172
|
+
)
|
|
173
|
+
else:
|
|
174
|
+
mutated = replace(mutated, alias=new_alias, path_suffix="")
|
|
175
|
+
|
|
176
|
+
ov = overrides.get(canon)
|
|
177
|
+
if ov:
|
|
178
|
+
kwargs = {}
|
|
179
|
+
if "request_schema" in ov:
|
|
180
|
+
kwargs["request_model"] = ov["request_schema"]
|
|
181
|
+
if "response_schema" in ov:
|
|
182
|
+
kwargs["response_model"] = ov["response_schema"]
|
|
183
|
+
if "persist" in ov:
|
|
184
|
+
kwargs["persist"] = ov["persist"]
|
|
185
|
+
if "arity" in ov:
|
|
186
|
+
kwargs["arity"] = ov["arity"]
|
|
187
|
+
if "rest" in ov:
|
|
188
|
+
kwargs["expose_routes"] = bool(ov["rest"])
|
|
189
|
+
if "engine" in ov:
|
|
190
|
+
kwargs["engine"] = ov["engine"]
|
|
191
|
+
if kwargs:
|
|
192
|
+
mutated = replace(mutated, **kwargs)
|
|
193
|
+
out.append(mutated)
|
|
194
|
+
return out
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def resolve(model: type) -> List[OpSpec]:
|
|
198
|
+
canon = _generate_canonical(model)
|
|
199
|
+
canon = _apply_alias_ctx_to_canon(canon, model)
|
|
200
|
+
|
|
201
|
+
class_specs = _collect_class_declared(model)
|
|
202
|
+
reg_specs = _collect_registry(model)
|
|
203
|
+
|
|
204
|
+
merged: Dict[Tuple[str, str], OpSpec] = {}
|
|
205
|
+
_dedupe(merged, canon)
|
|
206
|
+
_dedupe(merged, class_specs)
|
|
207
|
+
_dedupe(merged, reg_specs)
|
|
208
|
+
|
|
209
|
+
specs = list(merged.values())
|
|
210
|
+
specs = [_ensure_spec_table(sp, model) for sp in specs]
|
|
211
|
+
|
|
212
|
+
logger.debug("ops.resolver.resolve(%s): %d specs", model.__name__, len(specs))
|
|
213
|
+
return specs
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
__all__ = ["resolve"]
|