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,193 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
# tigrbl/v3/bindings/schemas/utils.py
|
|
5
|
+
|
|
6
|
+
from types import SimpleNamespace
|
|
7
|
+
from typing import Any, Optional, Tuple, Type
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, create_model
|
|
10
|
+
|
|
11
|
+
from ...schema.types import SchemaArg, SchemaRef
|
|
12
|
+
from ...schema import namely_model
|
|
13
|
+
|
|
14
|
+
logging.getLogger("uvicorn").setLevel(logging.DEBUG)
|
|
15
|
+
logger = logging.getLogger("uvicorn")
|
|
16
|
+
logger.debug("Loaded module v3/bindings/schemas/utils")
|
|
17
|
+
|
|
18
|
+
_Key = Tuple[str, str] # (alias, target)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _camel(s: str) -> str:
|
|
22
|
+
return "".join(p.capitalize() or "_" for p in s.split("_"))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _alias_schema(
|
|
26
|
+
schema: Type[BaseModel], *, model: type, alias: str, kind: str
|
|
27
|
+
) -> Type[BaseModel]:
|
|
28
|
+
logger.debug(
|
|
29
|
+
"Aliasing schema %s for %s as %s %s",
|
|
30
|
+
schema.__name__,
|
|
31
|
+
model.__name__,
|
|
32
|
+
alias,
|
|
33
|
+
kind,
|
|
34
|
+
)
|
|
35
|
+
name = f"{model.__name__}{_camel(alias)}{kind}"
|
|
36
|
+
if getattr(schema, "__name__", None) == name:
|
|
37
|
+
logger.debug("Schema already aliased as %s", name)
|
|
38
|
+
return schema
|
|
39
|
+
try:
|
|
40
|
+
clone = create_model(name, __base__=schema) # type: ignore[arg-type]
|
|
41
|
+
except Exception as e: # pragma: no cover - best effort
|
|
42
|
+
logger.debug("Failed to clone schema %s: %s", schema.__name__, e)
|
|
43
|
+
return schema
|
|
44
|
+
logger.debug("Created alias schema %s", name)
|
|
45
|
+
return namely_model(
|
|
46
|
+
clone,
|
|
47
|
+
name=name,
|
|
48
|
+
doc=f"{alias} {kind.lower()} schema for {model.__name__}",
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _ensure_alias_namespace(model: type, alias: str) -> SimpleNamespace:
|
|
53
|
+
ns = getattr(model.schemas, alias, None)
|
|
54
|
+
if ns is None:
|
|
55
|
+
ns = SimpleNamespace()
|
|
56
|
+
setattr(model.schemas, alias, ns)
|
|
57
|
+
logger.debug("Created namespace for %s.%s", model.__name__, alias)
|
|
58
|
+
else:
|
|
59
|
+
logger.debug("Using existing namespace for %s.%s", model.__name__, alias)
|
|
60
|
+
return ns
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _pk_info(model: type) -> Tuple[str, type | Any]:
|
|
64
|
+
"""
|
|
65
|
+
Return (pk_name, python_type) for single-PK tables. If composite, returns (pk, Any).
|
|
66
|
+
"""
|
|
67
|
+
table = getattr(model, "__table__", None)
|
|
68
|
+
if table is None or not getattr(table, "primary_key", None):
|
|
69
|
+
logger.debug("Model %s has no table or primary key", model.__name__)
|
|
70
|
+
return ("id", Any)
|
|
71
|
+
cols = list(table.primary_key.columns) # type: ignore[attr-defined]
|
|
72
|
+
if not cols:
|
|
73
|
+
logger.debug("Model %s primary key columns empty", model.__name__)
|
|
74
|
+
return ("id", Any)
|
|
75
|
+
if len(cols) > 1:
|
|
76
|
+
logger.debug("Model %s has composite primary key", model.__name__)
|
|
77
|
+
# Composite keys: schema builder uses verb='delete' to require what's needed.
|
|
78
|
+
# For bulk_delete we fall back to Any.
|
|
79
|
+
return ("id", Any)
|
|
80
|
+
col = cols[0]
|
|
81
|
+
py_t = getattr(getattr(col, "type", None), "python_type", Any)
|
|
82
|
+
logger.debug(
|
|
83
|
+
"Model %s primary key %s of type %s",
|
|
84
|
+
model.__name__,
|
|
85
|
+
getattr(col, "name", "id"),
|
|
86
|
+
py_t,
|
|
87
|
+
)
|
|
88
|
+
return (getattr(col, "name", "id"), py_t or Any)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _parse_str_ref(s: str) -> Tuple[str, str]:
|
|
92
|
+
"""
|
|
93
|
+
Parse dotted schema ref "alias.in" | "alias.out".
|
|
94
|
+
"""
|
|
95
|
+
s = s.strip()
|
|
96
|
+
logger.debug("Parsing schema ref '%s'", s)
|
|
97
|
+
if "." not in s:
|
|
98
|
+
logger.debug("Schema ref '%s' missing '.'", s)
|
|
99
|
+
raise ValueError(
|
|
100
|
+
f"Invalid schema path '{s}', expected 'alias.in' or 'alias.out'"
|
|
101
|
+
)
|
|
102
|
+
alias, kind = s.split(".", 1)
|
|
103
|
+
kind = kind.strip()
|
|
104
|
+
if kind not in {"in", "out"}:
|
|
105
|
+
logger.debug("Schema ref '%s' has invalid kind '%s'", s, kind)
|
|
106
|
+
raise ValueError(f"Invalid schema kind '{kind}', expected 'in' or 'out'")
|
|
107
|
+
logger.debug("Parsed schema ref alias=%s kind=%s", alias, kind)
|
|
108
|
+
return alias.strip(), kind
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _resolve_schema_arg(model: type, arg: SchemaArg) -> Optional[Type[BaseModel]]:
|
|
112
|
+
"""
|
|
113
|
+
Resolve an override to a concrete Pydantic model or raw:
|
|
114
|
+
• SchemaRef("alias","in"|"out") → that model
|
|
115
|
+
• "alias.in" | "alias.out" → that model
|
|
116
|
+
• "raw" → None (raw passthrough)
|
|
117
|
+
• None → caller should keep defaults for canonical ops;
|
|
118
|
+
for custom ops no defaults exist → raw
|
|
119
|
+
Unsupported (will raise):
|
|
120
|
+
• direct Pydantic model classes
|
|
121
|
+
• callables/thunks
|
|
122
|
+
• any other strings
|
|
123
|
+
"""
|
|
124
|
+
logger.debug("Resolving schema arg %s for %s", arg, model.__name__)
|
|
125
|
+
if arg is None:
|
|
126
|
+
logger.debug("Schema arg is None; returning None")
|
|
127
|
+
return None
|
|
128
|
+
|
|
129
|
+
# explicit raw
|
|
130
|
+
if isinstance(arg, str) and arg.strip().lower() == "raw":
|
|
131
|
+
logger.debug("Schema arg is explicit raw")
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
# direct Pydantic model
|
|
135
|
+
if isinstance(arg, type) and issubclass(arg, BaseModel):
|
|
136
|
+
logger.debug("Schema arg is direct model %s", arg.__name__)
|
|
137
|
+
return arg
|
|
138
|
+
|
|
139
|
+
# SchemaRef
|
|
140
|
+
if isinstance(arg, SchemaRef):
|
|
141
|
+
logger.debug("Schema arg is SchemaRef(alias=%s, kind=%s)", arg.alias, arg.kind)
|
|
142
|
+
if arg.kind not in ("in", "out"):
|
|
143
|
+
logger.debug("Unsupported SchemaRef kind '%s'", arg.kind)
|
|
144
|
+
raise ValueError(
|
|
145
|
+
f"Unsupported SchemaRef kind '{arg.kind}'. Use 'in' or 'out'.",
|
|
146
|
+
)
|
|
147
|
+
ns = getattr(model, "schemas", None)
|
|
148
|
+
if ns is None or getattr(ns, arg.alias, None) is None:
|
|
149
|
+
logger.debug("Unknown schema alias '%s' on %s", arg.alias, model.__name__)
|
|
150
|
+
raise KeyError(f"Unknown schema alias '{arg.alias}' on {model.__name__}")
|
|
151
|
+
alias_ns = getattr(ns, arg.alias)
|
|
152
|
+
attr = "in_" if arg.kind == "in" else "out"
|
|
153
|
+
res = getattr(alias_ns, attr, None)
|
|
154
|
+
if res is None:
|
|
155
|
+
logger.debug(
|
|
156
|
+
"Schema '%s.%s' not found on %s", arg.alias, attr, model.__name__
|
|
157
|
+
)
|
|
158
|
+
raise KeyError(f"Schema '{arg.alias}.{attr}' not found on {model.__name__}")
|
|
159
|
+
logger.debug(
|
|
160
|
+
"Resolved SchemaRef %s.%s to %s",
|
|
161
|
+
arg.alias,
|
|
162
|
+
attr,
|
|
163
|
+
getattr(res, "__name__", None),
|
|
164
|
+
)
|
|
165
|
+
return res # type: ignore[return-value]
|
|
166
|
+
|
|
167
|
+
# dotted string
|
|
168
|
+
if isinstance(arg, str):
|
|
169
|
+
alias, kind = _parse_str_ref(arg)
|
|
170
|
+
ns = getattr(model, "schemas", None)
|
|
171
|
+
if ns is None or getattr(ns, alias, None) is None:
|
|
172
|
+
logger.debug("Unknown schema alias '%s' on %s", alias, model.__name__)
|
|
173
|
+
raise KeyError(f"Unknown schema alias '{alias}' on {model.__name__}")
|
|
174
|
+
alias_ns = getattr(ns, alias)
|
|
175
|
+
attr = "in_" if kind == "in" else "out"
|
|
176
|
+
res = getattr(alias_ns, attr, None)
|
|
177
|
+
if res is None:
|
|
178
|
+
logger.debug("Schema '%s.%s' not found on %s", alias, attr, model.__name__)
|
|
179
|
+
raise KeyError(f"Schema '{alias}.{attr}' not found on {model.__name__}")
|
|
180
|
+
logger.debug(
|
|
181
|
+
"Resolved schema path %s.%s to %s",
|
|
182
|
+
alias,
|
|
183
|
+
attr,
|
|
184
|
+
getattr(res, "__name__", None),
|
|
185
|
+
)
|
|
186
|
+
return res # type: ignore[return-value]
|
|
187
|
+
|
|
188
|
+
logger.debug("Unsupported SchemaArg type: %s", type(arg))
|
|
189
|
+
# Everything else is unsupported now
|
|
190
|
+
raise TypeError(
|
|
191
|
+
f"Unsupported SchemaArg type: {type(arg)}. "
|
|
192
|
+
"Use SchemaRef(...,'in'|'out'), 'alias.in'/'alias.out', 'raw', or None.",
|
|
193
|
+
)
|
tigrbl/column/README.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Column Specs
|
|
2
|
+
|
|
3
|
+
Tigrbl columns are declared with helper functions like `acol` and `vcol`.
|
|
4
|
+
Each column is driven by three smaller specs that describe how the value is
|
|
5
|
+
stored, validated, and exposed. These specs keep column behaviour
|
|
6
|
+
consistent across the ORM, schema generation, and the runtime API.
|
|
7
|
+
|
|
8
|
+
## StorageSpec (`S`)
|
|
9
|
+
Describes the database shape and storage behaviour.
|
|
10
|
+
|
|
11
|
+
- `type_` – SQLAlchemy column type.
|
|
12
|
+
- `nullable`, `unique`, `index`, `primary_key`, `autoincrement`.
|
|
13
|
+
- Defaults and generation: `default`, `onupdate`, `server_default`,
|
|
14
|
+
`refresh_on_return`.
|
|
15
|
+
- Optional helpers: `transform`, `fk`, `check`, `comment`.
|
|
16
|
+
|
|
17
|
+
## FieldSpec (`F`)
|
|
18
|
+
Captures Pydantic metadata and request policy.
|
|
19
|
+
|
|
20
|
+
- `py_type` – Python type, inferred when omitted.
|
|
21
|
+
- `constraints` – passed to `pydantic.Field` for validation.
|
|
22
|
+
- `required_in` / `allow_null_in` – control required and nullable verbs.
|
|
23
|
+
|
|
24
|
+
## IOSpec (`IO`)
|
|
25
|
+
Controls API exposure and advanced value handling.
|
|
26
|
+
|
|
27
|
+
- `in_verbs` / `out_verbs` – verbs that accept or emit the value.
|
|
28
|
+
- `mutable_verbs` – verbs allowed to change the value.
|
|
29
|
+
- `alias_in` / `alias_out` – alternative field names.
|
|
30
|
+
- `sensitive`, `redact_last` – masking and redaction options.
|
|
31
|
+
- `filter_ops`, `sortable` – enable filtering and sorting.
|
|
32
|
+
- Helpers: `assemble`, `paired`, `alias_readtime` for computed aliases.
|
|
33
|
+
|
|
34
|
+
## ColumnSpec
|
|
35
|
+
`ColumnSpec` ties the three specs together and adds runtime helpers such as
|
|
36
|
+
`default_factory` and `read_producer`.
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from tigrbl.column import acol, vcol, F, IO, S
|
|
40
|
+
|
|
41
|
+
class Widget(Base):
|
|
42
|
+
__tablename__ = "widgets"
|
|
43
|
+
|
|
44
|
+
id: Mapped[int] = acol(storage=S(primary_key=True))
|
|
45
|
+
name: Mapped[str] = acol(
|
|
46
|
+
field=F(constraints={"max_length": 50}),
|
|
47
|
+
storage=S(nullable=False, index=True),
|
|
48
|
+
io=IO(
|
|
49
|
+
in_verbs=("create", "update"),
|
|
50
|
+
out_verbs=("read", "list"),
|
|
51
|
+
sortable=True,
|
|
52
|
+
),
|
|
53
|
+
)
|
|
54
|
+
checksum: Mapped[str] = vcol(
|
|
55
|
+
field=F(),
|
|
56
|
+
io=IO(out_verbs=("read",)),
|
|
57
|
+
read_producer=lambda obj, ctx: f"{obj.name}:{obj.id}",
|
|
58
|
+
)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
For additional background see the "Column-Level Configuration" section in the
|
|
62
|
+
parent [README](../README.md).
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# tigrbl/v3/column/__init__.py
|
|
2
|
+
"""Tigrbl v3 – column specs public API.
|
|
3
|
+
|
|
4
|
+
Unifies StorageSpec (DB-facing), FieldSpec (python/wire semantics), and IOSpec (in/out exposure)
|
|
5
|
+
into a :class:`ColumnSpec` with a :class:`Column` descriptor. Provides ergonomic constructors
|
|
6
|
+
``makeColumn`` and ``makeVirtualColumn`` (with ``acol``/``vcol`` convenience aliases) and re-exports the
|
|
7
|
+
bind-time type inference utilities and markers.
|
|
8
|
+
|
|
9
|
+
Public surface:
|
|
10
|
+
Column, ColumnSpec, FieldSpec as F, StorageSpec as S, IOSpec as IO
|
|
11
|
+
makeColumn, makeVirtualColumn, acol, vcol
|
|
12
|
+
infer, Inferred, DataKind, SATypePlan, JsonHint
|
|
13
|
+
markers: Email, Phone
|
|
14
|
+
exceptions: InferenceError, UnsupportedType
|
|
15
|
+
helper: is_virtual(ColumnSpec) -> bool
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
# Core spec types
|
|
21
|
+
from .column_spec import ColumnSpec
|
|
22
|
+
from ._column import Column
|
|
23
|
+
from .field_spec import FieldSpec as F
|
|
24
|
+
from .storage_spec import StorageSpec as S
|
|
25
|
+
from .io_spec import IOSpec as IO
|
|
26
|
+
|
|
27
|
+
# Ergonomic constructors
|
|
28
|
+
from .shortcuts import makeColumn, makeVirtualColumn, acol, vcol
|
|
29
|
+
|
|
30
|
+
# Bind-time inference (DB/vendor-agnostic)
|
|
31
|
+
from .infer import (
|
|
32
|
+
infer,
|
|
33
|
+
Inferred,
|
|
34
|
+
DataKind,
|
|
35
|
+
SATypePlan,
|
|
36
|
+
JsonHint,
|
|
37
|
+
Email,
|
|
38
|
+
Phone,
|
|
39
|
+
InferenceError,
|
|
40
|
+
UnsupportedType,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
__all__ = [
|
|
44
|
+
"ColumnSpec",
|
|
45
|
+
"Column",
|
|
46
|
+
"F",
|
|
47
|
+
"S",
|
|
48
|
+
"IO",
|
|
49
|
+
"makeColumn",
|
|
50
|
+
"makeVirtualColumn",
|
|
51
|
+
"acol",
|
|
52
|
+
"vcol",
|
|
53
|
+
"infer",
|
|
54
|
+
"Inferred",
|
|
55
|
+
"DataKind",
|
|
56
|
+
"SATypePlan",
|
|
57
|
+
"JsonHint",
|
|
58
|
+
"Email",
|
|
59
|
+
"Phone",
|
|
60
|
+
"InferenceError",
|
|
61
|
+
"UnsupportedType",
|
|
62
|
+
"is_virtual",
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def is_virtual(col: ColumnSpec) -> bool:
|
|
67
|
+
"""Return True if the column is wire-only (never persisted)."""
|
|
68
|
+
return getattr(col, "storage", None) is None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def __dir__():
|
|
72
|
+
return sorted(__all__)
|
tigrbl/column/_column.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Callable, Optional
|
|
4
|
+
|
|
5
|
+
from sqlalchemy import ForeignKey
|
|
6
|
+
from sqlalchemy.orm import MappedColumn
|
|
7
|
+
|
|
8
|
+
from .column_spec import ColumnSpec
|
|
9
|
+
from .field_spec import FieldSpec as F
|
|
10
|
+
from .io_spec import IOSpec as IO
|
|
11
|
+
from .storage_spec import StorageSpec as S
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Column(ColumnSpec, MappedColumn):
|
|
15
|
+
"""SQLAlchemy column implementing a :class:`ColumnSpec`."""
|
|
16
|
+
|
|
17
|
+
__slots__ = ()
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
*,
|
|
22
|
+
spec: ColumnSpec | None = None,
|
|
23
|
+
storage: S | None = None,
|
|
24
|
+
field: F | None = None,
|
|
25
|
+
io: IO | None = None,
|
|
26
|
+
default_factory: Optional[Callable[[dict], Any]] = None,
|
|
27
|
+
read_producer: Optional[Callable[[object, dict], Any]] = None,
|
|
28
|
+
**kw: Any,
|
|
29
|
+
) -> None:
|
|
30
|
+
if spec is not None and any(
|
|
31
|
+
x is not None for x in (storage, field, io, default_factory, read_producer)
|
|
32
|
+
):
|
|
33
|
+
raise ValueError("Provide either spec or individual components, not both.")
|
|
34
|
+
if spec is None:
|
|
35
|
+
spec = ColumnSpec(
|
|
36
|
+
storage=storage,
|
|
37
|
+
field=field,
|
|
38
|
+
io=io,
|
|
39
|
+
default_factory=default_factory,
|
|
40
|
+
read_producer=read_producer,
|
|
41
|
+
)
|
|
42
|
+
else:
|
|
43
|
+
storage = spec.storage
|
|
44
|
+
field = spec.field
|
|
45
|
+
io = spec.io
|
|
46
|
+
default_factory = spec.default_factory
|
|
47
|
+
read_producer = spec.read_producer
|
|
48
|
+
|
|
49
|
+
s = storage
|
|
50
|
+
if s is not None:
|
|
51
|
+
args: list[Any] = [s.type_]
|
|
52
|
+
fk = getattr(s, "fk", None)
|
|
53
|
+
if fk is not None:
|
|
54
|
+
args.append(
|
|
55
|
+
ForeignKey(
|
|
56
|
+
fk.target,
|
|
57
|
+
ondelete=fk.on_delete,
|
|
58
|
+
onupdate=fk.on_update,
|
|
59
|
+
deferrable=fk.deferrable,
|
|
60
|
+
initially="DEFERRED" if fk.initially_deferred else "IMMEDIATE",
|
|
61
|
+
match=fk.match,
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
MappedColumn.__init__(
|
|
65
|
+
self,
|
|
66
|
+
*args,
|
|
67
|
+
primary_key=s.primary_key,
|
|
68
|
+
nullable=s.nullable,
|
|
69
|
+
unique=s.unique,
|
|
70
|
+
index=s.index,
|
|
71
|
+
default=s.default,
|
|
72
|
+
autoincrement=s.autoincrement,
|
|
73
|
+
server_default=s.server_default,
|
|
74
|
+
onupdate=s.onupdate,
|
|
75
|
+
comment=s.comment,
|
|
76
|
+
**kw,
|
|
77
|
+
)
|
|
78
|
+
else:
|
|
79
|
+
MappedColumn.__init__(self, **kw)
|
|
80
|
+
|
|
81
|
+
self.storage = s
|
|
82
|
+
self.field = field if field is not None else F()
|
|
83
|
+
self.io = io if io is not None else IO()
|
|
84
|
+
self.default_factory = default_factory
|
|
85
|
+
self.read_producer = read_producer
|
|
86
|
+
|
|
87
|
+
def __set_name__(self, owner, name: str) -> None:
|
|
88
|
+
parent = getattr(super(), "__set_name__", None)
|
|
89
|
+
if parent:
|
|
90
|
+
parent(owner, name)
|
|
91
|
+
colspecs = owner.__dict__.get("__tigrbl_colspecs__")
|
|
92
|
+
if colspecs is None:
|
|
93
|
+
base_specs = getattr(owner, "__tigrbl_colspecs__", {})
|
|
94
|
+
colspecs = dict(base_specs)
|
|
95
|
+
setattr(owner, "__tigrbl_colspecs__", colspecs)
|
|
96
|
+
colspecs[name] = self
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Callable, Optional
|
|
4
|
+
|
|
5
|
+
from .field_spec import FieldSpec as F
|
|
6
|
+
from .io_spec import IOSpec as IO
|
|
7
|
+
from .storage_spec import StorageSpec as S
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ColumnSpec:
|
|
11
|
+
"""Aggregate configuration for a model attribute.
|
|
12
|
+
|
|
13
|
+
A :class:`ColumnSpec` brings together the three lower-level specs used by
|
|
14
|
+
Tigrbl's declarative column system:
|
|
15
|
+
|
|
16
|
+
- ``storage`` (:class:`~tigrbl.column.storage_spec.StorageSpec`) controls
|
|
17
|
+
how the value is persisted in the database.
|
|
18
|
+
- ``field`` (:class:`~tigrbl.column.field_spec.FieldSpec`) describes the
|
|
19
|
+
Python type and any schema metadata.
|
|
20
|
+
- ``io`` (:class:`~tigrbl.column.io_spec.IOSpec`) governs inbound and
|
|
21
|
+
outbound API exposure.
|
|
22
|
+
|
|
23
|
+
Optional ``default_factory`` and ``read_producer`` callables allow for
|
|
24
|
+
programmatic defaults and virtual read-time values respectively.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
*,
|
|
30
|
+
storage: S | None,
|
|
31
|
+
field: F | None = None,
|
|
32
|
+
io: IO | None = None,
|
|
33
|
+
default_factory: Optional[Callable[[dict], Any]] = None,
|
|
34
|
+
read_producer: Optional[Callable[[object, dict], Any]] = None,
|
|
35
|
+
) -> None:
|
|
36
|
+
self.storage = storage
|
|
37
|
+
self.field = field if field is not None else F()
|
|
38
|
+
self.io = io if io is not None else IO()
|
|
39
|
+
self.default_factory = default_factory
|
|
40
|
+
self.read_producer = read_producer
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# field_spec.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
from dataclasses import dataclass, field as dc_field
|
|
4
|
+
from typing import Any, Dict, Tuple, Callable
|
|
5
|
+
from pydantic import ValidationInfo # v2
|
|
6
|
+
|
|
7
|
+
PreFn = Callable[[Any, ValidationInfo], Any] # BeforeValidator
|
|
8
|
+
PostFn = Callable[[Any, ValidationInfo], Any] # AfterValidator
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class FieldSpec:
|
|
13
|
+
"""Describe Python-side metadata for a column or virtual field.
|
|
14
|
+
|
|
15
|
+
``py_type`` denotes the expected Python type and may be omitted when the
|
|
16
|
+
model attribute is annotated; the type will then be inferred. ``constraints``
|
|
17
|
+
mirrors arguments accepted by :func:`pydantic.Field` and participates in
|
|
18
|
+
schema generation. ``required_in`` and ``allow_null_in`` govern which API
|
|
19
|
+
verbs must supply the value or may explicitly send ``null`` in requests.
|
|
20
|
+
Responses rely on Pydantic's built-in encoders based solely on the
|
|
21
|
+
declared type.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
py_type: Any = Any
|
|
25
|
+
|
|
26
|
+
# For request/response schema generation (+ pydantic.Field)
|
|
27
|
+
constraints: Dict[str, Any] = dc_field(default_factory=dict)
|
|
28
|
+
|
|
29
|
+
# Request policy (DB nullability lives in StorageSpec.nullable)
|
|
30
|
+
required_in: Tuple[str, ...] = ()
|
|
31
|
+
allow_null_in: Tuple[str, ...] = ()
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from .core import infer
|
|
2
|
+
from .types import (
|
|
3
|
+
Email,
|
|
4
|
+
Phone,
|
|
5
|
+
DataKind,
|
|
6
|
+
PyTypeInfo,
|
|
7
|
+
SATypePlan,
|
|
8
|
+
JsonHint,
|
|
9
|
+
Inferred,
|
|
10
|
+
InferenceError,
|
|
11
|
+
UnsupportedType,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"infer",
|
|
16
|
+
"Email",
|
|
17
|
+
"Phone",
|
|
18
|
+
"DataKind",
|
|
19
|
+
"PyTypeInfo",
|
|
20
|
+
"SATypePlan",
|
|
21
|
+
"JsonHint",
|
|
22
|
+
"Inferred",
|
|
23
|
+
"InferenceError",
|
|
24
|
+
"UnsupportedType",
|
|
25
|
+
]
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, Optional, get_origin
|
|
4
|
+
|
|
5
|
+
from .types import DataKind, PyTypeInfo, Inferred
|
|
6
|
+
from .utils import _strip_optional, _strip_annotated, _array_item, _is_enum
|
|
7
|
+
from .planning import _plan_sa_type
|
|
8
|
+
from .jsonhints import _json_hint
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def infer(
|
|
12
|
+
annotation: Any,
|
|
13
|
+
*,
|
|
14
|
+
prefer_dialect: Optional[str] = "postgresql",
|
|
15
|
+
max_length: Optional[int] = None,
|
|
16
|
+
decimal_precision: Optional[int] = None,
|
|
17
|
+
decimal_scale: Optional[int] = None,
|
|
18
|
+
) -> Inferred:
|
|
19
|
+
"""Bind-time inference from Python annotation to DataKind and hints."""
|
|
20
|
+
base, is_opt = _strip_optional(annotation)
|
|
21
|
+
base, meta = _strip_annotated(base)
|
|
22
|
+
|
|
23
|
+
enum_cls = _is_enum(base)
|
|
24
|
+
item_tp = _array_item(base)
|
|
25
|
+
|
|
26
|
+
array_item_info: Optional[PyTypeInfo] = None
|
|
27
|
+
if item_tp is not None:
|
|
28
|
+
nested = infer(
|
|
29
|
+
item_tp,
|
|
30
|
+
prefer_dialect=prefer_dialect,
|
|
31
|
+
max_length=max_length,
|
|
32
|
+
decimal_precision=decimal_precision,
|
|
33
|
+
decimal_scale=decimal_scale,
|
|
34
|
+
)
|
|
35
|
+
array_item_info = nested.py
|
|
36
|
+
|
|
37
|
+
py_info = PyTypeInfo(
|
|
38
|
+
base=base,
|
|
39
|
+
is_optional=is_opt,
|
|
40
|
+
enum_cls=enum_cls,
|
|
41
|
+
array_item=array_item_info,
|
|
42
|
+
annotated=meta,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
import datetime as _dt
|
|
46
|
+
import decimal as _dc
|
|
47
|
+
import uuid as _uuid
|
|
48
|
+
|
|
49
|
+
origin = get_origin(base)
|
|
50
|
+
|
|
51
|
+
if enum_cls is not None:
|
|
52
|
+
kind = DataKind.ENUM
|
|
53
|
+
elif item_tp is not None:
|
|
54
|
+
kind = DataKind.ARRAY
|
|
55
|
+
elif base in (str,):
|
|
56
|
+
kind = DataKind.STRING
|
|
57
|
+
elif base in (bytes, bytearray, memoryview):
|
|
58
|
+
kind = DataKind.BYTES
|
|
59
|
+
elif base in (bool,):
|
|
60
|
+
kind = DataKind.BOOL
|
|
61
|
+
elif base in (int,):
|
|
62
|
+
kind = DataKind.INT
|
|
63
|
+
elif base in (float,):
|
|
64
|
+
kind = DataKind.FLOAT
|
|
65
|
+
elif base in (_dc.Decimal,):
|
|
66
|
+
kind = DataKind.DECIMAL
|
|
67
|
+
elif base in (_dt.datetime,):
|
|
68
|
+
kind = DataKind.DATETIME
|
|
69
|
+
elif base in (_dt.date,):
|
|
70
|
+
kind = DataKind.DATE
|
|
71
|
+
elif base in (_dt.time,):
|
|
72
|
+
kind = DataKind.TIME
|
|
73
|
+
elif base in (_uuid.UUID,):
|
|
74
|
+
kind = DataKind.UUID
|
|
75
|
+
else:
|
|
76
|
+
if origin in (dict, Dict):
|
|
77
|
+
kind = DataKind.JSON
|
|
78
|
+
else:
|
|
79
|
+
kind = DataKind.JSON
|
|
80
|
+
|
|
81
|
+
sa = _plan_sa_type(
|
|
82
|
+
kind,
|
|
83
|
+
py_info,
|
|
84
|
+
prefer_dialect=prefer_dialect,
|
|
85
|
+
max_length=max_length,
|
|
86
|
+
decimal_precision=decimal_precision,
|
|
87
|
+
decimal_scale=decimal_scale,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
jh = _json_hint(kind, py_info, max_length=max_length)
|
|
91
|
+
|
|
92
|
+
return Inferred(kind=kind, py=py_info, sa=sa, json=jh, nullable=is_opt)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from .types import DataKind, PyTypeInfo, JsonHint, Email, Phone
|
|
6
|
+
from .planning import _nested_kind_from_py
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _json_hint(
|
|
10
|
+
kind: DataKind, py: PyTypeInfo, *, max_length: Optional[int]
|
|
11
|
+
) -> JsonHint:
|
|
12
|
+
if kind is DataKind.STRING:
|
|
13
|
+
fmt = None
|
|
14
|
+
if Email in py.annotated:
|
|
15
|
+
fmt = "email"
|
|
16
|
+
if Phone in py.annotated:
|
|
17
|
+
fmt = "phone"
|
|
18
|
+
return JsonHint(type="string", format=fmt, maxLength=max_length)
|
|
19
|
+
if kind is DataKind.BYTES:
|
|
20
|
+
return JsonHint(type="string", format="byte")
|
|
21
|
+
if kind is DataKind.BOOL:
|
|
22
|
+
return JsonHint(type="boolean")
|
|
23
|
+
if kind is DataKind.INT or kind is DataKind.BIGINT:
|
|
24
|
+
return JsonHint(type="integer")
|
|
25
|
+
if kind is DataKind.FLOAT or kind is DataKind.DECIMAL:
|
|
26
|
+
return JsonHint(type="number")
|
|
27
|
+
if kind is DataKind.DATE:
|
|
28
|
+
return JsonHint(type="string", format="date")
|
|
29
|
+
if kind is DataKind.TIME:
|
|
30
|
+
return JsonHint(type="string", format="time")
|
|
31
|
+
if kind is DataKind.DATETIME:
|
|
32
|
+
return JsonHint(type="string", format="date-time")
|
|
33
|
+
if kind is DataKind.UUID:
|
|
34
|
+
return JsonHint(type="string", format="uuid")
|
|
35
|
+
if kind is DataKind.JSON:
|
|
36
|
+
return JsonHint(type="object")
|
|
37
|
+
if kind is DataKind.ENUM and py.enum_cls:
|
|
38
|
+
return JsonHint(type="string", enum=[e.name for e in py.enum_cls])
|
|
39
|
+
if kind is DataKind.ARRAY and py.array_item:
|
|
40
|
+
elem_kind = _nested_kind_from_py(py.array_item)
|
|
41
|
+
return JsonHint(
|
|
42
|
+
type="array", items=_json_hint(elem_kind, py.array_item, max_length=None)
|
|
43
|
+
)
|
|
44
|
+
return JsonHint(type="string")
|