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,265 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
import logging
|
|
5
|
+
from types import SimpleNamespace
|
|
6
|
+
from typing import (
|
|
7
|
+
Annotated,
|
|
8
|
+
Any,
|
|
9
|
+
Awaitable,
|
|
10
|
+
Callable,
|
|
11
|
+
Dict,
|
|
12
|
+
Mapping,
|
|
13
|
+
Sequence,
|
|
14
|
+
List as _List,
|
|
15
|
+
Union as _Union,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
from .common import (
|
|
19
|
+
TIGRBL_AUTH_CONTEXT_ATTR,
|
|
20
|
+
BaseModel,
|
|
21
|
+
Body,
|
|
22
|
+
Depends,
|
|
23
|
+
Response,
|
|
24
|
+
OpSpec,
|
|
25
|
+
Path,
|
|
26
|
+
Request,
|
|
27
|
+
_coerce_parent_kw,
|
|
28
|
+
_get_phase_chains,
|
|
29
|
+
_make_list_query_dep,
|
|
30
|
+
_request_model_for,
|
|
31
|
+
_serialize_output,
|
|
32
|
+
_validate_body,
|
|
33
|
+
_validate_query,
|
|
34
|
+
_executor,
|
|
35
|
+
_status_for,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
from ...runtime.executor.types import _Ctx
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
logging.getLogger("uvicorn").debug("Loaded module v3/bindings/rest/collection")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _ctx(model, alias, target, request, db, payload, parent_kw, api):
|
|
45
|
+
ctx: Dict[str, Any] = {
|
|
46
|
+
"request": request,
|
|
47
|
+
"db": db,
|
|
48
|
+
"payload": payload,
|
|
49
|
+
"path_params": parent_kw,
|
|
50
|
+
# expose both API router and FastAPI app; runtime opview resolution
|
|
51
|
+
# relies on the app object, which must be hashable.
|
|
52
|
+
"api": api if api is not None else getattr(request, "app", None),
|
|
53
|
+
"app": getattr(request, "app", None),
|
|
54
|
+
"model": model,
|
|
55
|
+
"op": alias,
|
|
56
|
+
"method": alias,
|
|
57
|
+
"target": target,
|
|
58
|
+
"env": SimpleNamespace(
|
|
59
|
+
method=alias, params=payload, target=target, model=model
|
|
60
|
+
),
|
|
61
|
+
}
|
|
62
|
+
ac = getattr(request.state, TIGRBL_AUTH_CONTEXT_ATTR, None)
|
|
63
|
+
if ac is not None:
|
|
64
|
+
ctx["auth_context"] = ac
|
|
65
|
+
return _Ctx(ctx)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _sig(nested_vars, extra):
|
|
69
|
+
params = [
|
|
70
|
+
inspect.Parameter(
|
|
71
|
+
nv,
|
|
72
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
73
|
+
annotation=Annotated[str, Path(...)],
|
|
74
|
+
)
|
|
75
|
+
for nv in nested_vars
|
|
76
|
+
]
|
|
77
|
+
params.extend(extra)
|
|
78
|
+
return inspect.Signature(params)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _list_ann(tp):
|
|
82
|
+
try:
|
|
83
|
+
return list[tp] # type: ignore[valid-type]
|
|
84
|
+
except Exception: # pragma: no cover - best effort
|
|
85
|
+
return _List[tp] # type: ignore[name-defined]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _union(a, b):
|
|
89
|
+
try:
|
|
90
|
+
return a | b # type: ignore[operator]
|
|
91
|
+
except Exception: # pragma: no cover - best effort
|
|
92
|
+
return _Union[a, b]
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _make_collection_endpoint(
|
|
96
|
+
model: type,
|
|
97
|
+
sp: OpSpec,
|
|
98
|
+
*,
|
|
99
|
+
resource: str,
|
|
100
|
+
db_dep: Callable[..., Any],
|
|
101
|
+
nested_vars: Sequence[str] | None = None,
|
|
102
|
+
api: Any | None = None,
|
|
103
|
+
) -> Callable[..., Awaitable[Any]]:
|
|
104
|
+
alias, target, nested_vars = sp.alias, sp.target, list(nested_vars or [])
|
|
105
|
+
status_code = _status_for(sp)
|
|
106
|
+
|
|
107
|
+
if target in {"list", "clear"}:
|
|
108
|
+
list_dep = _make_list_query_dep(model, alias) if target == "list" else None
|
|
109
|
+
|
|
110
|
+
async def _endpoint(
|
|
111
|
+
request: Request,
|
|
112
|
+
db: Any = Depends(db_dep),
|
|
113
|
+
q: Mapping[str, Any] | None = None,
|
|
114
|
+
**kw: Any,
|
|
115
|
+
):
|
|
116
|
+
parent_kw = {k: kw[k] for k in nested_vars if k in kw}
|
|
117
|
+
_coerce_parent_kw(model, parent_kw)
|
|
118
|
+
if q is not None:
|
|
119
|
+
query = {**dict(q), **parent_kw}
|
|
120
|
+
payload = _validate_query(model, alias, target, query)
|
|
121
|
+
else:
|
|
122
|
+
payload = dict(parent_kw)
|
|
123
|
+
ctx = _ctx(model, alias, target, request, db, payload, parent_kw, api)
|
|
124
|
+
ctx["response_serializer"] = lambda r: _serialize_output(
|
|
125
|
+
model, alias, target, sp, r
|
|
126
|
+
)
|
|
127
|
+
phases = _get_phase_chains(model, alias)
|
|
128
|
+
result = await _executor._invoke(
|
|
129
|
+
request=request, db=db, phases=phases, ctx=ctx
|
|
130
|
+
)
|
|
131
|
+
if isinstance(result, Response):
|
|
132
|
+
if sp.status_code is not None or result.status_code == 200:
|
|
133
|
+
result.status_code = status_code
|
|
134
|
+
return result
|
|
135
|
+
return result
|
|
136
|
+
|
|
137
|
+
params = [
|
|
138
|
+
inspect.Parameter(
|
|
139
|
+
nv,
|
|
140
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
141
|
+
annotation=Annotated[str, Path(...)],
|
|
142
|
+
)
|
|
143
|
+
for nv in nested_vars
|
|
144
|
+
]
|
|
145
|
+
params.extend(
|
|
146
|
+
[
|
|
147
|
+
inspect.Parameter(
|
|
148
|
+
"request",
|
|
149
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
150
|
+
annotation=Request,
|
|
151
|
+
),
|
|
152
|
+
inspect.Parameter(
|
|
153
|
+
"db",
|
|
154
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
155
|
+
annotation=Annotated[Any, Depends(db_dep)],
|
|
156
|
+
),
|
|
157
|
+
]
|
|
158
|
+
)
|
|
159
|
+
if target == "list":
|
|
160
|
+
params.insert(
|
|
161
|
+
len(nested_vars) + 1,
|
|
162
|
+
inspect.Parameter(
|
|
163
|
+
"q",
|
|
164
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
165
|
+
annotation=Annotated[Mapping[str, Any], Depends(list_dep)],
|
|
166
|
+
),
|
|
167
|
+
)
|
|
168
|
+
_endpoint.__signature__ = inspect.Signature(params)
|
|
169
|
+
else:
|
|
170
|
+
body_model = _request_model_for(sp, model)
|
|
171
|
+
base = body_model or Mapping[str, Any]
|
|
172
|
+
if target.startswith("bulk_"):
|
|
173
|
+
alias_ns = getattr(
|
|
174
|
+
getattr(model, "schemas", None) or SimpleNamespace(), alias, None
|
|
175
|
+
)
|
|
176
|
+
item_model = getattr(alias_ns, "in_item", None) if alias_ns else None
|
|
177
|
+
body_annotation = (
|
|
178
|
+
_list_ann(item_model)
|
|
179
|
+
if isinstance(item_model, type) and issubclass(item_model, BaseModel)
|
|
180
|
+
else _list_ann(Mapping[str, Any])
|
|
181
|
+
if body_model is None
|
|
182
|
+
else base
|
|
183
|
+
)
|
|
184
|
+
elif target in {"create", "update", "replace", "merge"}:
|
|
185
|
+
body_annotation = _union(
|
|
186
|
+
base if body_model else Mapping[str, Any],
|
|
187
|
+
_list_ann(Mapping[str, Any]),
|
|
188
|
+
)
|
|
189
|
+
else:
|
|
190
|
+
body_annotation = base
|
|
191
|
+
|
|
192
|
+
async def _endpoint(
|
|
193
|
+
request: Request,
|
|
194
|
+
db: Any = Depends(db_dep),
|
|
195
|
+
body=Body(...),
|
|
196
|
+
**kw: Any,
|
|
197
|
+
):
|
|
198
|
+
parent_kw = {k: kw[k] for k in nested_vars if k in kw}
|
|
199
|
+
_coerce_parent_kw(model, parent_kw)
|
|
200
|
+
payload = _validate_body(model, alias, target, body)
|
|
201
|
+
is_seq = (
|
|
202
|
+
target in {"create", "update", "replace", "merge"}
|
|
203
|
+
and isinstance(payload, Sequence)
|
|
204
|
+
and not isinstance(payload, Mapping)
|
|
205
|
+
)
|
|
206
|
+
exec_alias = f"bulk_{target}" if is_seq else alias
|
|
207
|
+
exec_target = f"bulk_{target}" if is_seq else target
|
|
208
|
+
if parent_kw:
|
|
209
|
+
if isinstance(payload, Mapping):
|
|
210
|
+
payload = {**payload, **parent_kw}
|
|
211
|
+
else:
|
|
212
|
+
payload = [{**dict(item), **parent_kw} for item in payload]
|
|
213
|
+
ctx = _ctx(
|
|
214
|
+
model, exec_alias, exec_target, request, db, payload, parent_kw, api
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
def _serializer(r, _ctx=ctx):
|
|
218
|
+
out = _serialize_output(model, exec_alias, exec_target, sp, r)
|
|
219
|
+
temp = getattr(_ctx, "temp", {}) if isinstance(_ctx, Mapping) else {}
|
|
220
|
+
extras = (
|
|
221
|
+
temp.get("response_extras", {}) if isinstance(temp, Mapping) else {}
|
|
222
|
+
)
|
|
223
|
+
if isinstance(out, dict) and isinstance(extras, dict):
|
|
224
|
+
out.update(extras)
|
|
225
|
+
return out
|
|
226
|
+
|
|
227
|
+
ctx["response_serializer"] = _serializer
|
|
228
|
+
phases = _get_phase_chains(model, exec_alias)
|
|
229
|
+
result = await _executor._invoke(
|
|
230
|
+
request=request, db=db, phases=phases, ctx=ctx
|
|
231
|
+
)
|
|
232
|
+
if isinstance(result, Response):
|
|
233
|
+
if sp.status_code is not None or result.status_code == 200:
|
|
234
|
+
result.status_code = status_code
|
|
235
|
+
return result
|
|
236
|
+
return result
|
|
237
|
+
|
|
238
|
+
_endpoint.__signature__ = _sig(
|
|
239
|
+
nested_vars,
|
|
240
|
+
[
|
|
241
|
+
inspect.Parameter(
|
|
242
|
+
"request",
|
|
243
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
244
|
+
annotation=Request,
|
|
245
|
+
),
|
|
246
|
+
inspect.Parameter(
|
|
247
|
+
"db",
|
|
248
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
249
|
+
annotation=Annotated[Any, Depends(db_dep)],
|
|
250
|
+
),
|
|
251
|
+
inspect.Parameter(
|
|
252
|
+
"body",
|
|
253
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
254
|
+
annotation=Annotated[body_annotation, Body(...)],
|
|
255
|
+
),
|
|
256
|
+
],
|
|
257
|
+
)
|
|
258
|
+
_endpoint.__annotations__["body"] = body_annotation
|
|
259
|
+
|
|
260
|
+
_endpoint.__name__ = f"rest_{model.__name__}_{alias}_collection"
|
|
261
|
+
_endpoint.__qualname__ = _endpoint.__name__
|
|
262
|
+
_endpoint.__doc__ = (
|
|
263
|
+
f"REST collection endpoint for {model.__name__}.{alias} ({target})"
|
|
264
|
+
)
|
|
265
|
+
return _endpoint
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Shared interfaces for tigrbl.bindings.rest.
|
|
2
|
+
|
|
3
|
+
This module re-exports helper utilities split across smaller modules to keep the
|
|
4
|
+
import surface stable while easing maintenance.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
import logging
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel
|
|
11
|
+
|
|
12
|
+
from .fastapi import (
|
|
13
|
+
Body,
|
|
14
|
+
Depends,
|
|
15
|
+
HTTPException,
|
|
16
|
+
Path,
|
|
17
|
+
Query,
|
|
18
|
+
Request,
|
|
19
|
+
Response,
|
|
20
|
+
Router,
|
|
21
|
+
Security,
|
|
22
|
+
_status,
|
|
23
|
+
)
|
|
24
|
+
from .helpers import (
|
|
25
|
+
_Key,
|
|
26
|
+
_coerce_parent_kw,
|
|
27
|
+
_ensure_jsonable,
|
|
28
|
+
_get_phase_chains,
|
|
29
|
+
_pk_name,
|
|
30
|
+
_pk_names,
|
|
31
|
+
_req_state_db,
|
|
32
|
+
_resource_name,
|
|
33
|
+
)
|
|
34
|
+
from .io import (
|
|
35
|
+
_make_list_query_dep,
|
|
36
|
+
_optionalize_list_in_model,
|
|
37
|
+
_serialize_output,
|
|
38
|
+
_strip_optional,
|
|
39
|
+
_validate_body,
|
|
40
|
+
_validate_query,
|
|
41
|
+
)
|
|
42
|
+
from .routing import (
|
|
43
|
+
_DEFAULT_METHODS,
|
|
44
|
+
_RESPONSES_META,
|
|
45
|
+
_default_path_suffix,
|
|
46
|
+
_normalize_deps,
|
|
47
|
+
_normalize_secdeps,
|
|
48
|
+
_path_for_spec,
|
|
49
|
+
_request_model_for,
|
|
50
|
+
_response_model_for,
|
|
51
|
+
_status_for,
|
|
52
|
+
)
|
|
53
|
+
from ...config.constants import (
|
|
54
|
+
TIGRBL_ALLOW_ANON_ATTR,
|
|
55
|
+
TIGRBL_AUTH_CONTEXT_ATTR,
|
|
56
|
+
TIGRBL_AUTH_DEP_ATTR,
|
|
57
|
+
TIGRBL_GET_DB_ATTR,
|
|
58
|
+
TIGRBL_REST_DEPENDENCIES_ATTR,
|
|
59
|
+
)
|
|
60
|
+
from ...op import OpSpec
|
|
61
|
+
from ...op.types import CANON, PHASES
|
|
62
|
+
from ...rest import _nested_prefix
|
|
63
|
+
from ...runtime import executor as _executor
|
|
64
|
+
from ...schema.builder import _strip_parent_fields
|
|
65
|
+
|
|
66
|
+
logger = logging.getLogger("uvicorn")
|
|
67
|
+
logger.debug("Loaded module v3/bindings/rest/common")
|
|
68
|
+
|
|
69
|
+
__all__ = [
|
|
70
|
+
"Body",
|
|
71
|
+
"Depends",
|
|
72
|
+
"HTTPException",
|
|
73
|
+
"Path",
|
|
74
|
+
"Query",
|
|
75
|
+
"Request",
|
|
76
|
+
"Response",
|
|
77
|
+
"Router",
|
|
78
|
+
"Security",
|
|
79
|
+
"_status",
|
|
80
|
+
"BaseModel",
|
|
81
|
+
"OpSpec",
|
|
82
|
+
"CANON",
|
|
83
|
+
"PHASES",
|
|
84
|
+
"_executor",
|
|
85
|
+
"TIGRBL_GET_DB_ATTR",
|
|
86
|
+
"TIGRBL_AUTH_DEP_ATTR",
|
|
87
|
+
"TIGRBL_REST_DEPENDENCIES_ATTR",
|
|
88
|
+
"TIGRBL_ALLOW_ANON_ATTR",
|
|
89
|
+
"TIGRBL_AUTH_CONTEXT_ATTR",
|
|
90
|
+
"_nested_prefix",
|
|
91
|
+
"_strip_parent_fields",
|
|
92
|
+
"logger",
|
|
93
|
+
"_Key",
|
|
94
|
+
"_ensure_jsonable",
|
|
95
|
+
"_req_state_db",
|
|
96
|
+
"_resource_name",
|
|
97
|
+
"_pk_name",
|
|
98
|
+
"_pk_names",
|
|
99
|
+
"_get_phase_chains",
|
|
100
|
+
"_coerce_parent_kw",
|
|
101
|
+
"_serialize_output",
|
|
102
|
+
"_validate_body",
|
|
103
|
+
"_validate_query",
|
|
104
|
+
"_strip_optional",
|
|
105
|
+
"_make_list_query_dep",
|
|
106
|
+
"_optionalize_list_in_model",
|
|
107
|
+
"_normalize_deps",
|
|
108
|
+
"_normalize_secdeps",
|
|
109
|
+
"_status_for",
|
|
110
|
+
"_RESPONSES_META",
|
|
111
|
+
"_DEFAULT_METHODS",
|
|
112
|
+
"_default_path_suffix",
|
|
113
|
+
"_path_for_spec",
|
|
114
|
+
"_response_model_for",
|
|
115
|
+
"_request_model_for",
|
|
116
|
+
]
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
from types import SimpleNamespace
|
|
5
|
+
from typing import Callable, Sequence
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger("uvicorn")
|
|
8
|
+
logger.debug("Loaded module v3/bindings/rest/fastapi")
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
from ...types import (
|
|
12
|
+
Router,
|
|
13
|
+
Request,
|
|
14
|
+
Body,
|
|
15
|
+
Depends,
|
|
16
|
+
HTTPException,
|
|
17
|
+
Response,
|
|
18
|
+
Path,
|
|
19
|
+
Security,
|
|
20
|
+
)
|
|
21
|
+
from fastapi import Query
|
|
22
|
+
from fastapi import status as _status
|
|
23
|
+
except Exception: # pragma: no cover
|
|
24
|
+
|
|
25
|
+
class Router: # type: ignore
|
|
26
|
+
def __init__(self, *a, **kw):
|
|
27
|
+
self.routes = []
|
|
28
|
+
|
|
29
|
+
def add_api_route(
|
|
30
|
+
self, path: str, endpoint: Callable, methods: Sequence[str], **opts
|
|
31
|
+
):
|
|
32
|
+
self.routes.append((path, methods, endpoint, opts))
|
|
33
|
+
|
|
34
|
+
class Request: # type: ignore
|
|
35
|
+
def __init__(self, scope=None):
|
|
36
|
+
self.scope = scope or {}
|
|
37
|
+
self.query_params = {}
|
|
38
|
+
self.state = SimpleNamespace()
|
|
39
|
+
|
|
40
|
+
def Body(default=None, **kw): # type: ignore
|
|
41
|
+
return default
|
|
42
|
+
|
|
43
|
+
def Depends(fn): # type: ignore
|
|
44
|
+
return fn
|
|
45
|
+
|
|
46
|
+
def Security(fn): # type: ignore
|
|
47
|
+
return fn
|
|
48
|
+
|
|
49
|
+
def Query(default=None, **kw): # type: ignore
|
|
50
|
+
return default
|
|
51
|
+
|
|
52
|
+
def Path(default=None, **kw): # type: ignore
|
|
53
|
+
return default
|
|
54
|
+
|
|
55
|
+
class HTTPException(Exception): # type: ignore
|
|
56
|
+
def __init__(self, status_code: int, detail: str | None = None):
|
|
57
|
+
super().__init__(detail)
|
|
58
|
+
self.status_code = status_code
|
|
59
|
+
self.detail = detail
|
|
60
|
+
|
|
61
|
+
class Response: # type: ignore
|
|
62
|
+
def __init__(self, *a, **kw):
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
class _status: # type: ignore
|
|
66
|
+
HTTP_200_OK = 200
|
|
67
|
+
HTTP_201_CREATED = 201
|
|
68
|
+
HTTP_204_NO_CONTENT = 204
|
|
69
|
+
HTTP_400_BAD_REQUEST = 400
|
|
70
|
+
HTTP_401_UNAUTHORIZED = 401
|
|
71
|
+
HTTP_403_FORBIDDEN = 403
|
|
72
|
+
HTTP_404_NOT_FOUND = 404
|
|
73
|
+
HTTP_409_CONFLICT = 409
|
|
74
|
+
HTTP_422_UNPROCESSABLE_ENTITY = 422
|
|
75
|
+
HTTP_429_TOO_MANY_REQUESTS = 429
|
|
76
|
+
HTTP_500_INTERNAL_SERVER_ERROR = 500
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from types import SimpleNamespace
|
|
5
|
+
from typing import Any, Awaitable, Callable, Dict, Mapping, Sequence, Tuple
|
|
6
|
+
|
|
7
|
+
from .fastapi import Request
|
|
8
|
+
from ...op.types import PHASES
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
from ...runtime.kernel import build_phase_chains as _kernel_build_phase_chains # type: ignore
|
|
12
|
+
except Exception: # pragma: no cover
|
|
13
|
+
_kernel_build_phase_chains = None # type: ignore
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger("uvicorn")
|
|
16
|
+
logger.debug("Loaded module v3/bindings/rest/helpers")
|
|
17
|
+
|
|
18
|
+
_Key = Tuple[str, str] # (alias, target)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _ensure_jsonable(obj: Any) -> Any:
|
|
22
|
+
"""Best-effort conversion of DB rows, row-mappings, or ORM objects to dicts."""
|
|
23
|
+
if isinstance(obj, (list, tuple)):
|
|
24
|
+
return [_ensure_jsonable(x) for x in obj]
|
|
25
|
+
|
|
26
|
+
if isinstance(obj, Mapping):
|
|
27
|
+
try:
|
|
28
|
+
return {k: _ensure_jsonable(v) for k, v in dict(obj).items()}
|
|
29
|
+
except Exception: # pragma: no cover - fall back to original object
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
data = vars(obj)
|
|
34
|
+
except TypeError:
|
|
35
|
+
return obj
|
|
36
|
+
|
|
37
|
+
return {k: _ensure_jsonable(v) for k, v in data.items() if not k.startswith("_")}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _req_state_db(request: Request) -> Any:
|
|
41
|
+
return getattr(request.state, "db", None)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _resource_name(model: type) -> str:
|
|
45
|
+
"""
|
|
46
|
+
Resource segment for HTTP paths/tags.
|
|
47
|
+
|
|
48
|
+
IMPORTANT: Never use table name here. Only allow an explicit __resource__
|
|
49
|
+
override or fall back to the model class name in lowercase.
|
|
50
|
+
"""
|
|
51
|
+
override = getattr(model, "__resource__", None)
|
|
52
|
+
return override or model.__name__.lower()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _pk_name(model: type) -> str:
|
|
56
|
+
"""
|
|
57
|
+
Single primary key name (fallback 'id'). For composite keys, still returns 'id'.
|
|
58
|
+
Used for backward-compat path-param aliasing and handler resolution.
|
|
59
|
+
"""
|
|
60
|
+
table = getattr(model, "__table__", None)
|
|
61
|
+
if table is None:
|
|
62
|
+
return "id"
|
|
63
|
+
pk = getattr(table, "primary_key", None)
|
|
64
|
+
if pk is None:
|
|
65
|
+
return "id"
|
|
66
|
+
try:
|
|
67
|
+
cols = list(pk.columns)
|
|
68
|
+
except Exception:
|
|
69
|
+
return "id"
|
|
70
|
+
if len(cols) != 1:
|
|
71
|
+
return "id"
|
|
72
|
+
return getattr(cols[0], "name", "id")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _pk_names(model: type) -> set[str]:
|
|
76
|
+
"""All PK column names (fallback {'id'})."""
|
|
77
|
+
table = getattr(model, "__table__", None)
|
|
78
|
+
try:
|
|
79
|
+
cols = getattr(getattr(table, "primary_key", None), "columns", None)
|
|
80
|
+
if cols is None:
|
|
81
|
+
return {"id"}
|
|
82
|
+
out = {getattr(c, "name", None) for c in cols}
|
|
83
|
+
out.discard(None)
|
|
84
|
+
return out or {"id"}
|
|
85
|
+
except Exception:
|
|
86
|
+
return {"id"}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _get_phase_chains(
|
|
90
|
+
model: type, alias: str
|
|
91
|
+
) -> Dict[str, Sequence[Callable[..., Awaitable[Any]]]]:
|
|
92
|
+
"""
|
|
93
|
+
Prefer building via runtime Kernel (atoms + system steps + hooks in one lifecycle).
|
|
94
|
+
Fallback: read the pre-built model.hooks.<alias> chains directly.
|
|
95
|
+
"""
|
|
96
|
+
if _kernel_build_phase_chains is not None:
|
|
97
|
+
try:
|
|
98
|
+
return _kernel_build_phase_chains(model, alias)
|
|
99
|
+
except Exception:
|
|
100
|
+
logger.exception(
|
|
101
|
+
"Kernel build_phase_chains failed for %s.%s; falling back to hooks",
|
|
102
|
+
getattr(model, "__name__", model),
|
|
103
|
+
alias,
|
|
104
|
+
)
|
|
105
|
+
hooks_root = getattr(model, "hooks", None) or SimpleNamespace()
|
|
106
|
+
alias_ns = getattr(hooks_root, alias, None)
|
|
107
|
+
out: Dict[str, Sequence[Callable[..., Awaitable[Any]]]] = {}
|
|
108
|
+
for ph in PHASES:
|
|
109
|
+
out[ph] = list(getattr(alias_ns, ph, []) or [])
|
|
110
|
+
return out
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _coerce_parent_kw(model: type, parent_kw: Dict[str, Any]) -> None:
|
|
114
|
+
for name, val in list(parent_kw.items()):
|
|
115
|
+
col = getattr(model, name, None)
|
|
116
|
+
try:
|
|
117
|
+
parent_kw[name] = col.type.python_type(val) # type: ignore[attr-defined]
|
|
118
|
+
except Exception:
|
|
119
|
+
pass
|