tigrbl 0.0.1.dev1__py3-none-any.whl → 0.3.0.dev2__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.dev2.dist-info/LICENSE +201 -0
- tigrbl-0.3.0.dev2.dist-info/METADATA +501 -0
- tigrbl-0.3.0.dev2.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.dev2.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"""Core schema builder logic for Tigrbl v3."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Any, Dict, Iterable, Set, Tuple, Type, Union
|
|
7
|
+
|
|
8
|
+
from pydantic import AliasChoices, BaseModel, ConfigDict, Field, create_model
|
|
9
|
+
|
|
10
|
+
from ..utils import namely_model
|
|
11
|
+
from ...column.mro_collect import mro_collect_columns
|
|
12
|
+
from .cache import _SchemaCache
|
|
13
|
+
from .extras import _merge_request_extras, _merge_response_extras
|
|
14
|
+
from .helpers import _add_field, _is_required, _python_type
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _build_schema(
|
|
20
|
+
orm_cls: type,
|
|
21
|
+
*,
|
|
22
|
+
name: str | None = None,
|
|
23
|
+
include: Set[str] | None = None,
|
|
24
|
+
exclude: Set[str] | None = None,
|
|
25
|
+
verb: str = "create",
|
|
26
|
+
) -> Type[BaseModel]:
|
|
27
|
+
"""Build (and cache) a verb-specific Pydantic schema for *orm_cls*."""
|
|
28
|
+
cache_key = (
|
|
29
|
+
orm_cls,
|
|
30
|
+
verb,
|
|
31
|
+
frozenset(include or ()),
|
|
32
|
+
frozenset(exclude or ()),
|
|
33
|
+
name,
|
|
34
|
+
)
|
|
35
|
+
cached = _SchemaCache.get(cache_key)
|
|
36
|
+
if cached is not None:
|
|
37
|
+
logger.debug("schema: cache hit %s verb=%s", orm_cls.__name__, verb)
|
|
38
|
+
return cached
|
|
39
|
+
|
|
40
|
+
logger.debug(
|
|
41
|
+
"schema: building %s verb=%s include=%s exclude=%s",
|
|
42
|
+
orm_cls.__name__,
|
|
43
|
+
verb,
|
|
44
|
+
include,
|
|
45
|
+
exclude,
|
|
46
|
+
)
|
|
47
|
+
fields: Dict[str, Tuple[type, Field]] = {}
|
|
48
|
+
|
|
49
|
+
# ── PASS 1: table-backed columns only (avoid mapper relationships)
|
|
50
|
+
table = getattr(orm_cls, "__table__", None)
|
|
51
|
+
table_cols: Iterable[Any] = tuple(table.columns) if table is not None else ()
|
|
52
|
+
specs: Dict[str, Any] = mro_collect_columns(orm_cls)
|
|
53
|
+
|
|
54
|
+
for col in table_cols:
|
|
55
|
+
attr_name = col.key or col.name
|
|
56
|
+
|
|
57
|
+
if include and attr_name not in include:
|
|
58
|
+
continue
|
|
59
|
+
if exclude and attr_name in exclude:
|
|
60
|
+
continue
|
|
61
|
+
|
|
62
|
+
spec = specs.get(attr_name)
|
|
63
|
+
io = getattr(spec, "io", None) if spec is not None else None
|
|
64
|
+
if verb in {"create", "update", "replace"}:
|
|
65
|
+
"""Determine if the column participates in inbound verbs.
|
|
66
|
+
|
|
67
|
+
When a ColumnSpec is present it may explicitly restrict inbound
|
|
68
|
+
verbs via ``io.in_verbs``. Columns that only declare outbound verbs
|
|
69
|
+
are treated as read-only and omitted from request schemas. If no
|
|
70
|
+
ColumnSpec or ``in_verbs`` is provided we allow all verbs.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
if getattr(col, "primary_key", False) and verb in {
|
|
74
|
+
"update",
|
|
75
|
+
"replace",
|
|
76
|
+
"delete",
|
|
77
|
+
}:
|
|
78
|
+
# Always expose the PK for mutating operations even when the
|
|
79
|
+
# ColumnSpec omits inbound verbs. The identifier is required so
|
|
80
|
+
# consumers can target the correct row.
|
|
81
|
+
pass
|
|
82
|
+
else:
|
|
83
|
+
if io is not None:
|
|
84
|
+
in_verbs = set(getattr(io, "in_verbs", ()) or ())
|
|
85
|
+
out_verbs = set(getattr(io, "out_verbs", ()) or ())
|
|
86
|
+
if not in_verbs:
|
|
87
|
+
if out_verbs:
|
|
88
|
+
continue
|
|
89
|
+
elif verb not in in_verbs:
|
|
90
|
+
continue
|
|
91
|
+
|
|
92
|
+
logger.debug("schema: processing column %s (verb=%s)", attr_name, verb)
|
|
93
|
+
|
|
94
|
+
# Determine type and requiredness
|
|
95
|
+
py_t = _python_type(col)
|
|
96
|
+
required = _is_required(col, verb)
|
|
97
|
+
|
|
98
|
+
# Field construction (collect kwargs then create Field once)
|
|
99
|
+
fs = getattr(spec, "field", None)
|
|
100
|
+
field_kwargs: Dict[str, Any] = dict(getattr(fs, "constraints", {}) or {})
|
|
101
|
+
|
|
102
|
+
default_factory = getattr(spec, "default_factory", None)
|
|
103
|
+
if default_factory and verb in set(getattr(io, "in_verbs", []) or []):
|
|
104
|
+
field_kwargs["default_factory"] = default_factory
|
|
105
|
+
required = False
|
|
106
|
+
else:
|
|
107
|
+
field_kwargs["default"] = None if not required else ...
|
|
108
|
+
|
|
109
|
+
# IOSpec aliases → Pydantic validation/serialization aliases
|
|
110
|
+
alias_in = getattr(io, "alias_in", None) if io is not None else None
|
|
111
|
+
alias_out = getattr(io, "alias_out", None) if io is not None else None
|
|
112
|
+
if alias_in:
|
|
113
|
+
field_kwargs["validation_alias"] = AliasChoices(alias_in, attr_name)
|
|
114
|
+
if alias_out:
|
|
115
|
+
field_kwargs["serialization_alias"] = alias_out
|
|
116
|
+
|
|
117
|
+
fld = Field(**field_kwargs)
|
|
118
|
+
|
|
119
|
+
# Optional typing if nullable
|
|
120
|
+
is_nullable = bool(getattr(col, "nullable", True))
|
|
121
|
+
if is_nullable and py_t is not Any:
|
|
122
|
+
py_t = Union[py_t, None]
|
|
123
|
+
|
|
124
|
+
# Apply alias mappings for IO specs so that generated Pydantic models
|
|
125
|
+
# accept both the canonical field name and any configured alias. This
|
|
126
|
+
# ensures request payloads can use ``alias_in`` and response models use
|
|
127
|
+
# ``alias_out`` while still normalizing to the canonical attribute name
|
|
128
|
+
# internally.
|
|
129
|
+
if io is not None:
|
|
130
|
+
if verb in {"read", "list"}:
|
|
131
|
+
alias = getattr(io, "alias_out", None)
|
|
132
|
+
else:
|
|
133
|
+
alias = getattr(io, "alias_in", None)
|
|
134
|
+
if alias:
|
|
135
|
+
fld.alias = alias
|
|
136
|
+
fld.serialization_alias = alias
|
|
137
|
+
fld.validation_alias = AliasChoices(attr_name, alias)
|
|
138
|
+
|
|
139
|
+
_add_field(fields, name=attr_name, py_t=py_t, field=fld)
|
|
140
|
+
logger.debug(
|
|
141
|
+
"schema: added field %s required=%s type=%r", attr_name, required, py_t
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
# ── PASS 1b: virtual columns declared via ColumnSpec --------------------
|
|
145
|
+
for attr_name, spec in specs.items():
|
|
146
|
+
if getattr(spec, "storage", None) is not None:
|
|
147
|
+
continue # real columns handled above
|
|
148
|
+
if include and attr_name not in include:
|
|
149
|
+
continue
|
|
150
|
+
if exclude and attr_name in exclude:
|
|
151
|
+
continue
|
|
152
|
+
|
|
153
|
+
io = getattr(spec, "io", None)
|
|
154
|
+
allowed_verbs = set(getattr(io, "in_verbs", ()) or ()) | set(
|
|
155
|
+
getattr(io, "out_verbs", ()) or ()
|
|
156
|
+
)
|
|
157
|
+
if allowed_verbs and verb not in allowed_verbs:
|
|
158
|
+
continue
|
|
159
|
+
|
|
160
|
+
fs = getattr(spec, "field", None)
|
|
161
|
+
py_t = getattr(fs, "py_type", Any) if fs is not None else Any
|
|
162
|
+
required = bool(fs and verb in getattr(fs, "required_in", ()))
|
|
163
|
+
allow_null = bool(fs and verb in getattr(fs, "allow_null_in", ()))
|
|
164
|
+
nullable = bool(getattr(spec, "nullable", True))
|
|
165
|
+
field_kwargs: Dict[str, Any] = dict(getattr(fs, "constraints", {}) or {})
|
|
166
|
+
|
|
167
|
+
default_factory = getattr(spec, "default_factory", None)
|
|
168
|
+
if default_factory and verb in set(getattr(spec.io, "in_verbs", []) or []):
|
|
169
|
+
field_kwargs["default_factory"] = default_factory
|
|
170
|
+
required = False
|
|
171
|
+
else:
|
|
172
|
+
field_kwargs["default"] = None if not required else ...
|
|
173
|
+
|
|
174
|
+
fld = Field(**field_kwargs)
|
|
175
|
+
|
|
176
|
+
if (allow_null or nullable) and py_t is not Any:
|
|
177
|
+
py_t = Union[py_t, None]
|
|
178
|
+
|
|
179
|
+
_add_field(fields, name=attr_name, py_t=py_t, field=fld)
|
|
180
|
+
logger.debug(
|
|
181
|
+
"schema: added virtual field %s required=%s type=%r",
|
|
182
|
+
attr_name,
|
|
183
|
+
required,
|
|
184
|
+
py_t,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
# ── PASS 2: request/response extras
|
|
188
|
+
_merge_request_extras(orm_cls, verb, fields, include=include, exclude=exclude)
|
|
189
|
+
_merge_response_extras(orm_cls, verb, fields, include=include, exclude=exclude)
|
|
190
|
+
|
|
191
|
+
model_name = name or f"{orm_cls.__name__}{verb.capitalize()}"
|
|
192
|
+
cfg_kwargs = {"from_attributes": True}
|
|
193
|
+
if verb in {"create", "update", "replace"}:
|
|
194
|
+
cfg_kwargs["extra"] = "forbid"
|
|
195
|
+
cfg = ConfigDict(**cfg_kwargs)
|
|
196
|
+
|
|
197
|
+
schema_cls = create_model(model_name, __config__=cfg, **fields) # type: ignore[arg-type]
|
|
198
|
+
schema_cls.model_rebuild(force=True)
|
|
199
|
+
schema_cls = namely_model(
|
|
200
|
+
schema_cls,
|
|
201
|
+
name=model_name,
|
|
202
|
+
doc=f"Tigrbl v3 {orm_cls.__name__} {verb} schema",
|
|
203
|
+
)
|
|
204
|
+
_SchemaCache[cache_key] = schema_cls
|
|
205
|
+
logger.debug("schema: created %s with %d fields", model_name, len(fields))
|
|
206
|
+
return schema_cls
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
__all__ = ["_build_schema"]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Cache and type definitions for schema builder."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Dict, Tuple, Type, Union, Literal
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
|
|
9
|
+
_SchemaVerb = Union[
|
|
10
|
+
Literal["create"],
|
|
11
|
+
Literal["read"],
|
|
12
|
+
Literal["update"],
|
|
13
|
+
Literal["replace"],
|
|
14
|
+
Literal["merge"],
|
|
15
|
+
Literal["delete"],
|
|
16
|
+
Literal["list"],
|
|
17
|
+
Literal["clear"],
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
_SchemaCache: Dict[
|
|
21
|
+
Tuple[type, str, frozenset, frozenset, str | None], Type[BaseModel]
|
|
22
|
+
] = {}
|
|
23
|
+
|
|
24
|
+
__all__ = ["_SchemaVerb", "_SchemaCache"]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Compatibility utilities for schema builder."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
# Pydantic v2 sentinel for "no default"
|
|
7
|
+
from pydantic_core import PydanticUndefined # type: ignore
|
|
8
|
+
except Exception: # pragma: no cover
|
|
9
|
+
|
|
10
|
+
class PydanticUndefinedClass: # type: ignore
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
PydanticUndefined = PydanticUndefinedClass() # type: ignore
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
__all__ = ["PydanticUndefined"]
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Support for request/response extras in schema building."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Any, Dict, Set, Tuple
|
|
7
|
+
|
|
8
|
+
from pydantic import Field
|
|
9
|
+
|
|
10
|
+
from ...config.constants import (
|
|
11
|
+
TIGRBL_REQUEST_EXTRAS_ATTR,
|
|
12
|
+
TIGRBL_RESPONSE_EXTRAS_ATTR,
|
|
13
|
+
)
|
|
14
|
+
from .helpers import _add_field
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _merge_request_extras(
|
|
20
|
+
orm_cls: type,
|
|
21
|
+
verb: str,
|
|
22
|
+
fields: Dict[str, Tuple[type, Field]],
|
|
23
|
+
*,
|
|
24
|
+
include: Set[str] | None,
|
|
25
|
+
exclude: Set[str] | None,
|
|
26
|
+
) -> None:
|
|
27
|
+
"""Merge request-only virtual fields into the schema."""
|
|
28
|
+
buckets = getattr(orm_cls, TIGRBL_REQUEST_EXTRAS_ATTR, None)
|
|
29
|
+
if not buckets:
|
|
30
|
+
return
|
|
31
|
+
if verb not in {"create", "update", "replace", "delete"}:
|
|
32
|
+
return
|
|
33
|
+
|
|
34
|
+
for bucket in (buckets.get("*", {}), buckets.get(verb, {})):
|
|
35
|
+
for name, spec in (bucket or {}).items():
|
|
36
|
+
if include and name not in include:
|
|
37
|
+
continue
|
|
38
|
+
if exclude and name in exclude:
|
|
39
|
+
continue
|
|
40
|
+
if isinstance(spec, tuple) and len(spec) == 2:
|
|
41
|
+
py_t, fld = spec
|
|
42
|
+
else:
|
|
43
|
+
py_t, fld = (spec or Any), Field(None)
|
|
44
|
+
_add_field(fields, name=name, py_t=py_t, field=fld)
|
|
45
|
+
logger.debug(
|
|
46
|
+
"schema: added request-extra field %s (verb=%s, type=%r)",
|
|
47
|
+
name,
|
|
48
|
+
verb,
|
|
49
|
+
py_t,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _merge_response_extras(
|
|
54
|
+
orm_cls: type,
|
|
55
|
+
verb: str,
|
|
56
|
+
fields: Dict[str, Tuple[type, Field]],
|
|
57
|
+
*,
|
|
58
|
+
include: Set[str] | None,
|
|
59
|
+
exclude: Set[str] | None,
|
|
60
|
+
) -> None:
|
|
61
|
+
"""Merge response-only virtual fields into the schema."""
|
|
62
|
+
buckets = getattr(orm_cls, TIGRBL_RESPONSE_EXTRAS_ATTR, None)
|
|
63
|
+
if not buckets:
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
for bucket in (buckets.get("*", {}), buckets.get(verb, {})):
|
|
67
|
+
for name, spec in (bucket or {}).items():
|
|
68
|
+
if include and name not in include:
|
|
69
|
+
continue
|
|
70
|
+
if exclude and name in exclude:
|
|
71
|
+
continue
|
|
72
|
+
if isinstance(spec, tuple) and len(spec) == 2:
|
|
73
|
+
py_t, fld = spec
|
|
74
|
+
else:
|
|
75
|
+
py_t, fld = (spec or Any), Field(None)
|
|
76
|
+
_add_field(fields, name=name, py_t=py_t, field=fld)
|
|
77
|
+
logger.debug(
|
|
78
|
+
"schema: added response-extra field %s (verb=%s, type=%r)",
|
|
79
|
+
name,
|
|
80
|
+
verb,
|
|
81
|
+
py_t,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
__all__ = ["_merge_request_extras", "_merge_response_extras"]
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Internal helper utilities for schema building."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, Tuple
|
|
6
|
+
|
|
7
|
+
from pydantic import Field
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _bool(x: Any) -> bool:
|
|
11
|
+
try:
|
|
12
|
+
return bool(x)
|
|
13
|
+
except Exception: # pragma: no cover
|
|
14
|
+
return False
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _add_field(
|
|
18
|
+
sink: Dict[str, Tuple[type, Field]],
|
|
19
|
+
*,
|
|
20
|
+
name: str,
|
|
21
|
+
py_t: type | Any,
|
|
22
|
+
field: Field | None = None,
|
|
23
|
+
) -> None:
|
|
24
|
+
sink[name] = (py_t, field if field is not None else Field(None))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _python_type(col: Any) -> type | Any:
|
|
28
|
+
try:
|
|
29
|
+
return col.type.python_type
|
|
30
|
+
except Exception: # pragma: no cover
|
|
31
|
+
return Any
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _is_required(col: Any, verb: str) -> bool:
|
|
35
|
+
"""Decide if a column should be required for the given verb."""
|
|
36
|
+
if getattr(col, "primary_key", False):
|
|
37
|
+
if verb in {"update", "replace", "delete"}:
|
|
38
|
+
return True
|
|
39
|
+
auto = getattr(col, "autoincrement", False)
|
|
40
|
+
if auto not in (False, None) or getattr(col, "identity", None) is not None:
|
|
41
|
+
return False
|
|
42
|
+
if verb == "update":
|
|
43
|
+
return False
|
|
44
|
+
is_nullable = bool(getattr(col, "nullable", True))
|
|
45
|
+
has_default = (getattr(col, "default", None) is not None) or (
|
|
46
|
+
getattr(col, "server_default", None) is not None
|
|
47
|
+
)
|
|
48
|
+
return not is_nullable and not has_default
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
__all__ = ["_bool", "_add_field", "_python_type", "_is_required"]
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Schema builders for list parameter models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import uuid
|
|
7
|
+
from typing import Any, Type
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, ConfigDict, Field, create_model
|
|
10
|
+
|
|
11
|
+
from ..utils import namely_model
|
|
12
|
+
from ...column.mro_collect import mro_collect_columns
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _build_list_params(model: type) -> Type[BaseModel]:
|
|
18
|
+
"""Create a list/filter schema for the given model."""
|
|
19
|
+
tab = model.__name__
|
|
20
|
+
logger.debug("schema: build_list_params for %s", tab)
|
|
21
|
+
|
|
22
|
+
base = dict(
|
|
23
|
+
skip=(int | None, Field(None, ge=0)),
|
|
24
|
+
limit=(int | None, Field(None, ge=10)),
|
|
25
|
+
sort=(str | list[str] | None, Field(None)),
|
|
26
|
+
)
|
|
27
|
+
_scalars = {str, int, float, bool, bytes, uuid.UUID}
|
|
28
|
+
cols: dict[str, tuple[type, Field]] = {}
|
|
29
|
+
|
|
30
|
+
table = getattr(model, "__table__", None)
|
|
31
|
+
if table is None or not getattr(table, "columns", None):
|
|
32
|
+
# No table info; return a minimal pager schema
|
|
33
|
+
schema = create_model(
|
|
34
|
+
f"{tab}ListParams", __config__=ConfigDict(extra="forbid"), **base
|
|
35
|
+
) # type: ignore[arg-type]
|
|
36
|
+
schema = namely_model(
|
|
37
|
+
schema,
|
|
38
|
+
name=f"{tab}ListParams",
|
|
39
|
+
doc=f"List parameters for {tab}",
|
|
40
|
+
)
|
|
41
|
+
logger.debug(
|
|
42
|
+
"schema: build_list_params generated %s (no columns)", schema.__name__
|
|
43
|
+
)
|
|
44
|
+
return schema
|
|
45
|
+
|
|
46
|
+
pk_name = None
|
|
47
|
+
for c in table.columns:
|
|
48
|
+
if getattr(c, "primary_key", False):
|
|
49
|
+
pk_name = c.name
|
|
50
|
+
break
|
|
51
|
+
|
|
52
|
+
_canon = {
|
|
53
|
+
"eq": "eq",
|
|
54
|
+
"=": "eq",
|
|
55
|
+
"==": "eq",
|
|
56
|
+
"ne": "ne",
|
|
57
|
+
"!=": "ne",
|
|
58
|
+
"<>": "ne",
|
|
59
|
+
"lt": "lt",
|
|
60
|
+
"<": "lt",
|
|
61
|
+
"gt": "gt",
|
|
62
|
+
">": "gt",
|
|
63
|
+
"lte": "lte",
|
|
64
|
+
"le": "lte",
|
|
65
|
+
"<=": "lte",
|
|
66
|
+
"gte": "gte",
|
|
67
|
+
"ge": "gte",
|
|
68
|
+
">=": "gte",
|
|
69
|
+
"like": "like",
|
|
70
|
+
"not_like": "not_like",
|
|
71
|
+
"ilike": "ilike",
|
|
72
|
+
"not_ilike": "not_ilike",
|
|
73
|
+
"in": "in",
|
|
74
|
+
"not_in": "not_in",
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
for c in table.columns:
|
|
78
|
+
if pk_name and c.name == pk_name:
|
|
79
|
+
continue
|
|
80
|
+
py_t = getattr(c.type, "python_type", Any)
|
|
81
|
+
if py_t in _scalars:
|
|
82
|
+
spec_map = mro_collect_columns(model)
|
|
83
|
+
spec = spec_map.get(c.name)
|
|
84
|
+
io = getattr(spec, "io", None)
|
|
85
|
+
ops_raw = set(getattr(io, "filter_ops", ()) or [])
|
|
86
|
+
if not ops_raw:
|
|
87
|
+
# Allow basic equality filtering by default on scalar columns
|
|
88
|
+
ops_raw = {"eq"}
|
|
89
|
+
ops = {_canon.get(op, op) for op in ops_raw}
|
|
90
|
+
if "eq" in ops:
|
|
91
|
+
cols[c.name] = (py_t | None, Field(None))
|
|
92
|
+
logger.debug("schema: list filter add %s type=%r", c.name, py_t)
|
|
93
|
+
for op in ops:
|
|
94
|
+
if op == "eq":
|
|
95
|
+
continue
|
|
96
|
+
fname = f"{c.name}__{op}"
|
|
97
|
+
cols[fname] = (py_t | None, Field(None))
|
|
98
|
+
logger.debug(
|
|
99
|
+
"schema: list filter add %s op=%s type=%r", c.name, op, py_t
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
schema = create_model(
|
|
103
|
+
f"{tab}ListParams",
|
|
104
|
+
__config__=ConfigDict(extra="forbid"),
|
|
105
|
+
**base, # type: ignore[arg-type]
|
|
106
|
+
**cols, # type: ignore[arg-type]
|
|
107
|
+
)
|
|
108
|
+
schema = namely_model(
|
|
109
|
+
schema,
|
|
110
|
+
name=f"{tab}ListParams",
|
|
111
|
+
doc=f"List parameters for {tab}",
|
|
112
|
+
)
|
|
113
|
+
logger.debug("schema: build_list_params generated %s", schema.__name__)
|
|
114
|
+
return schema
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
__all__ = ["_build_list_params"]
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Utilities for removing fields from parent schemas."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
6
|
+
from typing import (
|
|
7
|
+
Any,
|
|
8
|
+
Dict,
|
|
9
|
+
List,
|
|
10
|
+
Set,
|
|
11
|
+
Tuple,
|
|
12
|
+
Type,
|
|
13
|
+
Union,
|
|
14
|
+
get_args,
|
|
15
|
+
get_origin,
|
|
16
|
+
get_type_hints,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
from pydantic import BaseModel, create_model
|
|
20
|
+
|
|
21
|
+
from .compat import PydanticUndefined
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _strip_parent_fields(base: Type[BaseModel], *, drop: Set[str]) -> Type[BaseModel]:
|
|
25
|
+
"""Return a shallow clone of *base* with selected fields removed."""
|
|
26
|
+
assert issubclass(base, BaseModel), "base must be a Pydantic BaseModel subclass"
|
|
27
|
+
|
|
28
|
+
# RootModel[Union[Model, List[Model]]] – unwrap inner model so we can strip
|
|
29
|
+
# identifiers and return the cleaned schema directly.
|
|
30
|
+
if len(getattr(base, "model_fields", {})) == 1 and "root" in base.model_fields:
|
|
31
|
+
root_ann = base.model_fields["root"].annotation
|
|
32
|
+
if get_origin(root_ann) is Union:
|
|
33
|
+
item_type = None
|
|
34
|
+
for arg in get_args(root_ann):
|
|
35
|
+
origin = get_origin(arg)
|
|
36
|
+
if inspect.isclass(arg) and issubclass(arg, BaseModel):
|
|
37
|
+
item_type = arg
|
|
38
|
+
break
|
|
39
|
+
if origin in (list, List):
|
|
40
|
+
sub = get_args(arg)[0]
|
|
41
|
+
if inspect.isclass(sub) and issubclass(sub, BaseModel):
|
|
42
|
+
item_type = sub
|
|
43
|
+
break
|
|
44
|
+
if item_type is not None:
|
|
45
|
+
return _strip_parent_fields(item_type, drop=drop)
|
|
46
|
+
|
|
47
|
+
hints = get_type_hints(base, include_extras=True)
|
|
48
|
+
new_fields: Dict[str, Tuple[type, Any]] = {}
|
|
49
|
+
|
|
50
|
+
for name, finfo in base.model_fields.items(): # type: ignore[attr-defined]
|
|
51
|
+
if name in (drop or ()): # pragma: no branch
|
|
52
|
+
continue
|
|
53
|
+
typ = hints.get(name, Any)
|
|
54
|
+
default = (
|
|
55
|
+
finfo.default
|
|
56
|
+
if getattr(finfo, "default", PydanticUndefined) is not PydanticUndefined
|
|
57
|
+
else ...
|
|
58
|
+
)
|
|
59
|
+
new_fields[name] = (typ, default)
|
|
60
|
+
|
|
61
|
+
clone = create_model(
|
|
62
|
+
f"{base.__name__}Pruned",
|
|
63
|
+
__config__=getattr(base, "model_config", None),
|
|
64
|
+
**new_fields,
|
|
65
|
+
) # type: ignore[arg-type]
|
|
66
|
+
clone.model_rebuild(force=True)
|
|
67
|
+
return clone
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
__all__ = ["_strip_parent_fields"]
|
tigrbl/schema/collect.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# tigrbl/v3/schema/collect.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import inspect
|
|
5
|
+
import logging
|
|
6
|
+
from functools import lru_cache
|
|
7
|
+
from typing import Dict
|
|
8
|
+
|
|
9
|
+
from ..config.constants import TIGRBL_SCHEMA_DECLS_ATTR
|
|
10
|
+
|
|
11
|
+
from .decorators import _SchemaDecl
|
|
12
|
+
|
|
13
|
+
logging.getLogger("uvicorn").setLevel(logging.DEBUG)
|
|
14
|
+
logger = logging.getLogger("uvicorn")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@lru_cache(maxsize=None)
|
|
18
|
+
def collect_decorated_schemas(model: type) -> Dict[str, Dict[str, type]]:
|
|
19
|
+
"""Gather schema declarations for ``model`` across its MRO."""
|
|
20
|
+
logger.info("Collecting decorated schemas for %s", model.__name__)
|
|
21
|
+
out: Dict[str, Dict[str, type]] = {}
|
|
22
|
+
|
|
23
|
+
# Explicit registrations (MRO-merged)
|
|
24
|
+
for base in reversed(model.__mro__):
|
|
25
|
+
mapping: Dict[str, Dict[str, type]] = (
|
|
26
|
+
getattr(base, TIGRBL_SCHEMA_DECLS_ATTR, {}) or {}
|
|
27
|
+
)
|
|
28
|
+
if mapping:
|
|
29
|
+
logger.debug(
|
|
30
|
+
"Found explicit schema mapping on %s: %s", base.__name__, mapping
|
|
31
|
+
)
|
|
32
|
+
for alias, kinds in mapping.items():
|
|
33
|
+
bucket = out.setdefault(alias, {})
|
|
34
|
+
bucket.update(kinds or {})
|
|
35
|
+
|
|
36
|
+
# Nested classes with __tigrbl_schema_decl__
|
|
37
|
+
for base in reversed(model.__mro__):
|
|
38
|
+
for name, obj in base.__dict__.items():
|
|
39
|
+
if not inspect.isclass(obj):
|
|
40
|
+
logger.debug("Skipping non-class attribute %s.%s", base.__name__, name)
|
|
41
|
+
continue
|
|
42
|
+
decl: _SchemaDecl | None = getattr(obj, "__tigrbl_schema_decl__", None)
|
|
43
|
+
if not decl:
|
|
44
|
+
logger.debug(
|
|
45
|
+
"Class %s.%s has no schema declaration", base.__name__, name
|
|
46
|
+
)
|
|
47
|
+
continue
|
|
48
|
+
bucket = out.setdefault(decl.alias, {})
|
|
49
|
+
bucket[decl.kind] = obj
|
|
50
|
+
|
|
51
|
+
logger.debug("Collected schema aliases: %s", list(out.keys()))
|
|
52
|
+
return out
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
__all__ = ["collect_decorated_schemas"]
|