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,292 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
import inspect
|
|
5
|
+
import re
|
|
6
|
+
from uuid import uuid4
|
|
7
|
+
from typing import Any, Sequence
|
|
8
|
+
|
|
9
|
+
from .collection import _make_collection_endpoint
|
|
10
|
+
from .member import _make_member_endpoint
|
|
11
|
+
from .common import (
|
|
12
|
+
TIGRBL_ALLOW_ANON_ATTR,
|
|
13
|
+
TIGRBL_AUTH_DEP_ATTR,
|
|
14
|
+
TIGRBL_GET_DB_ATTR,
|
|
15
|
+
TIGRBL_REST_DEPENDENCIES_ATTR,
|
|
16
|
+
BaseModel,
|
|
17
|
+
CANON,
|
|
18
|
+
OpSpec,
|
|
19
|
+
Response,
|
|
20
|
+
Router,
|
|
21
|
+
_DEFAULT_METHODS,
|
|
22
|
+
_default_path_suffix,
|
|
23
|
+
_nested_prefix,
|
|
24
|
+
_normalize_deps,
|
|
25
|
+
_normalize_secdeps,
|
|
26
|
+
_optionalize_list_in_model,
|
|
27
|
+
_path_for_spec,
|
|
28
|
+
_req_state_db,
|
|
29
|
+
_resource_name,
|
|
30
|
+
_status,
|
|
31
|
+
_status_for,
|
|
32
|
+
_strip_parent_fields,
|
|
33
|
+
_RESPONSES_META,
|
|
34
|
+
)
|
|
35
|
+
from ...schema import _make_bulk_rows_model
|
|
36
|
+
import typing as _typing
|
|
37
|
+
from typing import get_args as _get_args, get_origin as _get_origin
|
|
38
|
+
|
|
39
|
+
logger = logging.getLogger("uvicorn")
|
|
40
|
+
logger.debug("Loaded module v3/bindings/rest/router")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _build_router(
|
|
44
|
+
model: type, specs: Sequence[OpSpec], *, api: Any | None = None
|
|
45
|
+
) -> Router:
|
|
46
|
+
resource = _resource_name(model)
|
|
47
|
+
|
|
48
|
+
# Router-level deps: extra deps only (transport-level; never part of kernel plan)
|
|
49
|
+
extra_router_deps = _normalize_deps(
|
|
50
|
+
getattr(model, TIGRBL_REST_DEPENDENCIES_ATTR, None)
|
|
51
|
+
)
|
|
52
|
+
auth_dep = getattr(model, TIGRBL_AUTH_DEP_ATTR, None)
|
|
53
|
+
|
|
54
|
+
# Verbs explicitly allowed without auth
|
|
55
|
+
allow_anon_attr = getattr(model, TIGRBL_ALLOW_ANON_ATTR, None)
|
|
56
|
+
allow_anon = set(
|
|
57
|
+
allow_anon_attr() if callable(allow_anon_attr) else allow_anon_attr or []
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
router = Router(dependencies=extra_router_deps or None)
|
|
61
|
+
|
|
62
|
+
pk_param = "item_id"
|
|
63
|
+
db_dep = getattr(model, TIGRBL_GET_DB_ATTR, None) or _req_state_db
|
|
64
|
+
|
|
65
|
+
raw_nested = _nested_prefix(model) or ""
|
|
66
|
+
nested_pref = re.sub(r"/{2,}", "/", raw_nested).rstrip("/") or ""
|
|
67
|
+
nested_vars = re.findall(r"{(\w+)}", raw_nested)
|
|
68
|
+
|
|
69
|
+
# When models are mounted on nested paths, parent identifiers should not
|
|
70
|
+
# appear in request schemas. Capture the original spec sequence so we can
|
|
71
|
+
# prune request models even if some specs (e.g. ``create`` when
|
|
72
|
+
# ``bulk_create`` is present) are later dropped from the router.
|
|
73
|
+
all_specs = list(specs)
|
|
74
|
+
|
|
75
|
+
if nested_vars:
|
|
76
|
+
schemas_root = getattr(model, "schemas", None)
|
|
77
|
+
if schemas_root:
|
|
78
|
+
for sp in all_specs:
|
|
79
|
+
alias_ns = getattr(schemas_root, sp.alias, None)
|
|
80
|
+
if not alias_ns:
|
|
81
|
+
continue
|
|
82
|
+
in_model = getattr(alias_ns, "in_", None)
|
|
83
|
+
if (
|
|
84
|
+
in_model
|
|
85
|
+
and inspect.isclass(in_model)
|
|
86
|
+
and issubclass(in_model, BaseModel)
|
|
87
|
+
):
|
|
88
|
+
root_field = getattr(in_model, "model_fields", {}).get("root")
|
|
89
|
+
if root_field is not None:
|
|
90
|
+
ann = root_field.annotation
|
|
91
|
+
inner = None
|
|
92
|
+
for t in _get_args(ann) or (ann,):
|
|
93
|
+
origin = _get_origin(t)
|
|
94
|
+
if origin in {list, _typing.List}:
|
|
95
|
+
t_args = _get_args(t)
|
|
96
|
+
if t_args:
|
|
97
|
+
t = t_args[0]
|
|
98
|
+
origin = _get_origin(t)
|
|
99
|
+
if inspect.isclass(t) and issubclass(t, BaseModel):
|
|
100
|
+
inner = t
|
|
101
|
+
break
|
|
102
|
+
if inner is not None:
|
|
103
|
+
pruned = _strip_parent_fields(inner, drop=set(nested_vars))
|
|
104
|
+
setattr(alias_ns, "in_item", pruned)
|
|
105
|
+
setattr(
|
|
106
|
+
alias_ns,
|
|
107
|
+
"in_",
|
|
108
|
+
_make_bulk_rows_model(model, sp.target, pruned),
|
|
109
|
+
)
|
|
110
|
+
continue
|
|
111
|
+
pruned = _strip_parent_fields(in_model, drop=set(nested_vars))
|
|
112
|
+
setattr(alias_ns, "in_", pruned)
|
|
113
|
+
|
|
114
|
+
# If bulk_delete is present, drop clear to avoid route conflicts
|
|
115
|
+
if any(sp.target == "bulk_delete" for sp in specs):
|
|
116
|
+
specs = [sp for sp in specs if sp.target != "clear"]
|
|
117
|
+
|
|
118
|
+
# When both ``create`` and ``bulk_create`` handlers are available,
|
|
119
|
+
# prefer ``bulk_create`` for the REST route to avoid conflicting POST
|
|
120
|
+
# registrations at the collection path. Both operations remain bound
|
|
121
|
+
# for schema generation, but only ``bulk_create`` should surface as a
|
|
122
|
+
# REST endpoint and in the OpenAPI spec.
|
|
123
|
+
if any(sp.target == "bulk_create" for sp in specs) and any(
|
|
124
|
+
sp.target == "create" for sp in specs
|
|
125
|
+
):
|
|
126
|
+
specs = [sp for sp in specs if sp.target != "create"]
|
|
127
|
+
|
|
128
|
+
# Register collection-level bulk routes before member routes so static paths
|
|
129
|
+
# like "/resource/bulk" aren't captured by dynamic member routes such as
|
|
130
|
+
# "/resource/{item_id}". FastAPI matches routes in the order they are
|
|
131
|
+
# added, so sorting here prevents "bulk" from being treated as an
|
|
132
|
+
# identifier.
|
|
133
|
+
specs = sorted(
|
|
134
|
+
specs,
|
|
135
|
+
key=lambda sp: (
|
|
136
|
+
-1
|
|
137
|
+
if sp.target == "clear"
|
|
138
|
+
else 0
|
|
139
|
+
if sp.target in {"bulk_update", "bulk_replace", "bulk_delete", "bulk_merge"}
|
|
140
|
+
else 1
|
|
141
|
+
if sp.target in {"create", "merge"}
|
|
142
|
+
else 2
|
|
143
|
+
if sp.target in {"bulk_create"}
|
|
144
|
+
else 3
|
|
145
|
+
),
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
for sp in specs:
|
|
149
|
+
if not sp.expose_routes:
|
|
150
|
+
continue
|
|
151
|
+
|
|
152
|
+
# Determine path and membership
|
|
153
|
+
if nested_pref:
|
|
154
|
+
if sp.path_suffix is None:
|
|
155
|
+
suffix = _default_path_suffix(sp) or ""
|
|
156
|
+
else:
|
|
157
|
+
suffix = sp.path_suffix or ""
|
|
158
|
+
if suffix and not suffix.startswith("/"):
|
|
159
|
+
suffix = "/" + suffix
|
|
160
|
+
base = nested_pref.rstrip("/")
|
|
161
|
+
if not base.endswith(f"/{resource}"):
|
|
162
|
+
base = f"{base}/{resource}"
|
|
163
|
+
if sp.arity == "member" or sp.target in {
|
|
164
|
+
"read",
|
|
165
|
+
"update",
|
|
166
|
+
"replace",
|
|
167
|
+
"merge",
|
|
168
|
+
"delete",
|
|
169
|
+
}:
|
|
170
|
+
path = f"{base}/{{{pk_param}}}{suffix}"
|
|
171
|
+
is_member = True
|
|
172
|
+
else:
|
|
173
|
+
path = f"{base}{suffix}"
|
|
174
|
+
is_member = False
|
|
175
|
+
else:
|
|
176
|
+
path, is_member = _path_for_spec(
|
|
177
|
+
model, sp, resource=resource, pk_param=pk_param
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
# HARDEN list.in_ at runtime to avoid bogus defaults blowing up empty GETs
|
|
181
|
+
if sp.target == "list":
|
|
182
|
+
schemas_root = getattr(model, "schemas", None)
|
|
183
|
+
if schemas_root:
|
|
184
|
+
alias_ns = getattr(schemas_root, sp.alias, None)
|
|
185
|
+
if alias_ns:
|
|
186
|
+
in_model = getattr(alias_ns, "in_", None)
|
|
187
|
+
if (
|
|
188
|
+
in_model
|
|
189
|
+
and inspect.isclass(in_model)
|
|
190
|
+
and issubclass(in_model, BaseModel)
|
|
191
|
+
and not getattr(in_model, "__tigrbl_optionalized__", False)
|
|
192
|
+
):
|
|
193
|
+
safe = _optionalize_list_in_model(in_model)
|
|
194
|
+
setattr(alias_ns, "in_", safe)
|
|
195
|
+
|
|
196
|
+
# HTTP methods
|
|
197
|
+
methods = list(sp.http_methods or _DEFAULT_METHODS.get(sp.target, ("POST",)))
|
|
198
|
+
response_model = None # Allow hooks to mutate response freely
|
|
199
|
+
|
|
200
|
+
# Build endpoint (split by body/no-body)
|
|
201
|
+
if is_member:
|
|
202
|
+
endpoint = _make_member_endpoint(
|
|
203
|
+
model,
|
|
204
|
+
sp,
|
|
205
|
+
resource=resource,
|
|
206
|
+
db_dep=db_dep,
|
|
207
|
+
pk_param=pk_param,
|
|
208
|
+
nested_vars=nested_vars,
|
|
209
|
+
api=api,
|
|
210
|
+
)
|
|
211
|
+
else:
|
|
212
|
+
endpoint = _make_collection_endpoint(
|
|
213
|
+
model,
|
|
214
|
+
sp,
|
|
215
|
+
resource=resource,
|
|
216
|
+
db_dep=db_dep,
|
|
217
|
+
nested_vars=nested_vars,
|
|
218
|
+
api=api,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
# Status codes
|
|
222
|
+
status_code = _status_for(sp)
|
|
223
|
+
|
|
224
|
+
# Capture OUT schema for OpenAPI without enforcing runtime validation
|
|
225
|
+
alias_ns = getattr(getattr(model, "schemas", None), sp.alias, None)
|
|
226
|
+
out_model = getattr(alias_ns, "out", None) if alias_ns else None
|
|
227
|
+
|
|
228
|
+
responses_meta = dict(_RESPONSES_META)
|
|
229
|
+
if out_model is not None and status_code != _status.HTTP_204_NO_CONTENT:
|
|
230
|
+
responses_meta[status_code] = {"model": out_model}
|
|
231
|
+
response_class = None
|
|
232
|
+
else:
|
|
233
|
+
responses_meta[status_code] = {"description": "Successful Response"}
|
|
234
|
+
response_class = Response
|
|
235
|
+
|
|
236
|
+
# Attach route
|
|
237
|
+
label = f"{model.__name__} - {sp.alias}"
|
|
238
|
+
route_deps = None
|
|
239
|
+
if auth_dep and sp.alias not in allow_anon and sp.target not in allow_anon:
|
|
240
|
+
route_deps = _normalize_deps([auth_dep])
|
|
241
|
+
|
|
242
|
+
unique_id = f"{endpoint.__name__}_{uuid4().hex}"
|
|
243
|
+
include_in_schema = bool(
|
|
244
|
+
getattr(sp, "extra", {}).get("include_in_schema", True)
|
|
245
|
+
)
|
|
246
|
+
route_kwargs = dict(
|
|
247
|
+
path=path,
|
|
248
|
+
endpoint=endpoint,
|
|
249
|
+
methods=methods,
|
|
250
|
+
name=f"{model.__name__}.{sp.alias}",
|
|
251
|
+
operation_id=unique_id,
|
|
252
|
+
summary=label,
|
|
253
|
+
description=label,
|
|
254
|
+
response_model=response_model,
|
|
255
|
+
status_code=status_code,
|
|
256
|
+
# IMPORTANT: only class name here; never table name
|
|
257
|
+
tags=list(sp.tags or (model.__name__,)),
|
|
258
|
+
responses=responses_meta,
|
|
259
|
+
include_in_schema=include_in_schema,
|
|
260
|
+
)
|
|
261
|
+
if route_deps:
|
|
262
|
+
route_kwargs["dependencies"] = route_deps
|
|
263
|
+
if response_class is not None:
|
|
264
|
+
route_kwargs["response_class"] = response_class
|
|
265
|
+
|
|
266
|
+
secdeps: list[Any] = []
|
|
267
|
+
if auth_dep and sp.alias not in allow_anon and sp.target not in allow_anon:
|
|
268
|
+
secdeps.append(auth_dep)
|
|
269
|
+
secdeps.extend(getattr(sp, "secdeps", ()))
|
|
270
|
+
route_secdeps = _normalize_secdeps(secdeps)
|
|
271
|
+
if route_secdeps:
|
|
272
|
+
route_kwargs["dependencies"] = route_secdeps
|
|
273
|
+
|
|
274
|
+
if (
|
|
275
|
+
sp.alias != sp.target
|
|
276
|
+
and sp.target in CANON
|
|
277
|
+
and sp.alias != getattr(sp.handler, "__name__", sp.alias)
|
|
278
|
+
):
|
|
279
|
+
route_kwargs["include_in_schema"] = False
|
|
280
|
+
|
|
281
|
+
router.add_api_route(**route_kwargs)
|
|
282
|
+
|
|
283
|
+
logger.debug(
|
|
284
|
+
"rest: registered %s %s -> %s.%s (response_model=%s)",
|
|
285
|
+
methods,
|
|
286
|
+
path,
|
|
287
|
+
model.__name__,
|
|
288
|
+
sp.alias,
|
|
289
|
+
getattr(response_model, "__name__", None) if response_model else None,
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
return router
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
from types import SimpleNamespace
|
|
5
|
+
from typing import Any, Dict, Optional, Sequence, Tuple
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
from .fastapi import Depends, Security, _status
|
|
9
|
+
from ...op import OpSpec
|
|
10
|
+
from ...op.types import CANON
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger("uvicorn")
|
|
13
|
+
logger.debug("Loaded module v3/bindings/rest/routing")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _normalize_deps(deps: Optional[Sequence[Any]]) -> list[Any]:
|
|
17
|
+
"""Turn callables into Depends(...) unless already a dependency object."""
|
|
18
|
+
if not deps:
|
|
19
|
+
return []
|
|
20
|
+
out: list[Any] = []
|
|
21
|
+
for d in deps:
|
|
22
|
+
is_dep_obj = getattr(d, "dependency", None) is not None
|
|
23
|
+
out.append(d if is_dep_obj else Depends(d))
|
|
24
|
+
return out
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _normalize_secdeps(secdeps: Optional[Sequence[Any]]) -> list[Any]:
|
|
28
|
+
"""Turn callables into Security(...) unless already a dependency object."""
|
|
29
|
+
if not secdeps:
|
|
30
|
+
return []
|
|
31
|
+
out: list[Any] = []
|
|
32
|
+
for d in secdeps:
|
|
33
|
+
is_dep_obj = getattr(d, "dependency", None) is not None
|
|
34
|
+
out.append(d if is_dep_obj else Security(d))
|
|
35
|
+
return out
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _status_for(sp: OpSpec) -> int:
|
|
39
|
+
if sp.status_code is not None:
|
|
40
|
+
return sp.status_code
|
|
41
|
+
target = sp.target
|
|
42
|
+
if target == "create":
|
|
43
|
+
return _status.HTTP_201_CREATED
|
|
44
|
+
if target in ("delete", "clear"):
|
|
45
|
+
return _status.HTTP_200_OK
|
|
46
|
+
return _status.HTTP_200_OK
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
_RESPONSES_META = {
|
|
50
|
+
400: {"description": "Bad Request"},
|
|
51
|
+
401: {"description": "Unauthorized"},
|
|
52
|
+
403: {"description": "Forbidden"},
|
|
53
|
+
404: {"description": "Not Found"},
|
|
54
|
+
409: {"description": "Conflict"},
|
|
55
|
+
422: {"description": "Unprocessable Entity"},
|
|
56
|
+
429: {"description": "Too Many Requests"},
|
|
57
|
+
500: {"description": "Internal Server Error"},
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
_DEFAULT_METHODS: Dict[str, Tuple[str, ...]] = {
|
|
62
|
+
"create": ("POST",),
|
|
63
|
+
"read": ("GET",),
|
|
64
|
+
"update": ("PATCH",),
|
|
65
|
+
"replace": ("PUT",),
|
|
66
|
+
"merge": ("PATCH",),
|
|
67
|
+
"delete": ("DELETE",),
|
|
68
|
+
"list": ("GET",),
|
|
69
|
+
"clear": ("DELETE",),
|
|
70
|
+
"bulk_create": ("POST",),
|
|
71
|
+
"bulk_update": ("PATCH",),
|
|
72
|
+
"bulk_replace": ("PUT",),
|
|
73
|
+
"bulk_merge": ("PATCH",),
|
|
74
|
+
"bulk_delete": ("DELETE",),
|
|
75
|
+
"custom": ("POST",),
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _default_path_suffix(sp: OpSpec) -> str | None:
|
|
80
|
+
if sp.target.startswith("bulk_"):
|
|
81
|
+
return None
|
|
82
|
+
if sp.alias != sp.target and (
|
|
83
|
+
sp.target in {"create", "custom"} or sp.target not in CANON
|
|
84
|
+
):
|
|
85
|
+
return f"/{sp.alias}"
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _path_for_spec(
|
|
90
|
+
model: type, sp: OpSpec, *, resource: str, pk_param: str = "item_id"
|
|
91
|
+
) -> Tuple[str, bool]:
|
|
92
|
+
if sp.path_suffix is None:
|
|
93
|
+
suffix = _default_path_suffix(sp) or ""
|
|
94
|
+
else:
|
|
95
|
+
suffix = sp.path_suffix or ""
|
|
96
|
+
if suffix and not suffix.startswith("/"):
|
|
97
|
+
suffix = "/" + suffix
|
|
98
|
+
|
|
99
|
+
if sp.target == "create":
|
|
100
|
+
return f"/{resource}{suffix}", False
|
|
101
|
+
if sp.arity == "member" or sp.target in {
|
|
102
|
+
"read",
|
|
103
|
+
"update",
|
|
104
|
+
"replace",
|
|
105
|
+
"merge",
|
|
106
|
+
"delete",
|
|
107
|
+
}:
|
|
108
|
+
return f"/{resource}/{{{pk_param}}}{suffix}", True
|
|
109
|
+
return f"/{resource}{suffix}", False
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _response_model_for(sp: OpSpec, model: type) -> Any | None:
|
|
113
|
+
if sp.target == "delete":
|
|
114
|
+
return None
|
|
115
|
+
alias_ns = getattr(
|
|
116
|
+
getattr(model, "schemas", None) or SimpleNamespace(), sp.alias, None
|
|
117
|
+
)
|
|
118
|
+
out_model = getattr(alias_ns, "out", None)
|
|
119
|
+
if out_model is None:
|
|
120
|
+
return None
|
|
121
|
+
if sp.target == "list":
|
|
122
|
+
try:
|
|
123
|
+
return list[out_model] # type: ignore[index]
|
|
124
|
+
except Exception:
|
|
125
|
+
return None
|
|
126
|
+
return out_model
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _request_model_for(sp: OpSpec, model: type) -> Any | None:
|
|
130
|
+
alias_ns = getattr(
|
|
131
|
+
getattr(model, "schemas", None) or SimpleNamespace(), sp.alias, None
|
|
132
|
+
)
|
|
133
|
+
return getattr(alias_ns, "in_", None)
|