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,317 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
import inspect
|
|
5
|
+
from types import SimpleNamespace
|
|
6
|
+
from typing import Any, Dict, Mapping, Sequence
|
|
7
|
+
from typing import get_origin as _get_origin, get_args as _get_args
|
|
8
|
+
import typing as _typing
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, Field, create_model
|
|
11
|
+
|
|
12
|
+
from .fastapi import HTTPException, Query, Request, _status
|
|
13
|
+
from .helpers import _ensure_jsonable
|
|
14
|
+
from ...op import OpSpec
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger("uvicorn")
|
|
17
|
+
logger.debug("Loaded module v3/bindings/rest/io")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _serialize_output(
|
|
21
|
+
model: type, alias: str, target: str, sp: OpSpec, result: Any
|
|
22
|
+
) -> Any:
|
|
23
|
+
"""
|
|
24
|
+
If a response schema exists (model.schemas.<alias>.out), serialize to it.
|
|
25
|
+
Otherwise, attempt a best-effort conversion to primitive types so FastAPI
|
|
26
|
+
can JSON-encode the response.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from ...types import Response as _Response # local import to avoid cycles
|
|
30
|
+
|
|
31
|
+
if isinstance(result, _Response):
|
|
32
|
+
return result
|
|
33
|
+
|
|
34
|
+
def _final(val: Any) -> Any:
|
|
35
|
+
if target == "list" and isinstance(val, (list, tuple)):
|
|
36
|
+
return [_ensure_jsonable(v) for v in val]
|
|
37
|
+
return _ensure_jsonable(val)
|
|
38
|
+
|
|
39
|
+
schemas_root = getattr(model, "schemas", None)
|
|
40
|
+
if not schemas_root:
|
|
41
|
+
return _final(result)
|
|
42
|
+
alias_ns = getattr(schemas_root, alias, None)
|
|
43
|
+
if not alias_ns:
|
|
44
|
+
return _final(result)
|
|
45
|
+
out_model = getattr(alias_ns, "out", None)
|
|
46
|
+
if (
|
|
47
|
+
not out_model
|
|
48
|
+
or not inspect.isclass(out_model)
|
|
49
|
+
or not issubclass(out_model, BaseModel)
|
|
50
|
+
):
|
|
51
|
+
return _final(result)
|
|
52
|
+
try:
|
|
53
|
+
if target == "list" and isinstance(result, (list, tuple)):
|
|
54
|
+
return [
|
|
55
|
+
out_model.model_validate(x).model_dump(
|
|
56
|
+
exclude_none=False, by_alias=True
|
|
57
|
+
)
|
|
58
|
+
for x in result
|
|
59
|
+
]
|
|
60
|
+
return out_model.model_validate(result).model_dump(
|
|
61
|
+
exclude_none=False, by_alias=True
|
|
62
|
+
)
|
|
63
|
+
except Exception:
|
|
64
|
+
logger.debug(
|
|
65
|
+
"rest output serialization failed for %s.%s",
|
|
66
|
+
model.__name__,
|
|
67
|
+
alias,
|
|
68
|
+
exc_info=True,
|
|
69
|
+
)
|
|
70
|
+
return _final(result)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _validate_body(
|
|
74
|
+
model: type, alias: str, target: str, body: Any | None
|
|
75
|
+
) -> Mapping[str, Any] | Sequence[Mapping[str, Any]]:
|
|
76
|
+
"""Normalize and validate the incoming request body."""
|
|
77
|
+
if isinstance(body, BaseModel):
|
|
78
|
+
return body.model_dump(exclude_none=True)
|
|
79
|
+
|
|
80
|
+
if target in {"bulk_create", "bulk_update", "bulk_replace", "bulk_merge"}:
|
|
81
|
+
items: Sequence[Any] = body or []
|
|
82
|
+
if not isinstance(items, Sequence) or isinstance(items, (str, bytes)):
|
|
83
|
+
items = []
|
|
84
|
+
|
|
85
|
+
schemas_root = getattr(model, "schemas", None)
|
|
86
|
+
alias_ns = getattr(schemas_root, alias, None) if schemas_root else None
|
|
87
|
+
in_item = getattr(alias_ns, "in_item", None) if alias_ns else None
|
|
88
|
+
|
|
89
|
+
out: list[Mapping[str, Any]] = []
|
|
90
|
+
for item in items:
|
|
91
|
+
if isinstance(item, BaseModel):
|
|
92
|
+
out.append(item.model_dump(exclude_none=True))
|
|
93
|
+
continue
|
|
94
|
+
data: Mapping[str, Any] | None = None
|
|
95
|
+
if in_item and inspect.isclass(in_item) and issubclass(in_item, BaseModel):
|
|
96
|
+
try:
|
|
97
|
+
inst = in_item.model_validate(item) # type: ignore[arg-type]
|
|
98
|
+
data = inst.model_dump(exclude_none=True)
|
|
99
|
+
except Exception:
|
|
100
|
+
logger.debug(
|
|
101
|
+
"rest input body validation failed for %s.%s",
|
|
102
|
+
model.__name__,
|
|
103
|
+
alias,
|
|
104
|
+
exc_info=True,
|
|
105
|
+
)
|
|
106
|
+
if data is None:
|
|
107
|
+
data = dict(item) if isinstance(item, Mapping) else {}
|
|
108
|
+
out.append(data)
|
|
109
|
+
return out
|
|
110
|
+
|
|
111
|
+
if (
|
|
112
|
+
target in {"create", "update", "replace", "merge"}
|
|
113
|
+
and isinstance(body, Sequence)
|
|
114
|
+
and not isinstance(body, (str, bytes, Mapping))
|
|
115
|
+
):
|
|
116
|
+
bulk_target = f"bulk_{target}"
|
|
117
|
+
items: Sequence[Any] = body
|
|
118
|
+
schemas_root = getattr(model, "schemas", None)
|
|
119
|
+
alias_ns = getattr(schemas_root, bulk_target, None) if schemas_root else None
|
|
120
|
+
in_item = getattr(alias_ns, "in_item", None) if alias_ns else None
|
|
121
|
+
|
|
122
|
+
out: list[Mapping[str, Any]] = []
|
|
123
|
+
for item in items:
|
|
124
|
+
if isinstance(item, BaseModel):
|
|
125
|
+
out.append(item.model_dump(exclude_none=True))
|
|
126
|
+
continue
|
|
127
|
+
data: Mapping[str, Any] | None = None
|
|
128
|
+
if in_item and inspect.isclass(in_item) and issubclass(in_item, BaseModel):
|
|
129
|
+
try:
|
|
130
|
+
inst = in_item.model_validate(item) # type: ignore[arg-type]
|
|
131
|
+
data = inst.model_dump(exclude_none=True)
|
|
132
|
+
except Exception:
|
|
133
|
+
logger.debug(
|
|
134
|
+
"rest input body validation failed for %s.%s",
|
|
135
|
+
model.__name__,
|
|
136
|
+
bulk_target,
|
|
137
|
+
exc_info=True,
|
|
138
|
+
)
|
|
139
|
+
if data is None:
|
|
140
|
+
data = dict(item) if isinstance(item, Mapping) else {}
|
|
141
|
+
out.append(data)
|
|
142
|
+
return out
|
|
143
|
+
|
|
144
|
+
body = body or {}
|
|
145
|
+
if not isinstance(body, Mapping):
|
|
146
|
+
body = {}
|
|
147
|
+
|
|
148
|
+
schemas_root = getattr(model, "schemas", None)
|
|
149
|
+
if not schemas_root:
|
|
150
|
+
return dict(body)
|
|
151
|
+
alias_ns = getattr(schemas_root, alias, None)
|
|
152
|
+
if not alias_ns:
|
|
153
|
+
return dict(body)
|
|
154
|
+
in_model = getattr(alias_ns, "in_", None)
|
|
155
|
+
|
|
156
|
+
if in_model and inspect.isclass(in_model) and issubclass(in_model, BaseModel):
|
|
157
|
+
try:
|
|
158
|
+
inst = in_model.model_validate(body) # type: ignore[arg-type]
|
|
159
|
+
return inst.model_dump(exclude_none=True)
|
|
160
|
+
except Exception as e:
|
|
161
|
+
logger.debug(
|
|
162
|
+
"rest input body validation failed for %s.%s",
|
|
163
|
+
model.__name__,
|
|
164
|
+
alias,
|
|
165
|
+
exc_info=True,
|
|
166
|
+
)
|
|
167
|
+
raise HTTPException(
|
|
168
|
+
status_code=_status.HTTP_422_UNPROCESSABLE_ENTITY,
|
|
169
|
+
detail=str(e),
|
|
170
|
+
)
|
|
171
|
+
return dict(body)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _validate_query(
|
|
175
|
+
model: type, alias: str, target: str, query: Mapping[str, Any]
|
|
176
|
+
) -> Mapping[str, Any]:
|
|
177
|
+
"""Validate list/clear inputs coming from the query string."""
|
|
178
|
+
if not query or (isinstance(query, Mapping) and len(query) == 0):
|
|
179
|
+
return {}
|
|
180
|
+
|
|
181
|
+
schemas_root = getattr(model, "schemas", None)
|
|
182
|
+
if not schemas_root:
|
|
183
|
+
return dict(query)
|
|
184
|
+
alias_ns = getattr(schemas_root, alias, None)
|
|
185
|
+
if not alias_ns:
|
|
186
|
+
return dict(query)
|
|
187
|
+
in_model = getattr(alias_ns, "in_", None)
|
|
188
|
+
|
|
189
|
+
if in_model and inspect.isclass(in_model) and issubclass(in_model, BaseModel):
|
|
190
|
+
try:
|
|
191
|
+
fields = getattr(in_model, "model_fields", {})
|
|
192
|
+
data: Dict[str, Any] = {}
|
|
193
|
+
for name, f in fields.items():
|
|
194
|
+
alias_key = getattr(f, "alias", None) or name
|
|
195
|
+
if alias_key in query:
|
|
196
|
+
val = query[alias_key]
|
|
197
|
+
elif name in query:
|
|
198
|
+
val = query[name]
|
|
199
|
+
else:
|
|
200
|
+
continue
|
|
201
|
+
if val is None:
|
|
202
|
+
continue
|
|
203
|
+
if isinstance(val, str) and not val.strip():
|
|
204
|
+
continue
|
|
205
|
+
if isinstance(val, (list, tuple, set)) and len(val) == 0:
|
|
206
|
+
continue
|
|
207
|
+
data[name] = val
|
|
208
|
+
|
|
209
|
+
if not data:
|
|
210
|
+
return {}
|
|
211
|
+
|
|
212
|
+
inst = in_model.model_validate(data) # type: ignore[arg-type]
|
|
213
|
+
return inst.model_dump(exclude_none=True)
|
|
214
|
+
except Exception as e:
|
|
215
|
+
raise HTTPException(
|
|
216
|
+
status_code=_status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(e)
|
|
217
|
+
)
|
|
218
|
+
return dict(query)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _strip_optional(t: Any) -> Any:
|
|
222
|
+
"""If annotation is Optional[T] return T; else return the input."""
|
|
223
|
+
origin = _get_origin(t)
|
|
224
|
+
if origin is _typing.Union:
|
|
225
|
+
args = tuple(a for a in _get_args(t) if a is not type(None))
|
|
226
|
+
return args[0] if len(args) == 1 else Any
|
|
227
|
+
return t
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _make_list_query_dep(model: type, alias: str):
|
|
231
|
+
"""Build a dependency exposing Query(...) params from schemas.<alias>.in_."""
|
|
232
|
+
alias_ns = getattr(
|
|
233
|
+
getattr(model, "schemas", None) or SimpleNamespace(), alias, None
|
|
234
|
+
)
|
|
235
|
+
in_model = getattr(alias_ns, "in_", None)
|
|
236
|
+
|
|
237
|
+
if not (in_model and inspect.isclass(in_model) and issubclass(in_model, BaseModel)):
|
|
238
|
+
|
|
239
|
+
def _dep(request: Request) -> Dict[str, Any]:
|
|
240
|
+
return dict(request.query_params)
|
|
241
|
+
|
|
242
|
+
_dep.__name__ = f"list_params_{model.__name__}_{alias}"
|
|
243
|
+
return _dep
|
|
244
|
+
|
|
245
|
+
fields = getattr(in_model, "model_fields", {})
|
|
246
|
+
|
|
247
|
+
def _dep(**raw: Any) -> Dict[str, Any]:
|
|
248
|
+
"""Collect only user-supplied values; never apply schema defaults here."""
|
|
249
|
+
data: Dict[str, Any] = {}
|
|
250
|
+
for name, f in fields.items():
|
|
251
|
+
key = getattr(f, "alias", None) or name
|
|
252
|
+
if key not in raw:
|
|
253
|
+
continue
|
|
254
|
+
val = raw[key]
|
|
255
|
+
if val is None:
|
|
256
|
+
continue
|
|
257
|
+
if isinstance(val, str) and not val.strip():
|
|
258
|
+
continue
|
|
259
|
+
if isinstance(val, (list, tuple, set)) and len(val) == 0:
|
|
260
|
+
continue
|
|
261
|
+
data[key] = val
|
|
262
|
+
return data
|
|
263
|
+
|
|
264
|
+
params: list[inspect.Parameter] = []
|
|
265
|
+
for name, f in fields.items():
|
|
266
|
+
key = getattr(f, "alias", None) or name
|
|
267
|
+
ann = getattr(f, "annotation", Any)
|
|
268
|
+
base = _strip_optional(ann)
|
|
269
|
+
origin = _get_origin(base)
|
|
270
|
+
if origin in (list, tuple, set):
|
|
271
|
+
inner = (_get_args(base) or (str,))[0]
|
|
272
|
+
annotation = list[inner] | None # type: ignore[index]
|
|
273
|
+
else:
|
|
274
|
+
annotation = base | None
|
|
275
|
+
default_q = Query(None, description=getattr(f, "description", None))
|
|
276
|
+
params.append(
|
|
277
|
+
inspect.Parameter(
|
|
278
|
+
name=key,
|
|
279
|
+
kind=inspect.Parameter.KEYWORD_ONLY,
|
|
280
|
+
default=default_q,
|
|
281
|
+
annotation=annotation,
|
|
282
|
+
)
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
_dep.__signature__ = inspect.Signature(
|
|
286
|
+
parameters=params, return_annotation=Dict[str, Any]
|
|
287
|
+
)
|
|
288
|
+
_dep.__name__ = f"list_params_{model.__name__}_{alias}"
|
|
289
|
+
return _dep
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _optionalize_list_in_model(in_model: type[BaseModel]) -> type[BaseModel]:
|
|
293
|
+
"""Make every field Optional[...] with default=None."""
|
|
294
|
+
try:
|
|
295
|
+
fields = getattr(in_model, "model_fields", {})
|
|
296
|
+
except Exception:
|
|
297
|
+
return in_model
|
|
298
|
+
|
|
299
|
+
defs: Dict[str, tuple[Any, Any]] = {}
|
|
300
|
+
for name, f in fields.items():
|
|
301
|
+
ann = getattr(f, "annotation", Any)
|
|
302
|
+
opt_ann = _typing.Union[ann, type(None)]
|
|
303
|
+
defs[name] = (
|
|
304
|
+
opt_ann,
|
|
305
|
+
Field(
|
|
306
|
+
default=None,
|
|
307
|
+
alias=getattr(f, "alias", None),
|
|
308
|
+
description=getattr(f, "description", None),
|
|
309
|
+
),
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
New = create_model( # type: ignore[misc]
|
|
313
|
+
f"{in_model.__name__}__Optionalized",
|
|
314
|
+
**defs,
|
|
315
|
+
)
|
|
316
|
+
setattr(New, "__tigrbl_optionalized__", True)
|
|
317
|
+
return New
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
import inspect
|
|
5
|
+
from types import SimpleNamespace
|
|
6
|
+
from typing import (
|
|
7
|
+
Annotated,
|
|
8
|
+
Any,
|
|
9
|
+
Awaitable,
|
|
10
|
+
Callable,
|
|
11
|
+
Dict,
|
|
12
|
+
Mapping,
|
|
13
|
+
Optional,
|
|
14
|
+
Sequence,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
from .common import (
|
|
18
|
+
TIGRBL_AUTH_CONTEXT_ATTR,
|
|
19
|
+
Body,
|
|
20
|
+
Depends,
|
|
21
|
+
HTTPException,
|
|
22
|
+
Response,
|
|
23
|
+
Path,
|
|
24
|
+
Request,
|
|
25
|
+
OpSpec,
|
|
26
|
+
_coerce_parent_kw,
|
|
27
|
+
_get_phase_chains,
|
|
28
|
+
_pk_name,
|
|
29
|
+
_pk_names,
|
|
30
|
+
_request_model_for,
|
|
31
|
+
_serialize_output,
|
|
32
|
+
_validate_body,
|
|
33
|
+
_executor,
|
|
34
|
+
_status,
|
|
35
|
+
_status_for,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
from ...runtime.executor.types import _Ctx
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
logger = logging.getLogger("uvicorn")
|
|
42
|
+
logger.debug("Loaded module v3/bindings/rest/member")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _make_member_endpoint(
|
|
46
|
+
model: type,
|
|
47
|
+
sp: OpSpec,
|
|
48
|
+
*,
|
|
49
|
+
resource: str,
|
|
50
|
+
db_dep: Callable[..., Any],
|
|
51
|
+
pk_param: str = "item_id",
|
|
52
|
+
nested_vars: Sequence[str] | None = None,
|
|
53
|
+
api: Any | None = None,
|
|
54
|
+
) -> Callable[..., Awaitable[Any]]:
|
|
55
|
+
alias = sp.alias
|
|
56
|
+
target = sp.target
|
|
57
|
+
status_code = _status_for(sp)
|
|
58
|
+
real_pk = _pk_name(model)
|
|
59
|
+
pk_names = _pk_names(model)
|
|
60
|
+
nested_vars = list(nested_vars or [])
|
|
61
|
+
|
|
62
|
+
# --- No body on GET read / DELETE delete ---
|
|
63
|
+
if target in {"read", "delete"}:
|
|
64
|
+
|
|
65
|
+
async def _endpoint(
|
|
66
|
+
item_id: Any,
|
|
67
|
+
request: Request,
|
|
68
|
+
db: Any = Depends(db_dep),
|
|
69
|
+
**kw: Any,
|
|
70
|
+
):
|
|
71
|
+
parent_kw = {k: kw[k] for k in nested_vars if k in kw}
|
|
72
|
+
_coerce_parent_kw(model, parent_kw)
|
|
73
|
+
payload: Mapping[str, Any] = dict(parent_kw)
|
|
74
|
+
path_params = {real_pk: item_id, pk_param: item_id, **parent_kw}
|
|
75
|
+
ctx: Dict[str, Any] = {
|
|
76
|
+
"request": request,
|
|
77
|
+
"db": db,
|
|
78
|
+
"payload": payload,
|
|
79
|
+
"path_params": path_params,
|
|
80
|
+
# expose contextual metadata for downstream atoms
|
|
81
|
+
"api": api if api is not None else getattr(request, "app", None),
|
|
82
|
+
"app": getattr(request, "app", None),
|
|
83
|
+
"model": model,
|
|
84
|
+
"op": alias,
|
|
85
|
+
"method": alias,
|
|
86
|
+
"target": target,
|
|
87
|
+
"env": SimpleNamespace(
|
|
88
|
+
method=alias, params=payload, target=target, model=model
|
|
89
|
+
),
|
|
90
|
+
}
|
|
91
|
+
ac = getattr(request.state, TIGRBL_AUTH_CONTEXT_ATTR, None)
|
|
92
|
+
if ac is not None:
|
|
93
|
+
ctx["auth_context"] = ac
|
|
94
|
+
ctx = _Ctx(ctx)
|
|
95
|
+
|
|
96
|
+
def _serializer(r, _ctx=ctx):
|
|
97
|
+
out = _serialize_output(model, alias, target, sp, r)
|
|
98
|
+
temp = getattr(_ctx, "temp", {}) if isinstance(_ctx, Mapping) else {}
|
|
99
|
+
extras = (
|
|
100
|
+
temp.get("response_extras", {}) if isinstance(temp, Mapping) else {}
|
|
101
|
+
)
|
|
102
|
+
if isinstance(out, dict) and isinstance(extras, dict):
|
|
103
|
+
out.update(extras)
|
|
104
|
+
return out
|
|
105
|
+
|
|
106
|
+
ctx["response_serializer"] = _serializer
|
|
107
|
+
phases = _get_phase_chains(model, alias)
|
|
108
|
+
result = await _executor._invoke(
|
|
109
|
+
request=request,
|
|
110
|
+
db=db,
|
|
111
|
+
phases=phases,
|
|
112
|
+
ctx=ctx,
|
|
113
|
+
)
|
|
114
|
+
if isinstance(result, Response):
|
|
115
|
+
if sp.status_code is not None or result.status_code == 200:
|
|
116
|
+
result.status_code = status_code
|
|
117
|
+
return result
|
|
118
|
+
return result
|
|
119
|
+
|
|
120
|
+
params = [
|
|
121
|
+
inspect.Parameter(
|
|
122
|
+
nv,
|
|
123
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
124
|
+
annotation=Annotated[str, Path(...)],
|
|
125
|
+
)
|
|
126
|
+
for nv in nested_vars
|
|
127
|
+
]
|
|
128
|
+
params.extend(
|
|
129
|
+
[
|
|
130
|
+
inspect.Parameter(
|
|
131
|
+
"item_id",
|
|
132
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
133
|
+
annotation=Annotated[Any, Path(...)],
|
|
134
|
+
),
|
|
135
|
+
inspect.Parameter(
|
|
136
|
+
"request",
|
|
137
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
138
|
+
annotation=Request,
|
|
139
|
+
),
|
|
140
|
+
inspect.Parameter(
|
|
141
|
+
"db",
|
|
142
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
143
|
+
annotation=Annotated[Any, Depends(db_dep)],
|
|
144
|
+
),
|
|
145
|
+
]
|
|
146
|
+
)
|
|
147
|
+
_endpoint.__signature__ = inspect.Signature(params)
|
|
148
|
+
|
|
149
|
+
_endpoint.__name__ = f"rest_{model.__name__}_{alias}_member"
|
|
150
|
+
_endpoint.__qualname__ = _endpoint.__name__
|
|
151
|
+
_endpoint.__doc__ = (
|
|
152
|
+
f"REST member endpoint for {model.__name__}.{alias} ({target})"
|
|
153
|
+
)
|
|
154
|
+
# NOTE: do NOT set body annotation for no-body endpoints
|
|
155
|
+
return _endpoint
|
|
156
|
+
|
|
157
|
+
body_model = _request_model_for(sp, model)
|
|
158
|
+
if body_model is None and sp.request_model is None and target == "custom":
|
|
159
|
+
|
|
160
|
+
async def _endpoint(
|
|
161
|
+
item_id: Any,
|
|
162
|
+
request: Request,
|
|
163
|
+
db: Any = Depends(db_dep),
|
|
164
|
+
**kw: Any,
|
|
165
|
+
):
|
|
166
|
+
parent_kw = {k: kw[k] for k in nested_vars if k in kw}
|
|
167
|
+
_coerce_parent_kw(model, parent_kw)
|
|
168
|
+
payload: Mapping[str, Any] = dict(parent_kw)
|
|
169
|
+
path_params = {real_pk: item_id, pk_param: item_id, **parent_kw}
|
|
170
|
+
ctx: Dict[str, Any] = {
|
|
171
|
+
"request": request,
|
|
172
|
+
"db": db,
|
|
173
|
+
"payload": payload,
|
|
174
|
+
"path_params": path_params,
|
|
175
|
+
# expose contextual metadata for downstream atoms
|
|
176
|
+
"api": api if api is not None else getattr(request, "app", None),
|
|
177
|
+
"app": getattr(request, "app", None),
|
|
178
|
+
"model": model,
|
|
179
|
+
"op": alias,
|
|
180
|
+
"method": alias,
|
|
181
|
+
"target": target,
|
|
182
|
+
"env": SimpleNamespace(
|
|
183
|
+
method=alias, params=payload, target=target, model=model
|
|
184
|
+
),
|
|
185
|
+
}
|
|
186
|
+
ac = getattr(request.state, TIGRBL_AUTH_CONTEXT_ATTR, None)
|
|
187
|
+
if ac is not None:
|
|
188
|
+
ctx["auth_context"] = ac
|
|
189
|
+
ctx = _Ctx(ctx)
|
|
190
|
+
|
|
191
|
+
def _serializer(r, _ctx=ctx):
|
|
192
|
+
out = _serialize_output(model, alias, target, sp, r)
|
|
193
|
+
temp = getattr(_ctx, "temp", {}) if isinstance(_ctx, Mapping) else {}
|
|
194
|
+
extras = (
|
|
195
|
+
temp.get("response_extras", {}) if isinstance(temp, Mapping) else {}
|
|
196
|
+
)
|
|
197
|
+
if isinstance(out, dict) and isinstance(extras, dict):
|
|
198
|
+
out.update(extras)
|
|
199
|
+
return out
|
|
200
|
+
|
|
201
|
+
ctx["response_serializer"] = _serializer
|
|
202
|
+
phases = _get_phase_chains(model, alias)
|
|
203
|
+
result = await _executor._invoke(
|
|
204
|
+
request=request,
|
|
205
|
+
db=db,
|
|
206
|
+
phases=phases,
|
|
207
|
+
ctx=ctx,
|
|
208
|
+
)
|
|
209
|
+
return result
|
|
210
|
+
|
|
211
|
+
params = [
|
|
212
|
+
inspect.Parameter(
|
|
213
|
+
nv,
|
|
214
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
215
|
+
annotation=Annotated[str, Path(...)],
|
|
216
|
+
)
|
|
217
|
+
for nv in nested_vars
|
|
218
|
+
]
|
|
219
|
+
params.extend(
|
|
220
|
+
[
|
|
221
|
+
inspect.Parameter(
|
|
222
|
+
"item_id",
|
|
223
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
224
|
+
annotation=Annotated[Any, Path(...)],
|
|
225
|
+
),
|
|
226
|
+
inspect.Parameter(
|
|
227
|
+
"request",
|
|
228
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
229
|
+
annotation=Request,
|
|
230
|
+
),
|
|
231
|
+
inspect.Parameter(
|
|
232
|
+
"db",
|
|
233
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
234
|
+
annotation=Annotated[Any, Depends(db_dep)],
|
|
235
|
+
),
|
|
236
|
+
]
|
|
237
|
+
)
|
|
238
|
+
_endpoint.__signature__ = inspect.Signature(params)
|
|
239
|
+
|
|
240
|
+
_endpoint.__name__ = f"rest_{model.__name__}_{alias}_member"
|
|
241
|
+
_endpoint.__qualname__ = _endpoint.__name__
|
|
242
|
+
_endpoint.__doc__ = (
|
|
243
|
+
f"REST member endpoint for {model.__name__}.{alias} ({target})"
|
|
244
|
+
)
|
|
245
|
+
return _endpoint
|
|
246
|
+
|
|
247
|
+
# --- Body-based member endpoints: PATCH update / PUT replace (and custom member) ---
|
|
248
|
+
|
|
249
|
+
if body_model is None:
|
|
250
|
+
body_annotation = Optional[Mapping[str, Any]]
|
|
251
|
+
body_default = Body(None)
|
|
252
|
+
else:
|
|
253
|
+
body_annotation = body_model
|
|
254
|
+
body_default = Body(...)
|
|
255
|
+
|
|
256
|
+
async def _endpoint(
|
|
257
|
+
item_id: Any,
|
|
258
|
+
request: Request,
|
|
259
|
+
db: Any = Depends(db_dep),
|
|
260
|
+
body=body_default,
|
|
261
|
+
**kw: Any,
|
|
262
|
+
):
|
|
263
|
+
parent_kw = {k: kw[k] for k in nested_vars if k in kw}
|
|
264
|
+
_coerce_parent_kw(model, parent_kw)
|
|
265
|
+
payload = _validate_body(model, alias, target, body)
|
|
266
|
+
|
|
267
|
+
# Enforce path-PK canonicality. If body echoes PK: drop if equal, 409 if mismatch.
|
|
268
|
+
for k in pk_names:
|
|
269
|
+
if k in payload:
|
|
270
|
+
if str(payload[k]) != str(item_id) and len(pk_names) == 1:
|
|
271
|
+
raise HTTPException(
|
|
272
|
+
status_code=_status.HTTP_409_CONFLICT,
|
|
273
|
+
detail=f"Identifier mismatch for '{k}': path={item_id}, body={payload[k]}",
|
|
274
|
+
)
|
|
275
|
+
payload.pop(k, None)
|
|
276
|
+
payload.pop(pk_param, None)
|
|
277
|
+
if parent_kw:
|
|
278
|
+
payload.update(parent_kw)
|
|
279
|
+
|
|
280
|
+
path_params = {real_pk: item_id, pk_param: item_id, **parent_kw}
|
|
281
|
+
|
|
282
|
+
ctx: Dict[str, Any] = {
|
|
283
|
+
"request": request,
|
|
284
|
+
"db": db,
|
|
285
|
+
"payload": payload,
|
|
286
|
+
"path_params": path_params,
|
|
287
|
+
# expose contextual metadata for downstream atoms
|
|
288
|
+
"app": getattr(request, "app", None),
|
|
289
|
+
"api": getattr(request, "app", None),
|
|
290
|
+
"model": model,
|
|
291
|
+
"op": alias,
|
|
292
|
+
"method": alias,
|
|
293
|
+
"target": target,
|
|
294
|
+
"env": SimpleNamespace(
|
|
295
|
+
method=alias, params=payload, target=target, model=model
|
|
296
|
+
),
|
|
297
|
+
}
|
|
298
|
+
ac = getattr(request.state, TIGRBL_AUTH_CONTEXT_ATTR, None)
|
|
299
|
+
if ac is not None:
|
|
300
|
+
ctx["auth_context"] = ac
|
|
301
|
+
ctx = _Ctx(ctx)
|
|
302
|
+
|
|
303
|
+
def _serializer(r, _ctx=ctx):
|
|
304
|
+
out = _serialize_output(model, alias, target, sp, r)
|
|
305
|
+
temp = getattr(_ctx, "temp", {}) if isinstance(_ctx, Mapping) else {}
|
|
306
|
+
extras = (
|
|
307
|
+
temp.get("response_extras", {}) if isinstance(temp, Mapping) else {}
|
|
308
|
+
)
|
|
309
|
+
if isinstance(out, dict) and isinstance(extras, dict):
|
|
310
|
+
out.update(extras)
|
|
311
|
+
return out
|
|
312
|
+
|
|
313
|
+
ctx["response_serializer"] = _serializer
|
|
314
|
+
phases = _get_phase_chains(model, alias)
|
|
315
|
+
result = await _executor._invoke(
|
|
316
|
+
request=request,
|
|
317
|
+
db=db,
|
|
318
|
+
phases=phases,
|
|
319
|
+
ctx=ctx,
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
if isinstance(result, Response):
|
|
323
|
+
if sp.status_code is not None or result.status_code == 200:
|
|
324
|
+
result.status_code = status_code
|
|
325
|
+
return result
|
|
326
|
+
return result
|
|
327
|
+
|
|
328
|
+
params = [
|
|
329
|
+
inspect.Parameter(
|
|
330
|
+
nv,
|
|
331
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
332
|
+
annotation=Annotated[str, Path(...)],
|
|
333
|
+
)
|
|
334
|
+
for nv in nested_vars
|
|
335
|
+
]
|
|
336
|
+
params.extend(
|
|
337
|
+
[
|
|
338
|
+
inspect.Parameter(
|
|
339
|
+
"item_id",
|
|
340
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
341
|
+
annotation=Annotated[Any, Path(...)],
|
|
342
|
+
),
|
|
343
|
+
inspect.Parameter(
|
|
344
|
+
"request",
|
|
345
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
346
|
+
annotation=Request,
|
|
347
|
+
),
|
|
348
|
+
inspect.Parameter(
|
|
349
|
+
"db",
|
|
350
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
351
|
+
annotation=Annotated[Any, Depends(db_dep)],
|
|
352
|
+
),
|
|
353
|
+
inspect.Parameter(
|
|
354
|
+
"body",
|
|
355
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
356
|
+
annotation=body_annotation,
|
|
357
|
+
default=body_default,
|
|
358
|
+
),
|
|
359
|
+
]
|
|
360
|
+
)
|
|
361
|
+
_endpoint.__signature__ = inspect.Signature(params)
|
|
362
|
+
|
|
363
|
+
_endpoint.__name__ = f"rest_{model.__name__}_{alias}_member"
|
|
364
|
+
_endpoint.__qualname__ = _endpoint.__name__
|
|
365
|
+
_endpoint.__doc__ = f"REST member endpoint for {model.__name__}.{alias} ({target})"
|
|
366
|
+
_endpoint.__annotations__["body"] = body_annotation
|
|
367
|
+
return _endpoint
|