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
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from types import SimpleNamespace
|
|
5
|
+
from typing import Any, Dict, Sequence, Tuple
|
|
6
|
+
|
|
7
|
+
from .common import (
|
|
8
|
+
ApiLike,
|
|
9
|
+
_default_prefix,
|
|
10
|
+
_ensure_api_ns,
|
|
11
|
+
_has_include_router,
|
|
12
|
+
_mount_router,
|
|
13
|
+
_resource_name,
|
|
14
|
+
)
|
|
15
|
+
from .resource_proxy import _ResourceProxy
|
|
16
|
+
from .. import model as _binder
|
|
17
|
+
from ...config.constants import (
|
|
18
|
+
TIGRBL_AUTH_DEP_ATTR,
|
|
19
|
+
TIGRBL_AUTHORIZE_ATTR,
|
|
20
|
+
TIGRBL_GET_DB_ATTR,
|
|
21
|
+
TIGRBL_REST_DEPENDENCIES_ATTR,
|
|
22
|
+
TIGRBL_RPC_DEPENDENCIES_ATTR,
|
|
23
|
+
TIGRBL_ALLOW_ANON_ATTR,
|
|
24
|
+
)
|
|
25
|
+
from ...engine import resolver as _resolver
|
|
26
|
+
|
|
27
|
+
logging.getLogger("uvicorn").setLevel(logging.DEBUG)
|
|
28
|
+
logger = logging.getLogger("uvicorn")
|
|
29
|
+
logger.debug("Loaded module v3/bindings/api/include")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# --- keep as helper, no behavior change to transports/kernel ---
|
|
33
|
+
def _seed_security_and_deps(api: Any, model: type) -> None:
|
|
34
|
+
"""
|
|
35
|
+
Copy API-level dependency hooks onto the model so downstream binders can use them.
|
|
36
|
+
- __tigrbl_get_db__ : DB dep (FastAPI Depends-compatible)
|
|
37
|
+
- __tigrbl_auth_dep__ : auth dependency (returns user or raises 401)
|
|
38
|
+
- __tigrbl_authorize__ : callable(request, model, alias, payload, user)→None/raise 403
|
|
39
|
+
- __tigrbl_rest_dependencies__ : list of extra dependencies for REST (e.g., rate-limits)
|
|
40
|
+
- __tigrbl_rpc_dependencies__ : list of extra dependencies for JSON-RPC router
|
|
41
|
+
"""
|
|
42
|
+
# DB deps
|
|
43
|
+
prov = _resolver.resolve_provider(api=api)
|
|
44
|
+
if prov is not None:
|
|
45
|
+
logger.debug("Resolved provider for %s", model.__name__)
|
|
46
|
+
setattr(model, TIGRBL_GET_DB_ATTR, prov.get_db)
|
|
47
|
+
else:
|
|
48
|
+
logger.debug("No provider resolved for %s", model.__name__)
|
|
49
|
+
|
|
50
|
+
# Authn (prefer optional dep when available)
|
|
51
|
+
auth_dep = None
|
|
52
|
+
if getattr(api, "_optional_authn_dep", None):
|
|
53
|
+
auth_dep = api._optional_authn_dep
|
|
54
|
+
logger.debug("Using optional auth dependency for %s", model.__name__)
|
|
55
|
+
elif getattr(api, "_allow_anon", True) is False and getattr(api, "_authn", None):
|
|
56
|
+
auth_dep = api._authn
|
|
57
|
+
logger.debug("Using required auth dependency for %s", model.__name__)
|
|
58
|
+
elif getattr(api, "_authn", None):
|
|
59
|
+
auth_dep = api._authn
|
|
60
|
+
logger.debug("Using default auth dependency for %s", model.__name__)
|
|
61
|
+
if auth_dep is not None:
|
|
62
|
+
setattr(model, TIGRBL_AUTH_DEP_ATTR, auth_dep)
|
|
63
|
+
else:
|
|
64
|
+
logger.debug("No auth dependency configured for %s", model.__name__)
|
|
65
|
+
|
|
66
|
+
# Allow anonymous verbs
|
|
67
|
+
allow_attr = getattr(model, TIGRBL_ALLOW_ANON_ATTR, None)
|
|
68
|
+
if allow_attr:
|
|
69
|
+
verbs = allow_attr() if callable(allow_attr) else allow_attr
|
|
70
|
+
logger.debug("Allowing anonymous verbs %s for %s", verbs, model.__name__)
|
|
71
|
+
for v in verbs:
|
|
72
|
+
api._allow_anon_ops.add(f"{model.__name__}.{v}")
|
|
73
|
+
else:
|
|
74
|
+
logger.debug("No anonymous verbs for %s", model.__name__)
|
|
75
|
+
|
|
76
|
+
# Authz
|
|
77
|
+
if getattr(api, "_authorize", None):
|
|
78
|
+
setattr(model, TIGRBL_AUTHORIZE_ATTR, api._authorize)
|
|
79
|
+
logger.debug("Authorization hook attached for %s", model.__name__)
|
|
80
|
+
else:
|
|
81
|
+
logger.debug("No authorization hook for %s", model.__name__)
|
|
82
|
+
|
|
83
|
+
# Extra deps (router-level only; never part of kernel plan)
|
|
84
|
+
if getattr(api, "rest_dependencies", None):
|
|
85
|
+
setattr(model, TIGRBL_REST_DEPENDENCIES_ATTR, list(api.rest_dependencies))
|
|
86
|
+
logger.debug("REST dependencies seeded for %s", model.__name__)
|
|
87
|
+
else:
|
|
88
|
+
logger.debug("No REST dependencies for %s", model.__name__)
|
|
89
|
+
if getattr(api, "rpc_dependencies", None):
|
|
90
|
+
setattr(model, TIGRBL_RPC_DEPENDENCIES_ATTR, list(api.rpc_dependencies))
|
|
91
|
+
logger.debug("RPC dependencies seeded for %s", model.__name__)
|
|
92
|
+
else:
|
|
93
|
+
logger.debug("No RPC dependencies for %s", model.__name__)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _attach_to_api(api: ApiLike, model: type) -> None:
|
|
97
|
+
"""
|
|
98
|
+
Attach the model’s bound namespaces to the api facade.
|
|
99
|
+
"""
|
|
100
|
+
_ensure_api_ns(api)
|
|
101
|
+
|
|
102
|
+
mname = model.__name__
|
|
103
|
+
rname = _resource_name(model)
|
|
104
|
+
rtitle = rname[:1].upper() + rname[1:]
|
|
105
|
+
logger.debug("Attaching model %s as resource '%s'", mname, rname)
|
|
106
|
+
|
|
107
|
+
# Index model object
|
|
108
|
+
api.models[mname] = model
|
|
109
|
+
api.tables[mname] = getattr(model, "__table__", None)
|
|
110
|
+
|
|
111
|
+
# Direct references to model namespaces
|
|
112
|
+
setattr(api.schemas, mname, getattr(model, "schemas", SimpleNamespace()))
|
|
113
|
+
setattr(api.handlers, mname, getattr(model, "handlers", SimpleNamespace()))
|
|
114
|
+
setattr(api.hooks, mname, getattr(model, "hooks", SimpleNamespace()))
|
|
115
|
+
rpc_ns = getattr(model, "rpc", SimpleNamespace())
|
|
116
|
+
setattr(api.rpc, mname, rpc_ns)
|
|
117
|
+
if rtitle != mname:
|
|
118
|
+
setattr(api.rpc, rtitle, rpc_ns)
|
|
119
|
+
logger.debug("Registered RPC namespace alias '%s'", rtitle)
|
|
120
|
+
# rest (router lives on model.rest.router)
|
|
121
|
+
rest_ns = getattr(api, "rest")
|
|
122
|
+
setattr(
|
|
123
|
+
rest_ns,
|
|
124
|
+
mname,
|
|
125
|
+
SimpleNamespace(
|
|
126
|
+
router=getattr(getattr(model, "rest", SimpleNamespace()), "router", None)
|
|
127
|
+
),
|
|
128
|
+
)
|
|
129
|
+
# also keep a flat routers dict for quick access
|
|
130
|
+
api.routers[mname] = getattr(
|
|
131
|
+
getattr(model, "rest", SimpleNamespace()), "router", None
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# Table metadata (introspection only)
|
|
135
|
+
api.columns[mname] = tuple(getattr(model, "columns", ()))
|
|
136
|
+
api.table_config[mname] = dict(getattr(model, "table_config", {}) or {})
|
|
137
|
+
|
|
138
|
+
# Core helper proxies (now aware of API for DB resolution precedence)
|
|
139
|
+
core_proxy = _ResourceProxy(model, api=api)
|
|
140
|
+
setattr(api.core, mname, core_proxy)
|
|
141
|
+
if rtitle != mname:
|
|
142
|
+
setattr(api.core, rtitle, core_proxy)
|
|
143
|
+
core_raw_proxy = _ResourceProxy(model, serialize=False, api=api)
|
|
144
|
+
setattr(api.core_raw, mname, core_raw_proxy)
|
|
145
|
+
if rtitle != mname:
|
|
146
|
+
setattr(api.core_raw, rtitle, core_raw_proxy)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def include_model(
|
|
150
|
+
api: ApiLike,
|
|
151
|
+
model: type,
|
|
152
|
+
*,
|
|
153
|
+
app: Any | None = None,
|
|
154
|
+
prefix: str | None = None,
|
|
155
|
+
mount_router: bool = True,
|
|
156
|
+
) -> Tuple[type, Any]:
|
|
157
|
+
"""
|
|
158
|
+
Bind a model (if not already bound), mount its REST router, and attach all namespaces to `api`.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
api: An arbitrary facade object; we’ll attach containers onto it if missing.
|
|
162
|
+
model: The SQLAlchemy model (table class).
|
|
163
|
+
app: Optional FastAPI app or Router (anything with `include_router`).
|
|
164
|
+
Routers are always mounted on `api.router`; if provided, we also
|
|
165
|
+
mount onto this `app` (or `api.app` when not given).
|
|
166
|
+
prefix: Optional mount prefix. When None, defaults to `/{ModelClassName}` or
|
|
167
|
+
`/{__resource__}` if set on the model.
|
|
168
|
+
mount_router: If False, we skip mounting onto the host app but still bind
|
|
169
|
+
the router under `api.router`/`api.rest`/`api.routers`.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
(model, router) – the model class and its Router (or None if not present).
|
|
173
|
+
"""
|
|
174
|
+
logger.debug("Including model %s", model.__name__)
|
|
175
|
+
|
|
176
|
+
# If another test or call disposed the SQLAlchemy registry, previously
|
|
177
|
+
# imported models lose their table mapping. Re-map on demand so tests that
|
|
178
|
+
# run after a registry dispose still have working models.
|
|
179
|
+
if not hasattr(model, "__table__"):
|
|
180
|
+
try: # pragma: no cover - defensive path exercised in tests
|
|
181
|
+
from ...table import Base
|
|
182
|
+
from ...table._base import _materialize_colspecs_to_sqla
|
|
183
|
+
|
|
184
|
+
# Recreate mapped_column attributes from ColumnSpecs then map
|
|
185
|
+
_materialize_colspecs_to_sqla(model)
|
|
186
|
+
Base.registry.map_declaratively(model)
|
|
187
|
+
except Exception: # pragma: no cover
|
|
188
|
+
logger.debug("Failed to remap model %s", model.__name__, exc_info=True)
|
|
189
|
+
|
|
190
|
+
# 0) seed deps/security so binders can see them (transport-level only)
|
|
191
|
+
_seed_security_and_deps(api, model)
|
|
192
|
+
|
|
193
|
+
# 1) Build/bind model namespaces (idempotent)
|
|
194
|
+
_binder.bind(model, api=api)
|
|
195
|
+
|
|
196
|
+
# 2) Pick a router & mount prefix
|
|
197
|
+
router = getattr(getattr(model, "rest", SimpleNamespace()), "router", None)
|
|
198
|
+
if prefix is None:
|
|
199
|
+
prefix = _default_prefix(model)
|
|
200
|
+
logger.debug("Computed default prefix '%s' for %s", prefix, model.__name__)
|
|
201
|
+
else:
|
|
202
|
+
logger.debug("Using provided prefix '%s' for %s", prefix, model.__name__)
|
|
203
|
+
|
|
204
|
+
# 3) Always bind model router to the API object when possible
|
|
205
|
+
root_router = api if _has_include_router(api) else getattr(api, "router", None)
|
|
206
|
+
if router is not None:
|
|
207
|
+
logger.debug("Mounting model router for %s on api", model.__name__)
|
|
208
|
+
_mount_router(root_router, router, prefix=prefix)
|
|
209
|
+
else:
|
|
210
|
+
logger.debug("Model %s has no router to mount", model.__name__)
|
|
211
|
+
|
|
212
|
+
# Optionally mount onto a host app
|
|
213
|
+
target_app = app or getattr(api, "app", None)
|
|
214
|
+
if mount_router and router is not None:
|
|
215
|
+
logger.debug("Mounting router for %s on host app", model.__name__)
|
|
216
|
+
_mount_router(target_app, router, prefix=prefix)
|
|
217
|
+
else:
|
|
218
|
+
logger.debug(
|
|
219
|
+
"Skipping host app mount for %s (mount_router=%s, router=%s)",
|
|
220
|
+
model.__name__,
|
|
221
|
+
mount_router,
|
|
222
|
+
router is not None,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
# 4) Attach all namespaces onto api
|
|
226
|
+
_attach_to_api(api, model)
|
|
227
|
+
|
|
228
|
+
logger.debug("bindings.api: included %s at prefix %s", model.__name__, prefix)
|
|
229
|
+
return model, router
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def include_models(
|
|
233
|
+
api: ApiLike,
|
|
234
|
+
models: Sequence[type],
|
|
235
|
+
*,
|
|
236
|
+
app: Any | None = None,
|
|
237
|
+
base_prefix: str | None = None,
|
|
238
|
+
mount_router: bool = True,
|
|
239
|
+
) -> Dict[str, Any]:
|
|
240
|
+
"""
|
|
241
|
+
Convenience helper to include multiple models.
|
|
242
|
+
|
|
243
|
+
If ``base_prefix`` is provided, each model's router is mounted under that
|
|
244
|
+
prefix. The model router itself already has its own `/{resource}` prefix.
|
|
245
|
+
"""
|
|
246
|
+
logger.debug("Including %d models", len(models))
|
|
247
|
+
results: Dict[str, Any] = {}
|
|
248
|
+
for mdl in models:
|
|
249
|
+
px = base_prefix.rstrip("/") if base_prefix else None
|
|
250
|
+
logger.debug("Including model %s with base prefix %s", mdl.__name__, px)
|
|
251
|
+
_, router = include_model(
|
|
252
|
+
api, mdl, app=app, prefix=px, mount_router=mount_router
|
|
253
|
+
)
|
|
254
|
+
results[mdl.__name__] = router
|
|
255
|
+
logger.debug("Finished including models")
|
|
256
|
+
return results
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from types import SimpleNamespace
|
|
5
|
+
from typing import Any, Awaitable, Callable, Dict, Mapping, Optional
|
|
6
|
+
|
|
7
|
+
from ..rpc import _coerce_payload, _get_phase_chains, _validate_input, _serialize_output
|
|
8
|
+
from ...runtime import executor as _executor
|
|
9
|
+
from ...engine import resolver as _resolver
|
|
10
|
+
|
|
11
|
+
logging.getLogger("uvicorn").setLevel(logging.DEBUG)
|
|
12
|
+
logger = logging.getLogger("uvicorn")
|
|
13
|
+
logger.debug("Loaded module v3/bindings/api/resource_proxy")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class _ResourceProxy:
|
|
17
|
+
"""Dynamic proxy that executes core operations."""
|
|
18
|
+
|
|
19
|
+
__slots__ = ("_model", "_serialize", "_api")
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self, model: type, *, serialize: bool = True, api: Any = None
|
|
23
|
+
) -> None: # pragma: no cover - trivial
|
|
24
|
+
self._model = model
|
|
25
|
+
self._serialize = serialize
|
|
26
|
+
self._api = api
|
|
27
|
+
|
|
28
|
+
def __repr__(self) -> str: # pragma: no cover - debug helper
|
|
29
|
+
return f"<ResourceProxy {self._model.__name__}>"
|
|
30
|
+
|
|
31
|
+
def __getattr__(self, alias: str) -> Callable[..., Awaitable[Any]]:
|
|
32
|
+
logger.debug("Resolving core handler '%s' for %s", alias, self._model.__name__)
|
|
33
|
+
handlers_root = getattr(self._model, "handlers", None)
|
|
34
|
+
h_alias = getattr(handlers_root, alias, None) if handlers_root else None
|
|
35
|
+
if h_alias is None or not hasattr(h_alias, "core"):
|
|
36
|
+
logger.debug(
|
|
37
|
+
"No core handler '%s' found for %s", alias, self._model.__name__
|
|
38
|
+
)
|
|
39
|
+
raise AttributeError(f"{self._model.__name__} has no core method '{alias}'")
|
|
40
|
+
|
|
41
|
+
async def _call(
|
|
42
|
+
payload: Any = None,
|
|
43
|
+
*,
|
|
44
|
+
db: Any | None = None,
|
|
45
|
+
request: Any = None,
|
|
46
|
+
ctx: Optional[Dict[str, Any]] = None,
|
|
47
|
+
) -> Any:
|
|
48
|
+
raw_payload = _coerce_payload(payload)
|
|
49
|
+
logger.debug(
|
|
50
|
+
"Calling %s.%s with payload %s",
|
|
51
|
+
self._model.__name__,
|
|
52
|
+
alias,
|
|
53
|
+
raw_payload,
|
|
54
|
+
)
|
|
55
|
+
if alias == "bulk_delete" and not isinstance(raw_payload, Mapping):
|
|
56
|
+
raw_payload = {"ids": raw_payload}
|
|
57
|
+
logger.debug("Coerced bulk_delete payload to mapping: %s", raw_payload)
|
|
58
|
+
norm_payload = _validate_input(self._model, alias, alias, raw_payload)
|
|
59
|
+
logger.debug(
|
|
60
|
+
"Validated payload for %s.%s: %s",
|
|
61
|
+
self._model.__name__,
|
|
62
|
+
alias,
|
|
63
|
+
norm_payload,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
base_ctx: Dict[str, Any] = dict(ctx or {})
|
|
67
|
+
base_ctx.setdefault("payload", norm_payload)
|
|
68
|
+
if request is not None:
|
|
69
|
+
logger.debug("Request provided for %s.%s", self._model.__name__, alias)
|
|
70
|
+
base_ctx.setdefault("request", request)
|
|
71
|
+
# surface contextual metadata for runtime atoms
|
|
72
|
+
app_ref = getattr(request, "app", None) or base_ctx.get("app") or self._api
|
|
73
|
+
base_ctx.setdefault("app", app_ref)
|
|
74
|
+
base_ctx.setdefault("api", base_ctx.get("api") or self._api or app_ref)
|
|
75
|
+
base_ctx.setdefault("model", self._model)
|
|
76
|
+
base_ctx.setdefault("op", alias)
|
|
77
|
+
base_ctx.setdefault("method", alias)
|
|
78
|
+
base_ctx.setdefault("target", alias)
|
|
79
|
+
base_ctx.setdefault(
|
|
80
|
+
"env",
|
|
81
|
+
SimpleNamespace(
|
|
82
|
+
method=alias, params=norm_payload, target=alias, model=self._model
|
|
83
|
+
),
|
|
84
|
+
)
|
|
85
|
+
if self._serialize:
|
|
86
|
+
logger.debug(
|
|
87
|
+
"Serialization enabled for %s.%s", self._model.__name__, alias
|
|
88
|
+
)
|
|
89
|
+
base_ctx.setdefault(
|
|
90
|
+
"response_serializer",
|
|
91
|
+
lambda r: _serialize_output(self._model, alias, alias, r),
|
|
92
|
+
)
|
|
93
|
+
else:
|
|
94
|
+
logger.debug(
|
|
95
|
+
"Serialization disabled for %s.%s", self._model.__name__, alias
|
|
96
|
+
)
|
|
97
|
+
base_ctx.setdefault("response_serializer", lambda r: r)
|
|
98
|
+
|
|
99
|
+
# Acquire DB if one was not explicitly provided (op > model > api > app)
|
|
100
|
+
_release_db = None
|
|
101
|
+
if db is None:
|
|
102
|
+
try:
|
|
103
|
+
logger.debug(
|
|
104
|
+
"Acquiring DB for %s.%s via resolver",
|
|
105
|
+
self._model.__name__,
|
|
106
|
+
alias,
|
|
107
|
+
)
|
|
108
|
+
db, _release_db = _resolver.acquire(
|
|
109
|
+
api=self._api, model=self._model, op_alias=alias
|
|
110
|
+
)
|
|
111
|
+
except Exception:
|
|
112
|
+
logger.exception(
|
|
113
|
+
"DB acquire failed for %s.%s; no default configured?",
|
|
114
|
+
self._model.__name__,
|
|
115
|
+
alias,
|
|
116
|
+
)
|
|
117
|
+
raise
|
|
118
|
+
else:
|
|
119
|
+
logger.debug("Using provided DB for %s.%s", self._model.__name__, alias)
|
|
120
|
+
|
|
121
|
+
base_ctx.setdefault("db", db)
|
|
122
|
+
phases = _get_phase_chains(self._model, alias)
|
|
123
|
+
logger.debug(
|
|
124
|
+
"Executing phases %s for %s.%s", phases, self._model.__name__, alias
|
|
125
|
+
)
|
|
126
|
+
try:
|
|
127
|
+
return await _executor._invoke(
|
|
128
|
+
request=request,
|
|
129
|
+
db=db,
|
|
130
|
+
phases=phases,
|
|
131
|
+
ctx=base_ctx,
|
|
132
|
+
)
|
|
133
|
+
finally:
|
|
134
|
+
if _release_db is not None:
|
|
135
|
+
try:
|
|
136
|
+
_release_db()
|
|
137
|
+
logger.debug(
|
|
138
|
+
"Released DB for %s.%s", self._model.__name__, alias
|
|
139
|
+
)
|
|
140
|
+
except Exception:
|
|
141
|
+
logger.debug(
|
|
142
|
+
"Non-fatal: error releasing acquired DB session",
|
|
143
|
+
exc_info=True,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
_call.__name__ = f"{self._model.__name__}.{alias}"
|
|
147
|
+
_call.__qualname__ = _call.__name__
|
|
148
|
+
_call.__doc__ = f"Helper for core call {self._model.__name__}.{alias}"
|
|
149
|
+
return _call
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from types import SimpleNamespace
|
|
5
|
+
from typing import Any, Dict, Mapping, Optional, Union
|
|
6
|
+
|
|
7
|
+
from .common import ApiLike, _ensure_api_ns
|
|
8
|
+
from ...engine import resolver as _resolver
|
|
9
|
+
from ...core.crud.helpers.model import _single_pk_name
|
|
10
|
+
|
|
11
|
+
logging.getLogger("uvicorn").setLevel(logging.DEBUG)
|
|
12
|
+
logger = logging.getLogger("uvicorn")
|
|
13
|
+
logger.debug("Loaded module v3/bindings/api/rpc")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
async def rpc_call(
|
|
17
|
+
api: ApiLike,
|
|
18
|
+
model_or_name: Union[type, str],
|
|
19
|
+
method: str,
|
|
20
|
+
payload: Any = None,
|
|
21
|
+
*,
|
|
22
|
+
db: Any | None = None,
|
|
23
|
+
request: Any = None,
|
|
24
|
+
ctx: Optional[Dict[str, Any]] = None,
|
|
25
|
+
) -> Any:
|
|
26
|
+
"""
|
|
27
|
+
Call a registered RPC method by (model, method) pair.
|
|
28
|
+
`model_or_name` may be a model class or its name.
|
|
29
|
+
"""
|
|
30
|
+
logger.debug("rpc_call invoked for model=%s method=%s", model_or_name, method)
|
|
31
|
+
_ensure_api_ns(api)
|
|
32
|
+
|
|
33
|
+
if isinstance(model_or_name, str):
|
|
34
|
+
mdl = api.models.get(model_or_name)
|
|
35
|
+
if mdl is None:
|
|
36
|
+
logger.debug("Unknown model name '%s'", model_or_name)
|
|
37
|
+
raise KeyError(f"Unknown model '{model_or_name}'")
|
|
38
|
+
logger.debug("Resolved model name '%s' to %s", model_or_name, mdl)
|
|
39
|
+
else:
|
|
40
|
+
mdl = model_or_name
|
|
41
|
+
logger.debug("Using model class %s", getattr(mdl, "__name__", mdl))
|
|
42
|
+
|
|
43
|
+
fn = getattr(getattr(mdl, "rpc", SimpleNamespace()), method, None)
|
|
44
|
+
if fn is None:
|
|
45
|
+
logger.debug(
|
|
46
|
+
"RPC method '%s' not found on %s", method, getattr(mdl, "__name__", mdl)
|
|
47
|
+
)
|
|
48
|
+
raise AttributeError(
|
|
49
|
+
f"{getattr(mdl, '__name__', mdl)} has no RPC method '{method}'"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
# Acquire DB if not explicitly provided (op > model > api > app)
|
|
53
|
+
_release_db = None
|
|
54
|
+
if db is None:
|
|
55
|
+
try:
|
|
56
|
+
logger.debug(
|
|
57
|
+
"Acquiring DB for rpc_call %s.%s", getattr(mdl, "__name__", mdl), method
|
|
58
|
+
)
|
|
59
|
+
db, _release_db = _resolver.acquire(api=api, model=mdl, op_alias=method)
|
|
60
|
+
except Exception:
|
|
61
|
+
logger.exception(
|
|
62
|
+
"DB acquire failed for rpc_call %s.%s; no default configured?",
|
|
63
|
+
getattr(mdl, "__name__", mdl),
|
|
64
|
+
method,
|
|
65
|
+
)
|
|
66
|
+
raise
|
|
67
|
+
else:
|
|
68
|
+
logger.debug(
|
|
69
|
+
"Using provided DB for rpc_call %s.%s",
|
|
70
|
+
getattr(mdl, "__name__", mdl),
|
|
71
|
+
method,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# Ensure execution context contains basic runtime metadata. In tests or
|
|
75
|
+
# other direct calls there may be no ``request`` object to supply an app
|
|
76
|
+
# reference, which the runtime uses to resolve the opview. When absent, the
|
|
77
|
+
# kernel falls back to cached specs for the given model and alias.
|
|
78
|
+
ctx_dict: Dict[str, Any] = dict(ctx or {})
|
|
79
|
+
# Opportunistically derive path params from the payload when the caller
|
|
80
|
+
# supplies the primary key in the body. Many RPC handlers expect the
|
|
81
|
+
# identifier via ``ctx['path_params']`` (mirroring REST semantics), but
|
|
82
|
+
# test code invokes ``rpc_call`` directly with the id embedded in the
|
|
83
|
+
# payload. Normalizing here preserves backwards compatibility and keeps
|
|
84
|
+
# default CRUD handlers happy.
|
|
85
|
+
if isinstance(payload, Mapping):
|
|
86
|
+
try:
|
|
87
|
+
pk_name = _single_pk_name(mdl)
|
|
88
|
+
except Exception: # model may not be bound to a table
|
|
89
|
+
pk_name = None
|
|
90
|
+
if pk_name and pk_name in payload:
|
|
91
|
+
pp = dict(ctx_dict.get("path_params", {}))
|
|
92
|
+
pp.setdefault(pk_name, payload[pk_name])
|
|
93
|
+
ctx_dict["path_params"] = pp
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
logger.debug("Executing rpc_call %s.%s", getattr(mdl, "__name__", mdl), method)
|
|
97
|
+
return await fn(payload, db=db, request=request, ctx=ctx_dict)
|
|
98
|
+
finally:
|
|
99
|
+
if _release_db is not None:
|
|
100
|
+
try:
|
|
101
|
+
_release_db()
|
|
102
|
+
logger.debug(
|
|
103
|
+
"Released DB for rpc_call %s.%s",
|
|
104
|
+
getattr(mdl, "__name__", mdl),
|
|
105
|
+
method,
|
|
106
|
+
)
|
|
107
|
+
except Exception:
|
|
108
|
+
logger.debug(
|
|
109
|
+
"Non-fatal: error releasing acquired DB session (rpc_call)",
|
|
110
|
+
exc_info=True,
|
|
111
|
+
)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
# tigrbl/v3/bindings/columns.py
|
|
4
|
+
from sqlalchemy import Column
|
|
5
|
+
from ..specs import ColumnSpec, is_virtual
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger("uvicorn")
|
|
8
|
+
logger.debug("Loaded module v3/bindings/columns")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def build_and_attach(model: type, specs=None, only_keys=None):
|
|
12
|
+
cols = {}
|
|
13
|
+
for name, attr in list(model.__dict__.items()):
|
|
14
|
+
if not isinstance(attr, ColumnSpec):
|
|
15
|
+
continue
|
|
16
|
+
if is_virtual(attr):
|
|
17
|
+
cols[name] = attr
|
|
18
|
+
continue
|
|
19
|
+
|
|
20
|
+
st = attr.storage
|
|
21
|
+
# Ensure type instantiation
|
|
22
|
+
col_type = st.type_
|
|
23
|
+
if isinstance(col_type, type):
|
|
24
|
+
# Special case UUID/Enum
|
|
25
|
+
if col_type.__name__ == "UUID":
|
|
26
|
+
col_type = col_type(as_uuid=True)
|
|
27
|
+
elif col_type.__name__ == "Enum":
|
|
28
|
+
raise RuntimeError("Use SAEnum(enum_cls) not bare Enum class")
|
|
29
|
+
else:
|
|
30
|
+
col_type = col_type()
|
|
31
|
+
|
|
32
|
+
col = Column(
|
|
33
|
+
col_type,
|
|
34
|
+
primary_key=st.primary_key,
|
|
35
|
+
nullable=st.nullable,
|
|
36
|
+
unique=st.unique,
|
|
37
|
+
index=st.index,
|
|
38
|
+
autoincrement=st.autoincrement,
|
|
39
|
+
default=st.default,
|
|
40
|
+
onupdate=st.onupdate,
|
|
41
|
+
server_default=st.server_default,
|
|
42
|
+
comment=st.comment,
|
|
43
|
+
)
|
|
44
|
+
setattr(model, name, col)
|
|
45
|
+
cols[name] = attr
|
|
46
|
+
|
|
47
|
+
# register map for later (your Key already has one, but ensure default)
|
|
48
|
+
if not hasattr(model, "__tigrbl_cols__"):
|
|
49
|
+
model.__tigrbl_cols__ = cols
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# tigrbl/v3/bindings/handlers/__init__.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from .builder import build_and_attach
|
|
6
|
+
|
|
7
|
+
logging.getLogger("uvicorn").setLevel(logging.DEBUG)
|
|
8
|
+
logger = logging.getLogger("uvicorn")
|
|
9
|
+
logger.debug("Loaded module v3/bindings/handlers/__init__")
|
|
10
|
+
|
|
11
|
+
__all__ = ["build_and_attach"]
|