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/app/tigrbl_app.py
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
# tigrbl/v3/app/tigrbl_app.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import copy
|
|
5
|
+
from types import SimpleNamespace
|
|
6
|
+
from typing import (
|
|
7
|
+
Any,
|
|
8
|
+
Callable,
|
|
9
|
+
Dict,
|
|
10
|
+
Iterable,
|
|
11
|
+
Mapping,
|
|
12
|
+
Optional,
|
|
13
|
+
Sequence,
|
|
14
|
+
Tuple,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
from ._app import App as _App
|
|
18
|
+
from ..engine.engine_spec import EngineCfg
|
|
19
|
+
from ..engine import resolver as _resolver
|
|
20
|
+
from ..ddl import initialize as _ddl_initialize
|
|
21
|
+
from ..bindings.api import (
|
|
22
|
+
include_model as _include_model,
|
|
23
|
+
include_models as _include_models,
|
|
24
|
+
rpc_call as _rpc_call,
|
|
25
|
+
_seed_security_and_deps,
|
|
26
|
+
_mount_router,
|
|
27
|
+
_default_prefix,
|
|
28
|
+
AttrDict,
|
|
29
|
+
)
|
|
30
|
+
from ..bindings.model import rebind as _rebind, bind as _bind
|
|
31
|
+
from ..bindings.rest import build_router_and_attach as _build_router_and_attach
|
|
32
|
+
from ..transport import mount_jsonrpc as _mount_jsonrpc
|
|
33
|
+
from ..system import mount_diagnostics as _mount_diagnostics
|
|
34
|
+
from ..op import get_registry, OpSpec
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# optional compat: legacy transactional decorator
|
|
38
|
+
try:
|
|
39
|
+
from .compat.transactional import transactional as _txn_decorator
|
|
40
|
+
except Exception: # pragma: no cover
|
|
41
|
+
_txn_decorator = None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class TigrblApp(_App):
|
|
45
|
+
"""
|
|
46
|
+
Monolithic facade that owns:
|
|
47
|
+
• containers (models, schemas, handlers, hooks, rpc, rest, routers, columns, table_config, core proxies)
|
|
48
|
+
• model inclusion (REST + RPC wiring)
|
|
49
|
+
• JSON-RPC / diagnostics mounting
|
|
50
|
+
• (optional) legacy-friendly helpers (transactional decorator, auth flags)
|
|
51
|
+
|
|
52
|
+
It composes v3 primitives; you can still use the functions directly if you prefer.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
TITLE = "TigrblApp"
|
|
56
|
+
VERSION = "0.1.0"
|
|
57
|
+
LIFESPAN = None
|
|
58
|
+
MIDDLEWARES: Sequence[Any] = ()
|
|
59
|
+
APIS: Sequence[Any] = ()
|
|
60
|
+
MODELS: Sequence[Any] = ()
|
|
61
|
+
|
|
62
|
+
# --- optional auth knobs recognized by some middlewares/dispatchers (kept for back-compat) ---
|
|
63
|
+
_authn: Any = None
|
|
64
|
+
_allow_anon: bool = True
|
|
65
|
+
_authorize: Any = None
|
|
66
|
+
_optional_authn_dep: Any = None
|
|
67
|
+
_allow_anon_ops: set[str] = set()
|
|
68
|
+
|
|
69
|
+
def __init__(
|
|
70
|
+
self,
|
|
71
|
+
*,
|
|
72
|
+
engine: EngineCfg | None = None,
|
|
73
|
+
jsonrpc_prefix: str = "/rpc",
|
|
74
|
+
system_prefix: str = "/system",
|
|
75
|
+
api_hooks: Mapping[str, Iterable[Callable]]
|
|
76
|
+
| Mapping[str, Mapping[str, Iterable[Callable]]]
|
|
77
|
+
| None = None,
|
|
78
|
+
**fastapi_kwargs: Any,
|
|
79
|
+
) -> None:
|
|
80
|
+
title = fastapi_kwargs.pop("title", None)
|
|
81
|
+
if title is not None:
|
|
82
|
+
self.TITLE = title
|
|
83
|
+
version = fastapi_kwargs.pop("version", None)
|
|
84
|
+
if version is not None:
|
|
85
|
+
self.VERSION = version
|
|
86
|
+
lifespan = fastapi_kwargs.pop("lifespan", None)
|
|
87
|
+
if lifespan is not None:
|
|
88
|
+
self.LIFESPAN = lifespan
|
|
89
|
+
super().__init__(engine=engine, **fastapi_kwargs)
|
|
90
|
+
# capture initial routes so refreshes retain FastAPI defaults
|
|
91
|
+
self._base_routes = list(self.router.routes)
|
|
92
|
+
self.jsonrpc_prefix = jsonrpc_prefix
|
|
93
|
+
self.system_prefix = system_prefix
|
|
94
|
+
|
|
95
|
+
# public containers (mirrors used by bindings.api)
|
|
96
|
+
self.models: Dict[str, type] = {}
|
|
97
|
+
self.schemas = SimpleNamespace()
|
|
98
|
+
self.handlers = SimpleNamespace()
|
|
99
|
+
self.hooks = SimpleNamespace()
|
|
100
|
+
self.rpc = SimpleNamespace()
|
|
101
|
+
self.rest = SimpleNamespace()
|
|
102
|
+
self.routers: Dict[str, Any] = {}
|
|
103
|
+
self.tables = AttrDict()
|
|
104
|
+
self.columns: Dict[str, Tuple[str, ...]] = {}
|
|
105
|
+
self.table_config: Dict[str, Dict[str, Any]] = {}
|
|
106
|
+
self.core = SimpleNamespace()
|
|
107
|
+
self.core_raw = SimpleNamespace()
|
|
108
|
+
|
|
109
|
+
# API-level hooks map (merged into each model at include-time; precedence handled in bindings.hooks)
|
|
110
|
+
self._api_hooks_map = copy.deepcopy(api_hooks) if api_hooks else None
|
|
111
|
+
|
|
112
|
+
# ------------------------- internal helpers -------------------------
|
|
113
|
+
|
|
114
|
+
@staticmethod
|
|
115
|
+
def _merge_api_hooks_into_model(model: type, hooks_map: Any) -> None:
|
|
116
|
+
"""
|
|
117
|
+
Install API-level hooks on the model so the binder can see them.
|
|
118
|
+
Accepted shapes:
|
|
119
|
+
{phase: [fn, ...]} # global, all aliases
|
|
120
|
+
{alias: {phase: [fn, ...]}, "*": {...}} # per-alias + wildcard
|
|
121
|
+
If the model already has __tigrbl_api_hooks__, we shallow-merge keys.
|
|
122
|
+
"""
|
|
123
|
+
if not hooks_map:
|
|
124
|
+
return
|
|
125
|
+
existing = getattr(model, "__tigrbl_api_hooks__", None)
|
|
126
|
+
if existing is None:
|
|
127
|
+
setattr(model, "__tigrbl_api_hooks__", copy.deepcopy(hooks_map))
|
|
128
|
+
return
|
|
129
|
+
|
|
130
|
+
# shallow merge (alias or phase keys); values are lists we extend
|
|
131
|
+
merged = copy.deepcopy(existing)
|
|
132
|
+
for k, v in (hooks_map or {}).items():
|
|
133
|
+
if k not in merged:
|
|
134
|
+
merged[k] = copy.deepcopy(v)
|
|
135
|
+
else:
|
|
136
|
+
# when both are dicts, merge phase lists
|
|
137
|
+
if isinstance(v, Mapping) and isinstance(merged[k], Mapping):
|
|
138
|
+
for ph, fns in v.items():
|
|
139
|
+
merged[k].setdefault(ph, [])
|
|
140
|
+
merged[k][ph] = list(merged[k][ph]) + list(fns or [])
|
|
141
|
+
else:
|
|
142
|
+
# fallback: prefer model-local value, then append api-level
|
|
143
|
+
if isinstance(merged[k], list):
|
|
144
|
+
merged[k] = list(merged[k]) + list(v or [])
|
|
145
|
+
else:
|
|
146
|
+
merged[k] = v
|
|
147
|
+
setattr(model, "__tigrbl_api_hooks__", merged)
|
|
148
|
+
|
|
149
|
+
# ------------------------- primary operations -------------------------
|
|
150
|
+
|
|
151
|
+
def include_model(
|
|
152
|
+
self, model: type, *, prefix: str | None = None, mount_router: bool = True
|
|
153
|
+
) -> Tuple[type, Any]:
|
|
154
|
+
"""
|
|
155
|
+
Bind a model, mount its REST router, and attach all namespaces to this facade.
|
|
156
|
+
"""
|
|
157
|
+
# inject API-level hooks so the binder merges them
|
|
158
|
+
self._merge_api_hooks_into_model(model, self._api_hooks_map)
|
|
159
|
+
return _include_model(self, model, prefix=prefix, mount_router=mount_router)
|
|
160
|
+
|
|
161
|
+
def include_models(
|
|
162
|
+
self,
|
|
163
|
+
models: Sequence[type],
|
|
164
|
+
*,
|
|
165
|
+
base_prefix: str | None = None,
|
|
166
|
+
mount_router: bool = True,
|
|
167
|
+
) -> Dict[str, Any]:
|
|
168
|
+
for m in models:
|
|
169
|
+
self._merge_api_hooks_into_model(m, self._api_hooks_map)
|
|
170
|
+
return _include_models(
|
|
171
|
+
self,
|
|
172
|
+
models,
|
|
173
|
+
base_prefix=base_prefix,
|
|
174
|
+
mount_router=mount_router,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
async def rpc_call(
|
|
178
|
+
self,
|
|
179
|
+
model_or_name: type | str,
|
|
180
|
+
method: str,
|
|
181
|
+
payload: Any = None,
|
|
182
|
+
*,
|
|
183
|
+
db: Any,
|
|
184
|
+
request: Any = None,
|
|
185
|
+
ctx: Optional[Dict[str, Any]] = None,
|
|
186
|
+
) -> Any:
|
|
187
|
+
return await _rpc_call(
|
|
188
|
+
self, model_or_name, method, payload, db=db, request=request, ctx=ctx
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# ------------------------- extras / mounting -------------------------
|
|
192
|
+
|
|
193
|
+
def mount_jsonrpc(self, *, prefix: str | None = None) -> Any:
|
|
194
|
+
"""Mount JSON-RPC router onto this app."""
|
|
195
|
+
px = prefix if prefix is not None else self.jsonrpc_prefix
|
|
196
|
+
prov = _resolver.resolve_provider(api=self)
|
|
197
|
+
get_db = prov.get_db if prov else None
|
|
198
|
+
router = _mount_jsonrpc(
|
|
199
|
+
self,
|
|
200
|
+
self,
|
|
201
|
+
prefix=px,
|
|
202
|
+
get_db=get_db,
|
|
203
|
+
)
|
|
204
|
+
self._base_routes = list(self.router.routes)
|
|
205
|
+
return router
|
|
206
|
+
|
|
207
|
+
def attach_diagnostics(
|
|
208
|
+
self, *, prefix: str | None = None, app: Any | None = None
|
|
209
|
+
) -> Any:
|
|
210
|
+
"""Mount diagnostics router onto this app or the provided ``app``."""
|
|
211
|
+
px = prefix if prefix is not None else self.system_prefix
|
|
212
|
+
prov = _resolver.resolve_provider(api=self)
|
|
213
|
+
get_db = prov.get_db if prov else None
|
|
214
|
+
router = _mount_diagnostics(self, get_db=get_db)
|
|
215
|
+
include_self = getattr(self, "include_router", None)
|
|
216
|
+
if callable(include_self):
|
|
217
|
+
include_self(router, prefix=px)
|
|
218
|
+
if app is not None and app is not self:
|
|
219
|
+
include_other = getattr(app, "include_router", None)
|
|
220
|
+
if callable(include_other):
|
|
221
|
+
include_other(router, prefix=px)
|
|
222
|
+
if app is None:
|
|
223
|
+
self._base_routes = list(self.router.routes)
|
|
224
|
+
return router
|
|
225
|
+
|
|
226
|
+
# ------------------------- registry passthroughs -------------------------
|
|
227
|
+
|
|
228
|
+
def registry(self, model: type):
|
|
229
|
+
"""Return the per-model OpspecRegistry."""
|
|
230
|
+
return get_registry(model)
|
|
231
|
+
|
|
232
|
+
def bind(self, model: type) -> Tuple[OpSpec, ...]:
|
|
233
|
+
"""Bind/rebuild a model in place (without mounting)."""
|
|
234
|
+
self._merge_api_hooks_into_model(model, self._api_hooks_map)
|
|
235
|
+
return _bind(model)
|
|
236
|
+
|
|
237
|
+
def rebind(
|
|
238
|
+
self, model: type, *, changed_keys: Optional[set[tuple[str, str]]] = None
|
|
239
|
+
) -> Tuple[OpSpec, ...]:
|
|
240
|
+
"""Targeted rebuild of a bound model."""
|
|
241
|
+
return _rebind(model, changed_keys=changed_keys)
|
|
242
|
+
|
|
243
|
+
# ------------------------- legacy helpers -------------------------
|
|
244
|
+
|
|
245
|
+
def transactional(self, *dargs, **dkw):
|
|
246
|
+
"""
|
|
247
|
+
Legacy-friendly decorator: @api.transactional(...)
|
|
248
|
+
Wraps a function as a v3 custom op with START_TX/END_TX.
|
|
249
|
+
"""
|
|
250
|
+
if _txn_decorator is None:
|
|
251
|
+
raise RuntimeError("transactional decorator not available")
|
|
252
|
+
return _txn_decorator(self, *dargs, **dkw)
|
|
253
|
+
|
|
254
|
+
# Optional: let callers set auth knobs used by some middlewares/dispatchers
|
|
255
|
+
def set_auth(
|
|
256
|
+
self,
|
|
257
|
+
*,
|
|
258
|
+
authn: Any = None,
|
|
259
|
+
allow_anon: Optional[bool] = None,
|
|
260
|
+
authorize: Any = None,
|
|
261
|
+
optional_authn_dep: Any = None,
|
|
262
|
+
) -> None:
|
|
263
|
+
if authn is not None:
|
|
264
|
+
self._authn = authn
|
|
265
|
+
if allow_anon is not None:
|
|
266
|
+
self._allow_anon = bool(allow_anon)
|
|
267
|
+
if authorize is not None:
|
|
268
|
+
self._authorize = authorize
|
|
269
|
+
if optional_authn_dep is not None:
|
|
270
|
+
self._optional_authn_dep = optional_authn_dep
|
|
271
|
+
|
|
272
|
+
# Refresh already-included models so routers pick up new auth settings
|
|
273
|
+
if self.models:
|
|
274
|
+
self._refresh_security()
|
|
275
|
+
|
|
276
|
+
def _refresh_security(self) -> None:
|
|
277
|
+
"""Re-seed auth deps on models and rebuild routers."""
|
|
278
|
+
# Reset router to baseline and allow_anon ops cache
|
|
279
|
+
self.router.routes = list(self._base_routes)
|
|
280
|
+
self._allow_anon_ops = set()
|
|
281
|
+
for model in self.models.values():
|
|
282
|
+
_seed_security_and_deps(self, model)
|
|
283
|
+
specs = getattr(getattr(model, "opspecs", SimpleNamespace()), "all", ())
|
|
284
|
+
if specs:
|
|
285
|
+
_build_router_and_attach(model, list(specs))
|
|
286
|
+
router = getattr(getattr(model, "rest", SimpleNamespace()), "router", None)
|
|
287
|
+
if router is None:
|
|
288
|
+
continue
|
|
289
|
+
# update api-level references
|
|
290
|
+
mname = model.__name__
|
|
291
|
+
rest_ns = getattr(self.rest, mname, SimpleNamespace())
|
|
292
|
+
rest_ns.router = router
|
|
293
|
+
setattr(self.rest, mname, rest_ns)
|
|
294
|
+
self.routers[mname] = router
|
|
295
|
+
prefix = _default_prefix(model)
|
|
296
|
+
_mount_router(self, router, prefix=prefix)
|
|
297
|
+
|
|
298
|
+
def _collect_tables(self):
|
|
299
|
+
# dedupe; handle multiple DeclarativeBases (multiple metadatas)
|
|
300
|
+
seen = set()
|
|
301
|
+
tables = []
|
|
302
|
+
for m in self.models.values():
|
|
303
|
+
t = getattr(m, "__table__", None)
|
|
304
|
+
if t is not None and t not in seen:
|
|
305
|
+
seen.add(t)
|
|
306
|
+
tables.append(t)
|
|
307
|
+
return tables
|
|
308
|
+
|
|
309
|
+
initialize = _ddl_initialize
|
|
310
|
+
|
|
311
|
+
# ------------------------- repr -------------------------
|
|
312
|
+
|
|
313
|
+
def __repr__(self) -> str: # pragma: no cover
|
|
314
|
+
return f"<TigrblApp models={list(self.models)} rpc={list(getattr(self.rpc, '__dict__', {}).keys())}>"
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# tigrbl/v3/bindings/__init__.py
|
|
2
|
+
"""
|
|
3
|
+
Tigrbl v3 – Bindings package.
|
|
4
|
+
|
|
5
|
+
This package wires OpSpec-derived artifacts onto models and an API facade.
|
|
6
|
+
Concerns are kept strictly separated:
|
|
7
|
+
• Security deps & extra deps → transport/router only (REST/RPC)
|
|
8
|
+
• System steps → injected by runtime lifecycle (Kernel), not by bindings
|
|
9
|
+
• Atoms → discovered/injected by runtime, not by bindings
|
|
10
|
+
• Hooks → bindable at API / model / op levels (no imperative hooks)
|
|
11
|
+
|
|
12
|
+
Public surface (re-exports)
|
|
13
|
+
|
|
14
|
+
Model binding:
|
|
15
|
+
- bind(model, *, only_keys=None) → builds/refreshes model namespaces
|
|
16
|
+
- rebind(model, *, changed_keys=None) → targeted refresh
|
|
17
|
+
|
|
18
|
+
Per-concern builders:
|
|
19
|
+
- build_schemas(model, specs, *, only_keys=None)
|
|
20
|
+
• Seeds schemas declared via @schema_ctx before computing defaults.
|
|
21
|
+
• Supports overrides via SchemaRef("alias","in|out"), "alias.in"/"alias.out", or "raw".
|
|
22
|
+
- build_hooks(model, specs, *, only_keys=None)
|
|
23
|
+
• Merges API → MODEL → OP for pre-like phases; OP → MODEL → API for post/error.
|
|
24
|
+
• No imperative hook source.
|
|
25
|
+
- build_handlers(model, specs, *, only_keys=None)
|
|
26
|
+
• Inserts the core/raw step into the HANDLER phase (other phases handled by runtime).
|
|
27
|
+
- register_rpc(model, specs, *, only_keys=None)
|
|
28
|
+
- build_rest(model, specs, *, only_keys=None)
|
|
29
|
+
• Router-level only; serializes responses iff a response schema exists.
|
|
30
|
+
|
|
31
|
+
API integration:
|
|
32
|
+
- include_model(api, model, *, app=None, prefix=None, mount_router=True)
|
|
33
|
+
- include_models(api, models, *, app=None, base_prefix=None, mount_router=True)
|
|
34
|
+
- rpc_call(api, model_or_name, method, payload=None, *, db, request=None, ctx=None)
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
from __future__ import annotations
|
|
38
|
+
import logging
|
|
39
|
+
|
|
40
|
+
# Core model orchestrator
|
|
41
|
+
from .model import bind, rebind
|
|
42
|
+
|
|
43
|
+
# Per-concern builders (aliased for a clean public API)
|
|
44
|
+
from .schemas import build_and_attach as build_schemas
|
|
45
|
+
from .hooks import normalize_and_attach as build_hooks
|
|
46
|
+
from .handlers import build_and_attach as build_handlers
|
|
47
|
+
from .rpc import register_and_attach as register_rpc
|
|
48
|
+
from .rest import build_router_and_attach as build_rest
|
|
49
|
+
from ..response.bind import bind as bind_response
|
|
50
|
+
|
|
51
|
+
# API facade integration
|
|
52
|
+
from .api import include_model, include_models, rpc_call
|
|
53
|
+
|
|
54
|
+
logger = logging.getLogger("uvicorn")
|
|
55
|
+
logger.debug("Loaded module v3/bindings/__init__")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
__all__ = [
|
|
59
|
+
# model orchestrator
|
|
60
|
+
"bind",
|
|
61
|
+
"rebind",
|
|
62
|
+
# per-concern builders
|
|
63
|
+
"build_schemas",
|
|
64
|
+
"build_hooks",
|
|
65
|
+
"build_handlers",
|
|
66
|
+
"register_rpc",
|
|
67
|
+
"build_rest",
|
|
68
|
+
"bind_response",
|
|
69
|
+
# api integration
|
|
70
|
+
"include_model",
|
|
71
|
+
"include_models",
|
|
72
|
+
"rpc_call",
|
|
73
|
+
]
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
from .common import AttrDict, _default_prefix, _mount_router # noqa: F401
|
|
5
|
+
from .include import include_model, include_models, _seed_security_and_deps # noqa: F401
|
|
6
|
+
from .rpc import rpc_call
|
|
7
|
+
|
|
8
|
+
logging.getLogger("uvicorn").setLevel(logging.DEBUG)
|
|
9
|
+
logger = logging.getLogger("uvicorn")
|
|
10
|
+
logger.debug("Loaded module v3/bindings/api/__init__")
|
|
11
|
+
|
|
12
|
+
__all__ = ["include_model", "include_models", "rpc_call"]
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from types import SimpleNamespace
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
logging.getLogger("uvicorn").setLevel(logging.DEBUG)
|
|
8
|
+
logger = logging.getLogger("uvicorn")
|
|
9
|
+
logger.debug("Loaded module v3/bindings/api/common")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AttrDict(dict):
|
|
13
|
+
"""Dictionary providing attribute-style access."""
|
|
14
|
+
|
|
15
|
+
def __getattr__(self, item: str) -> Any: # pragma: no cover - trivial
|
|
16
|
+
try:
|
|
17
|
+
return self[item]
|
|
18
|
+
except KeyError as e: # pragma: no cover - debug aid
|
|
19
|
+
raise AttributeError(item) from e
|
|
20
|
+
|
|
21
|
+
def __setattr__(self, key: str, value: Any) -> None: # pragma: no cover - trivial
|
|
22
|
+
self[key] = value
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# Public type for the API facade object users pass to include_model(...)
|
|
26
|
+
ApiLike = Any
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _resource_name(model: type) -> str:
|
|
30
|
+
"""
|
|
31
|
+
Compute the API resource segment.
|
|
32
|
+
|
|
33
|
+
Policy:
|
|
34
|
+
- Prefer explicit `__resource__` when present (caller-controlled).
|
|
35
|
+
- Otherwise, use the model *class name* in lowercase.
|
|
36
|
+
- DO NOT use `__tablename__` here (strictly DB-only per project policy).
|
|
37
|
+
"""
|
|
38
|
+
if hasattr(model, "__resource__"):
|
|
39
|
+
resource = model.__resource__
|
|
40
|
+
logger.debug("Using explicit resource '%s' for %s", resource, model.__name__)
|
|
41
|
+
else:
|
|
42
|
+
resource = model.__name__.lower()
|
|
43
|
+
logger.debug("Derived resource '%s' for %s", resource, model.__name__)
|
|
44
|
+
return resource
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _default_prefix(model: type) -> str:
|
|
48
|
+
"""Default mount prefix for a model router.
|
|
49
|
+
|
|
50
|
+
Historically routers were mounted under ``/{resource}``, resulting in
|
|
51
|
+
duplicated path segments such as ``/item/item``. To expose REST endpoints
|
|
52
|
+
under ``/item`` we now mount routers at the application root by default.
|
|
53
|
+
"""
|
|
54
|
+
logger.debug("Default prefix for %s is root '/'", model.__name__)
|
|
55
|
+
return ""
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _has_include_router(obj: Any) -> bool:
|
|
59
|
+
has_router = hasattr(obj, "include_router") and callable(
|
|
60
|
+
getattr(obj, "include_router")
|
|
61
|
+
)
|
|
62
|
+
logger.debug("Object %s has include_router: %s", obj, has_router)
|
|
63
|
+
return has_router
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _mount_router(app_or_router: Any, router: Any, *, prefix: str) -> None:
|
|
67
|
+
"""
|
|
68
|
+
Best-effort mount onto a FastAPI app or Router.
|
|
69
|
+
If not available, we still attach router under api.routers for later use.
|
|
70
|
+
"""
|
|
71
|
+
if app_or_router is None:
|
|
72
|
+
logger.debug("No app/router; skipping mount for prefix %s", prefix)
|
|
73
|
+
return
|
|
74
|
+
try:
|
|
75
|
+
if _has_include_router(app_or_router):
|
|
76
|
+
logger.debug("Mounting router %s at prefix %s", router, prefix)
|
|
77
|
+
app_or_router.include_router(router, prefix=prefix) # FastAPI / Router
|
|
78
|
+
else:
|
|
79
|
+
logger.debug(
|
|
80
|
+
"Provided object %s lacks include_router; not mounting router",
|
|
81
|
+
app_or_router,
|
|
82
|
+
)
|
|
83
|
+
except Exception:
|
|
84
|
+
logger.exception("Failed to mount router at %s", prefix)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _ensure_api_ns(api: ApiLike) -> None:
|
|
88
|
+
"""
|
|
89
|
+
Ensure containers exist on the api facade object.
|
|
90
|
+
"""
|
|
91
|
+
for attr, default in (
|
|
92
|
+
("models", {}),
|
|
93
|
+
("tables", AttrDict()),
|
|
94
|
+
("schemas", SimpleNamespace()),
|
|
95
|
+
("handlers", SimpleNamespace()),
|
|
96
|
+
("hooks", SimpleNamespace()),
|
|
97
|
+
("rpc", SimpleNamespace()),
|
|
98
|
+
("rest", SimpleNamespace()),
|
|
99
|
+
("routers", {}),
|
|
100
|
+
("columns", {}),
|
|
101
|
+
("table_config", {}),
|
|
102
|
+
("core", SimpleNamespace()), # helper method proxies
|
|
103
|
+
("core_raw", SimpleNamespace()),
|
|
104
|
+
):
|
|
105
|
+
if not hasattr(api, attr):
|
|
106
|
+
setattr(api, attr, default)
|
|
107
|
+
logger.debug("Initialized api.%s", attr)
|
|
108
|
+
else:
|
|
109
|
+
logger.debug("api already has attribute %s", attr)
|