tigrbl 0.0.1.dev1__py3-none-any.whl → 0.3.0__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 +97 -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 +291 -0
- tigrbl/app/__init__.py +0 -0
- tigrbl/app/_app.py +86 -0
- tigrbl/app/_model_registry.py +41 -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 +319 -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 +286 -0
- tigrbl/bindings/rest/common.py +120 -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/io_headers.py +49 -0
- tigrbl/bindings/rest/member.py +386 -0
- tigrbl/bindings/rest/router.py +296 -0
- tigrbl/bindings/rest/routing.py +153 -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 +136 -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 +45 -0
- tigrbl/engine/_engine.py +144 -0
- tigrbl/engine/bind.py +33 -0
- tigrbl/engine/builders.py +236 -0
- tigrbl/engine/capabilities.py +29 -0
- tigrbl/engine/collect.py +111 -0
- tigrbl/engine/decorators.py +110 -0
- tigrbl/engine/docs/PLUGINS.md +49 -0
- tigrbl/engine/engine_spec.py +355 -0
- tigrbl/engine/plugins.py +52 -0
- tigrbl/engine/registry.py +36 -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 +118 -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 +171 -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 +19 -0
- tigrbl/runtime/atoms/response/headers_from_payload.py +57 -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 +644 -0
- tigrbl/runtime/labels.py +353 -0
- tigrbl/runtime/opview.py +89 -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 +79 -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/session/README.md +14 -0
- tigrbl/session/__init__.py +28 -0
- tigrbl/session/abc.py +76 -0
- tigrbl/session/base.py +151 -0
- tigrbl/session/decorators.py +43 -0
- tigrbl/session/default.py +118 -0
- tigrbl/session/shortcuts.py +50 -0
- tigrbl/session/spec.py +112 -0
- tigrbl/shortcuts.py +22 -0
- tigrbl/specs.py +44 -0
- tigrbl/system/__init__.py +13 -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/system/uvicorn.py +60 -0
- tigrbl/table/__init__.py +9 -0
- tigrbl/table/_base.py +260 -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 +170 -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/types/uuid.py +55 -0
- tigrbl-0.3.0.dist-info/METADATA +516 -0
- tigrbl-0.3.0.dist-info/RECORD +266 -0
- {tigrbl-0.0.1.dev1.dist-info → tigrbl-0.3.0.dist-info}/WHEEL +1 -1
- tigrbl-0.3.0.dist-info/licenses/LICENSE +201 -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
|
@@ -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,49 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
from typing import Mapping, Any
|
|
5
|
+
|
|
6
|
+
from fastapi import Header
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _build_signature_with_header_params(
|
|
10
|
+
hdr_fields: list[tuple[str, str, bool]],
|
|
11
|
+
) -> inspect.Signature:
|
|
12
|
+
params: list[inspect.Parameter] = []
|
|
13
|
+
for _field, header, required in hdr_fields:
|
|
14
|
+
param = header.lower().replace("-", "_")
|
|
15
|
+
default = Header(... if required else None, alias=header)
|
|
16
|
+
params.append(
|
|
17
|
+
inspect.Parameter(
|
|
18
|
+
name=param,
|
|
19
|
+
kind=inspect.Parameter.KEYWORD_ONLY,
|
|
20
|
+
default=default,
|
|
21
|
+
annotation=str | None,
|
|
22
|
+
)
|
|
23
|
+
)
|
|
24
|
+
return inspect.Signature(parameters=params, return_annotation=Mapping[str, object])
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _make_header_dep(model: type, alias: str):
|
|
28
|
+
hdr_fields: list[tuple[str, str, bool]] = []
|
|
29
|
+
for name, spec in getattr(model, "__tigrbl_cols__", {}).items():
|
|
30
|
+
io = getattr(spec, "io", None)
|
|
31
|
+
if not io or not getattr(io, "header_in", None):
|
|
32
|
+
continue
|
|
33
|
+
if alias not in set(getattr(io, "in_verbs", ()) or ()): # honor IO.in_verbs
|
|
34
|
+
continue
|
|
35
|
+
hdr_fields.append(
|
|
36
|
+
(name, io.header_in, bool(getattr(io, "header_required_in", False)))
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
async def _dep(**kw: Any) -> Mapping[str, object]:
|
|
40
|
+
out: dict[str, object] = {}
|
|
41
|
+
for field, header, _req in hdr_fields:
|
|
42
|
+
param = header.lower().replace("-", "_")
|
|
43
|
+
v = kw.get(param)
|
|
44
|
+
if v is not None:
|
|
45
|
+
out[field] = v
|
|
46
|
+
return out
|
|
47
|
+
|
|
48
|
+
_dep.__signature__ = _build_signature_with_header_params(hdr_fields)
|
|
49
|
+
return _dep
|