tigrbl 0.0.1.dev1__py3-none-any.whl → 0.3.0.dev3__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 +72 -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 +286 -0
- tigrbl/app/__init__.py +0 -0
- tigrbl/app/_app.py +61 -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 +314 -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 +265 -0
- tigrbl/bindings/rest/common.py +116 -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/member.py +367 -0
- tigrbl/bindings/rest/router.py +292 -0
- tigrbl/bindings/rest/routing.py +133 -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 +133 -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 +26 -0
- tigrbl/engine/_engine.py +130 -0
- tigrbl/engine/bind.py +33 -0
- tigrbl/engine/builders.py +236 -0
- tigrbl/engine/collect.py +111 -0
- tigrbl/engine/decorators.py +108 -0
- tigrbl/engine/engine_spec.py +261 -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 +111 -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 +144 -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 +17 -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 +628 -0
- tigrbl/runtime/labels.py +353 -0
- tigrbl/runtime/opview.py +87 -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 +55 -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/shortcuts.py +22 -0
- tigrbl/specs.py +44 -0
- tigrbl/system/__init__.py +12 -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/table/__init__.py +9 -0
- tigrbl/table/_base.py +237 -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 +174 -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-0.3.0.dev3.dist-info/LICENSE +201 -0
- tigrbl-0.3.0.dev3.dist-info/METADATA +501 -0
- tigrbl-0.3.0.dev3.dist-info/RECORD +249 -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
- {tigrbl-0.0.1.dev1.dist-info → tigrbl-0.3.0.dev3.dist-info}/WHEEL +0 -0
tigrbl/bindings/model.py
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# tigrbl/v3/bindings/model.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
from dataclasses import replace
|
|
6
|
+
from types import SimpleNamespace
|
|
7
|
+
from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Tuple
|
|
8
|
+
|
|
9
|
+
from ..op import OpSpec
|
|
10
|
+
from ..op.mro_collect import mro_alias_map_for, mro_collect_decorated_ops
|
|
11
|
+
from ..op import resolve as resolve_ops
|
|
12
|
+
from ..op.types import PHASES # phase allowlist for hook merges
|
|
13
|
+
|
|
14
|
+
# Ctx-only decorators integration
|
|
15
|
+
from ..hook.mro_collect import mro_collect_decorated_hooks
|
|
16
|
+
|
|
17
|
+
# Sub-binders (implemented elsewhere)
|
|
18
|
+
from . import (
|
|
19
|
+
schemas as _schemas_binding,
|
|
20
|
+
) # build_and_attach(model, specs, only_keys=None) -> None
|
|
21
|
+
from . import (
|
|
22
|
+
hooks as _hooks_binding,
|
|
23
|
+
) # normalize_and_attach(model, specs, only_keys=None) -> None
|
|
24
|
+
from . import (
|
|
25
|
+
handlers as _handlers_binding,
|
|
26
|
+
) # build_and_attach(model, specs, only_keys=None) -> None
|
|
27
|
+
from . import (
|
|
28
|
+
rpc as _rpc_binding,
|
|
29
|
+
) # register_and_attach(model, specs, only_keys=None) -> None
|
|
30
|
+
from . import (
|
|
31
|
+
rest as _rest_binding,
|
|
32
|
+
) # build_router_and_attach(model, specs, api=None, only_keys=None) -> None
|
|
33
|
+
from . import columns as _columns_binding
|
|
34
|
+
from .model_helpers import (
|
|
35
|
+
_Key,
|
|
36
|
+
_drop_old_entries,
|
|
37
|
+
_ensure_model_namespaces,
|
|
38
|
+
_filter_specs,
|
|
39
|
+
_index_specs,
|
|
40
|
+
_key,
|
|
41
|
+
)
|
|
42
|
+
from .model_registry import _ensure_op_ctx_attach_hook, _ensure_registry_listener
|
|
43
|
+
|
|
44
|
+
logger = logging.getLogger("uvicorn")
|
|
45
|
+
logger.debug("Loaded module v3/bindings/model")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _dedupe_by_name(funcs: Iterable[Callable[..., Any]]) -> List[Callable[..., Any]]:
|
|
49
|
+
"""Return callables deduplicated by qualified name preserving last occurrence."""
|
|
50
|
+
return list({getattr(fn, "__qualname__", str(fn)): fn for fn in funcs}.values())
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# ───────────────────────────────────────────────────────────────────────────────
|
|
54
|
+
# Public API
|
|
55
|
+
# ───────────────────────────────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def bind(
|
|
59
|
+
model: type, *, api: Any | None = None, only_keys: Optional[Set[_Key]] = None
|
|
60
|
+
) -> Tuple[OpSpec, ...]:
|
|
61
|
+
"""
|
|
62
|
+
Build (or refresh) all Tigrbl namespaces on the model class.
|
|
63
|
+
|
|
64
|
+
Steps:
|
|
65
|
+
1) Ensure model namespaces exist.
|
|
66
|
+
2) Resolve canonical OpSpecs and merge ctx-only ops (@op_ctx). If both define
|
|
67
|
+
the same (alias,target), the ctx-only op overrides.
|
|
68
|
+
3) Optionally drop old entries for targeted (alias,target) keys.
|
|
69
|
+
4) Merge ctx-only hooks (@hook_ctx) into model.__tigrbl_hooks__ (alias-aware),
|
|
70
|
+
filtering to known PHASES.
|
|
71
|
+
5) Rebuild & attach (scoped by only_keys when provided):
|
|
72
|
+
• schemas (in_/out)
|
|
73
|
+
• hooks (phase chains, with START_TX/END_TX or mark_skip_persist defaults)
|
|
74
|
+
• handlers (raw & handler entrypoint for HANDLER)
|
|
75
|
+
• rpc (model.rpc.<alias>)
|
|
76
|
+
• rest (model.rest.router)
|
|
77
|
+
6) Index ops under model.ops.{all, by_key, by_alias}
|
|
78
|
+
7) Install a registry listener (once) so imperative updates rebind automatically.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
tuple of OpSpec (the effective set).
|
|
82
|
+
"""
|
|
83
|
+
_ensure_model_namespaces(model)
|
|
84
|
+
|
|
85
|
+
# 0) Columns first
|
|
86
|
+
_columns_binding.build_and_attach(model)
|
|
87
|
+
|
|
88
|
+
# 1) Resolve canonical specs (source of truth)
|
|
89
|
+
base_specs: List[OpSpec] = list(resolve_ops(model))
|
|
90
|
+
|
|
91
|
+
# 2) Add ctx-only ops discovered via decorators (tables + mixins)
|
|
92
|
+
ctx_specs: List[OpSpec] = list(mro_collect_decorated_ops(model))
|
|
93
|
+
|
|
94
|
+
# 2a) Inherit canonical schemas for aliased ops lacking explicit schemas
|
|
95
|
+
base_by_target: Dict[str, OpSpec] = {sp.target: sp for sp in base_specs}
|
|
96
|
+
fixed_ctx_specs: List[OpSpec] = []
|
|
97
|
+
for sp in ctx_specs:
|
|
98
|
+
if (
|
|
99
|
+
sp.alias != sp.target
|
|
100
|
+
and sp.target != "custom"
|
|
101
|
+
and sp.request_model is None
|
|
102
|
+
and sp.response_model is None
|
|
103
|
+
):
|
|
104
|
+
base = base_by_target.get(sp.target)
|
|
105
|
+
if base:
|
|
106
|
+
sp = replace(
|
|
107
|
+
sp,
|
|
108
|
+
request_model=base.request_model,
|
|
109
|
+
response_model=base.response_model,
|
|
110
|
+
)
|
|
111
|
+
fixed_ctx_specs.append(sp)
|
|
112
|
+
ctx_specs = fixed_ctx_specs
|
|
113
|
+
|
|
114
|
+
# 2b) De-dupe by (alias,target) with ctx-only overriding canonical/defaults
|
|
115
|
+
merged_by_key: Dict[_Key, OpSpec] = {}
|
|
116
|
+
for sp in base_specs:
|
|
117
|
+
merged_by_key[_key(sp)] = sp
|
|
118
|
+
for sp in ctx_specs:
|
|
119
|
+
merged_by_key[_key(sp)] = sp
|
|
120
|
+
|
|
121
|
+
all_merged_specs: List[OpSpec] = list(merged_by_key.values())
|
|
122
|
+
|
|
123
|
+
# 3) Targeted rebuild support: drop old entries and restrict working set if requested
|
|
124
|
+
_drop_old_entries(model, keys=only_keys)
|
|
125
|
+
specs: List[OpSpec] = _filter_specs(all_merged_specs, only_keys)
|
|
126
|
+
|
|
127
|
+
# 4) Merge ctx-only hooks (alias-aware) BEFORE normalization/attachment
|
|
128
|
+
visible_aliases = (
|
|
129
|
+
{sp.alias for sp in specs} if specs else {sp.alias for sp in all_merged_specs}
|
|
130
|
+
)
|
|
131
|
+
ctx_hooks = mro_collect_decorated_hooks(model, visible_aliases=visible_aliases)
|
|
132
|
+
base_hooks = getattr(model, "__tigrbl_hooks__", {}) or {}
|
|
133
|
+
|
|
134
|
+
# Coerce any pre-existing phase sequences to mutable lists and deduplicate
|
|
135
|
+
for phases in base_hooks.values():
|
|
136
|
+
for phase, fns in list(phases.items()):
|
|
137
|
+
phases[phase] = _dedupe_by_name(fns if isinstance(fns, list) else list(fns))
|
|
138
|
+
|
|
139
|
+
for alias, phases in ctx_hooks.items():
|
|
140
|
+
per = base_hooks.setdefault(alias, {})
|
|
141
|
+
for phase, fns in phases.items():
|
|
142
|
+
if phase in PHASES:
|
|
143
|
+
existing = per.setdefault(phase, [])
|
|
144
|
+
per[phase] = _dedupe_by_name([*existing, *fns])
|
|
145
|
+
|
|
146
|
+
setattr(model, "__tigrbl_hooks__", base_hooks)
|
|
147
|
+
|
|
148
|
+
# 5) Attach schemas, hooks, handlers, rpc, router (sub-binders honor only_keys)
|
|
149
|
+
_schemas_binding.build_and_attach(model, specs, only_keys=only_keys)
|
|
150
|
+
_hooks_binding.normalize_and_attach(model, specs, only_keys=only_keys)
|
|
151
|
+
_handlers_binding.build_and_attach(model, specs, only_keys=only_keys)
|
|
152
|
+
_rpc_binding.register_and_attach(model, specs, only_keys=only_keys)
|
|
153
|
+
_rest_binding.build_router_and_attach(model, specs, api=api, only_keys=only_keys)
|
|
154
|
+
|
|
155
|
+
# 6) Index on the model (always overwrite with fresh views)
|
|
156
|
+
all_specs, by_key, by_alias = _index_specs(all_merged_specs)
|
|
157
|
+
model.ops = SimpleNamespace(all=all_specs, by_key=by_key, by_alias=by_alias)
|
|
158
|
+
# Maintain `.opspecs` alias for backward compatibility
|
|
159
|
+
model.opspecs = model.ops
|
|
160
|
+
|
|
161
|
+
# (Optional) expose resolved alias map for diagnostics
|
|
162
|
+
try:
|
|
163
|
+
model.alias_map = mro_alias_map_for(model)
|
|
164
|
+
except Exception: # defensive
|
|
165
|
+
pass
|
|
166
|
+
|
|
167
|
+
# 7) Ensure we have a registry listener to refresh on changes
|
|
168
|
+
_ensure_registry_listener(model)
|
|
169
|
+
_ensure_op_ctx_attach_hook(model)
|
|
170
|
+
setattr(model, "__tigrbl_op_ctx_watch__", True)
|
|
171
|
+
|
|
172
|
+
logger.debug(
|
|
173
|
+
"tigrbl.bindings.model.bind(%s): %d ops bound (visible=%d)",
|
|
174
|
+
model.__name__,
|
|
175
|
+
len(all_specs),
|
|
176
|
+
len(specs),
|
|
177
|
+
)
|
|
178
|
+
return all_specs
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def rebind(
|
|
182
|
+
model: type,
|
|
183
|
+
*,
|
|
184
|
+
api: Any | None = None,
|
|
185
|
+
changed_keys: Optional[Set[_Key]] = None,
|
|
186
|
+
) -> Tuple[OpSpec, ...]:
|
|
187
|
+
"""
|
|
188
|
+
Public helper to trigger a rebind for the model. If `changed_keys` is provided,
|
|
189
|
+
we attempt a targeted refresh; otherwise we rebuild everything.
|
|
190
|
+
"""
|
|
191
|
+
return bind(model, api=api, only_keys=changed_keys)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
__all__ = ["bind", "rebind"]
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# tigrbl/v3/bindings/model_helpers.py
|
|
2
|
+
"""Internal helpers for the model bindings."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
from types import SimpleNamespace
|
|
8
|
+
from typing import Dict, List, Optional, Sequence, Set, Tuple
|
|
9
|
+
|
|
10
|
+
from ..op import OpSpec
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger("uvicorn")
|
|
13
|
+
logger.debug("Loaded module v3/bindings/model_helpers")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
_Key = Tuple[str, str] # (alias, target)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _key(sp: OpSpec) -> _Key:
|
|
20
|
+
return (sp.alias, sp.target)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _ensure_model_namespaces(model: type) -> None:
|
|
24
|
+
"""Create top-level namespaces on the model class if missing."""
|
|
25
|
+
|
|
26
|
+
# op indexes & metadata
|
|
27
|
+
if not hasattr(model, "ops"):
|
|
28
|
+
if hasattr(model, "opspecs"):
|
|
29
|
+
model.ops = model.opspecs
|
|
30
|
+
else:
|
|
31
|
+
model.ops = SimpleNamespace(all=(), by_key={}, by_alias={})
|
|
32
|
+
# Backwards compatibility: older code may still expect `model.opspecs`
|
|
33
|
+
model.opspecs = model.ops
|
|
34
|
+
# pydantic schemas: .<alias>.in_ / .<alias>.out
|
|
35
|
+
if not hasattr(model, "schemas"):
|
|
36
|
+
model.schemas = SimpleNamespace()
|
|
37
|
+
# hooks: phase chains & raw hook descriptors if you want to expose them
|
|
38
|
+
if not hasattr(model, "hooks"):
|
|
39
|
+
model.hooks = SimpleNamespace()
|
|
40
|
+
# handlers: .<alias>.raw (core/custom), .<alias>.handler (HANDLER chain entry point)
|
|
41
|
+
if not hasattr(model, "handlers"):
|
|
42
|
+
model.handlers = SimpleNamespace()
|
|
43
|
+
# rpc: callables to be registered/mounted elsewhere as JSON-RPC methods
|
|
44
|
+
if not hasattr(model, "rpc"):
|
|
45
|
+
model.rpc = SimpleNamespace()
|
|
46
|
+
# rest: .router (FastAPI Router or compatible) – built in rest binding
|
|
47
|
+
if not hasattr(model, "rest"):
|
|
48
|
+
model.rest = SimpleNamespace(router=None)
|
|
49
|
+
# basic table metadata for convenience (introspective only; NEVER used for HTTP paths)
|
|
50
|
+
if not hasattr(model, "columns"):
|
|
51
|
+
table = getattr(model, "__table__", None)
|
|
52
|
+
cols = tuple(getattr(table, "columns", ()) or ())
|
|
53
|
+
model.columns = tuple(
|
|
54
|
+
getattr(c, "name", None) for c in cols if getattr(c, "name", None)
|
|
55
|
+
)
|
|
56
|
+
if not hasattr(model, "table_config"):
|
|
57
|
+
table = getattr(model, "__table__", None)
|
|
58
|
+
model.table_config = dict(getattr(table, "kwargs", {}) or {})
|
|
59
|
+
# ensure raw hook store exists for decorator merges
|
|
60
|
+
if not hasattr(model, "__tigrbl_hooks__"):
|
|
61
|
+
setattr(model, "__tigrbl_hooks__", {})
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _index_specs(
|
|
65
|
+
specs: Sequence[OpSpec],
|
|
66
|
+
) -> Tuple[Tuple[OpSpec, ...], Dict[_Key, OpSpec], Dict[str, List[OpSpec]]]:
|
|
67
|
+
all_specs: Tuple[OpSpec, ...] = tuple(specs)
|
|
68
|
+
by_key: Dict[_Key, OpSpec] = {}
|
|
69
|
+
by_alias: Dict[str, List[OpSpec]] = {}
|
|
70
|
+
for sp in specs:
|
|
71
|
+
k = _key(sp)
|
|
72
|
+
by_key[k] = sp
|
|
73
|
+
by_alias.setdefault(sp.alias, []).append(sp)
|
|
74
|
+
return all_specs, by_key, by_alias
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _drop_old_entries(model: type, *, keys: Set[_Key] | None) -> None:
|
|
78
|
+
"""
|
|
79
|
+
Remove per-op artifacts for the provided keys before a targeted rebuild.
|
|
80
|
+
Safe no-ops if keys are None (full rebuild happens cleanly by overwrite).
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
if not keys:
|
|
84
|
+
return
|
|
85
|
+
# schemas
|
|
86
|
+
for alias, _target in keys:
|
|
87
|
+
ns = getattr(model.schemas, alias, None)
|
|
88
|
+
if ns:
|
|
89
|
+
for attr in ("in_", "out", "list"):
|
|
90
|
+
try:
|
|
91
|
+
delattr(ns, attr)
|
|
92
|
+
except Exception:
|
|
93
|
+
pass
|
|
94
|
+
if not ns.__dict__:
|
|
95
|
+
try:
|
|
96
|
+
delattr(model.schemas, alias)
|
|
97
|
+
except Exception:
|
|
98
|
+
pass
|
|
99
|
+
# handlers
|
|
100
|
+
for alias, _target in keys:
|
|
101
|
+
if hasattr(model.handlers, alias):
|
|
102
|
+
try:
|
|
103
|
+
delattr(model.handlers, alias)
|
|
104
|
+
except Exception:
|
|
105
|
+
pass
|
|
106
|
+
# hooks
|
|
107
|
+
for alias, _target in keys:
|
|
108
|
+
if hasattr(model.hooks, alias):
|
|
109
|
+
try:
|
|
110
|
+
delattr(model.hooks, alias)
|
|
111
|
+
except Exception:
|
|
112
|
+
pass
|
|
113
|
+
# rpc
|
|
114
|
+
for alias, _target in keys:
|
|
115
|
+
if hasattr(model.rpc, alias):
|
|
116
|
+
try:
|
|
117
|
+
delattr(model.rpc, alias)
|
|
118
|
+
except Exception:
|
|
119
|
+
pass
|
|
120
|
+
# REST endpoints are refreshed wholesale by rest binding as needed
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _filter_specs(
|
|
124
|
+
specs: Sequence[OpSpec], only_keys: Optional[Set[_Key]]
|
|
125
|
+
) -> List[OpSpec]:
|
|
126
|
+
if not only_keys:
|
|
127
|
+
return list(specs)
|
|
128
|
+
ok = only_keys
|
|
129
|
+
return [sp for sp in specs if _key(sp) in ok]
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
__all__ = [
|
|
133
|
+
"_Key",
|
|
134
|
+
"_key",
|
|
135
|
+
"_ensure_model_namespaces",
|
|
136
|
+
"_index_specs",
|
|
137
|
+
"_drop_old_entries",
|
|
138
|
+
"_filter_specs",
|
|
139
|
+
]
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# tigrbl/v3/bindings/model_registry.py
|
|
2
|
+
"""Registry helpers for model bindings."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
from typing import Set
|
|
8
|
+
|
|
9
|
+
from ..config.constants import TIGRBL_REGISTRY_LISTENER_ATTR
|
|
10
|
+
from ..op import OpspecRegistry, get_registry
|
|
11
|
+
|
|
12
|
+
from .model_helpers import _Key
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger("uvicorn")
|
|
15
|
+
logger.debug("Loaded module v3/bindings/model_registry")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _ensure_registry_listener(model: type) -> None:
|
|
19
|
+
"""
|
|
20
|
+
Subscribe (once) to the per-model OpspecRegistry so future register_ops/add/remove/set
|
|
21
|
+
calls automatically refresh the model namespaces.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
reg: OpspecRegistry = get_registry(model)
|
|
25
|
+
|
|
26
|
+
# If we already subscribed, skip
|
|
27
|
+
if getattr(model, TIGRBL_REGISTRY_LISTENER_ATTR, None):
|
|
28
|
+
return
|
|
29
|
+
|
|
30
|
+
def _on_registry_change(registry: OpspecRegistry, changed: Set[_Key]) -> None:
|
|
31
|
+
from .model import rebind
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
rebind(model, changed_keys=changed) # targeted rebind
|
|
35
|
+
except Exception as e: # pragma: no cover
|
|
36
|
+
logger.exception(
|
|
37
|
+
"tigrbl: rebind failed for %s on ops %s: %s",
|
|
38
|
+
model.__name__,
|
|
39
|
+
changed,
|
|
40
|
+
e,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
reg.subscribe(_on_registry_change)
|
|
44
|
+
# Keep a reference to avoid GC of the closure and to prevent double-subscribe
|
|
45
|
+
setattr(model, TIGRBL_REGISTRY_LISTENER_ATTR, _on_registry_change)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _ensure_op_ctx_attach_hook(model: type) -> None:
|
|
49
|
+
"""Patch the model's metaclass to auto-rebind on ctx-only op attachment."""
|
|
50
|
+
|
|
51
|
+
meta = type(model)
|
|
52
|
+
if getattr(meta, "__tigrbl_op_ctx_meta_patch__", False):
|
|
53
|
+
return
|
|
54
|
+
|
|
55
|
+
orig_meta_setattr = meta.__setattr__
|
|
56
|
+
|
|
57
|
+
def _meta_setattr(cls, name, value):
|
|
58
|
+
from .model import rebind
|
|
59
|
+
from ..op.mro_collect import mro_collect_decorated_ops
|
|
60
|
+
|
|
61
|
+
orig_meta_setattr(cls, name, value)
|
|
62
|
+
fn = getattr(value, "__func__", value)
|
|
63
|
+
decl = getattr(fn, "__tigrbl_op_decl__", None)
|
|
64
|
+
if decl and getattr(cls, "__tigrbl_op_ctx_watch__", False):
|
|
65
|
+
alias = decl.alias or name
|
|
66
|
+
target = decl.target or "custom"
|
|
67
|
+
mro_collect_decorated_ops.cache_clear()
|
|
68
|
+
rebind(cls, changed_keys={(alias, target)})
|
|
69
|
+
|
|
70
|
+
meta.__setattr__ = _meta_setattr # type: ignore[attr-defined]
|
|
71
|
+
setattr(meta, "__tigrbl_op_ctx_meta_patch__", True)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
__all__ = [
|
|
75
|
+
"_ensure_registry_listener",
|
|
76
|
+
"_ensure_op_ctx_attach_hook",
|
|
77
|
+
]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
from types import SimpleNamespace
|
|
5
|
+
from typing import Any, Optional, Sequence
|
|
6
|
+
|
|
7
|
+
from .common import OpSpec, _Key
|
|
8
|
+
from .router import _build_router
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger("uvicorn")
|
|
11
|
+
logger.debug("Loaded module v3/bindings/rest/attach")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def build_router_and_attach(
|
|
15
|
+
model: type,
|
|
16
|
+
specs: Sequence[OpSpec],
|
|
17
|
+
*,
|
|
18
|
+
api: Any | None = None,
|
|
19
|
+
only_keys: Optional[Sequence[_Key]] = None,
|
|
20
|
+
) -> None:
|
|
21
|
+
"""
|
|
22
|
+
Build a Router for the model and attach it to `model.rest.router`.
|
|
23
|
+
For simplicity and correctness with FastAPI, we **rebuild the entire router**
|
|
24
|
+
on each call (FastAPI does not support removing individual routes cleanly).
|
|
25
|
+
"""
|
|
26
|
+
router = _build_router(model, specs, api=api)
|
|
27
|
+
rest_ns = getattr(model, "rest", None) or SimpleNamespace()
|
|
28
|
+
rest_ns.router = router
|
|
29
|
+
setattr(model, "rest", rest_ns)
|
|
30
|
+
logger.debug(
|
|
31
|
+
"rest: %s router attached with %d routes",
|
|
32
|
+
model.__name__,
|
|
33
|
+
len(getattr(router, "routes", []) or []),
|
|
34
|
+
)
|