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,227 @@
|
|
|
1
|
+
# tigrbl/v3/runtime/atoms/wire/validate_in.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import datetime as _dt
|
|
5
|
+
import decimal as _dc
|
|
6
|
+
import uuid as _uuid
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Any, Dict, Mapping, Optional, Tuple
|
|
9
|
+
|
|
10
|
+
from fastapi import HTTPException, status as _status
|
|
11
|
+
|
|
12
|
+
from ... import events as _ev
|
|
13
|
+
from ...opview import opview_from_ctx, ensure_schema_in, _ensure_temp
|
|
14
|
+
|
|
15
|
+
# PRE_HANDLER, runs after wire:build_in
|
|
16
|
+
ANCHOR = _ev.IN_VALIDATE # "in:validate"
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger("uvicorn")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def run(obj: Optional[object], ctx: Any) -> None:
|
|
22
|
+
"""
|
|
23
|
+
wire:validate_in@in:validate
|
|
24
|
+
|
|
25
|
+
Validates the normalized inbound payload (ctx.temp["in_values"]) using the
|
|
26
|
+
schema collected by schema:collect_in plus tolerant signals from ColumnSpec.
|
|
27
|
+
|
|
28
|
+
What it checks
|
|
29
|
+
--------------
|
|
30
|
+
- Required fields are present (ABSENT → error; present(None) is a separate check)
|
|
31
|
+
- Nullability (value is None while column is non-nullable → error)
|
|
32
|
+
- Lightweight type conformance (optional coercion if enabled)
|
|
33
|
+
- String max_length (when available)
|
|
34
|
+
- Author validators (validate_in / in_validator / validator_in on ColumnSpec/FieldSpec/IOSpec)
|
|
35
|
+
|
|
36
|
+
Effects
|
|
37
|
+
-------
|
|
38
|
+
- Mutates ctx.temp["in_values"] with any successful coercions/validator returns
|
|
39
|
+
- Writes diagnostics to:
|
|
40
|
+
ctx.temp["in_errors"] : list of {field, code, message}
|
|
41
|
+
ctx.temp["in_invalid"] : bool
|
|
42
|
+
ctx.temp["in_coerced"] : tuple of coerced field names (if any)
|
|
43
|
+
- Raises HTTPException(422) if any validation errors are found
|
|
44
|
+
"""
|
|
45
|
+
logger.debug("Running wire:validate_in")
|
|
46
|
+
temp = _ensure_temp(ctx)
|
|
47
|
+
ov = opview_from_ctx(ctx)
|
|
48
|
+
schema_in = ensure_schema_in(ctx, ov)
|
|
49
|
+
|
|
50
|
+
in_values: Dict[str, Any] = dict(temp.get("in_values") or {})
|
|
51
|
+
by_field: Mapping[str, Mapping[str, Any]] = schema_in.get("by_field", {}) # type: ignore[assignment]
|
|
52
|
+
required: Tuple[str, ...] = tuple(schema_in.get("required", ())) # type: ignore[assignment]
|
|
53
|
+
|
|
54
|
+
errors: list[Dict[str, Any]] = []
|
|
55
|
+
coerced: list[str] = []
|
|
56
|
+
|
|
57
|
+
# 1) Required presence (ABSENT → error)
|
|
58
|
+
for name in required:
|
|
59
|
+
if name not in in_values:
|
|
60
|
+
logger.debug("Required field %s missing", name)
|
|
61
|
+
errors.append(
|
|
62
|
+
_err(name, "required", "Field is required but was not provided.")
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# 2) Per-field validation
|
|
66
|
+
for name, value in list(in_values.items()):
|
|
67
|
+
entry = by_field.get(name) or {}
|
|
68
|
+
|
|
69
|
+
# Nullability
|
|
70
|
+
nullable = entry.get("nullable", None)
|
|
71
|
+
if value is None and nullable is False:
|
|
72
|
+
logger.debug("Field %s is null but not nullable", name)
|
|
73
|
+
errors.append(
|
|
74
|
+
_err(name, "null_not_allowed", "Null is not allowed for this field.")
|
|
75
|
+
)
|
|
76
|
+
continue # skip further checks for this field
|
|
77
|
+
|
|
78
|
+
# Type (optionally coerce)
|
|
79
|
+
target_type = entry.get("py_type")
|
|
80
|
+
if value is not None and isinstance(target_type, type):
|
|
81
|
+
allow_coerce = bool(entry.get("coerce", True))
|
|
82
|
+
ok, new_val, msg = _coerce_if_needed(value, target_type, allow=allow_coerce)
|
|
83
|
+
if not ok:
|
|
84
|
+
logger.debug("Type mismatch for field %s", name)
|
|
85
|
+
errors.append(
|
|
86
|
+
_err(
|
|
87
|
+
name,
|
|
88
|
+
"type_mismatch",
|
|
89
|
+
msg or f"Expected {_type_name(target_type)}.",
|
|
90
|
+
)
|
|
91
|
+
)
|
|
92
|
+
continue
|
|
93
|
+
if new_val is not value:
|
|
94
|
+
in_values[name] = new_val
|
|
95
|
+
coerced.append(name)
|
|
96
|
+
|
|
97
|
+
# Max length (strings)
|
|
98
|
+
max_len = entry.get("max_length", None)
|
|
99
|
+
if (
|
|
100
|
+
isinstance(max_len, int)
|
|
101
|
+
and max_len > 0
|
|
102
|
+
and isinstance(in_values.get(name), str)
|
|
103
|
+
):
|
|
104
|
+
if len(in_values[name]) > max_len:
|
|
105
|
+
logger.debug("Field %s exceeds max_length %d", name, max_len)
|
|
106
|
+
errors.append(
|
|
107
|
+
_err(name, "max_length", f"String exceeds max_length={max_len}.")
|
|
108
|
+
)
|
|
109
|
+
continue
|
|
110
|
+
|
|
111
|
+
# Author-supplied validator(s)
|
|
112
|
+
vfn = entry.get("validator")
|
|
113
|
+
if callable(vfn) and in_values.get(name) is not None:
|
|
114
|
+
try:
|
|
115
|
+
out = vfn(in_values[name], ctx)
|
|
116
|
+
if out is not None:
|
|
117
|
+
in_values[name] = out
|
|
118
|
+
except Exception as e:
|
|
119
|
+
logger.debug("Validator failed for field %s: %s", name, e)
|
|
120
|
+
errors.append(
|
|
121
|
+
_err(name, "validator_failed", f"{type(e).__name__}: {e}")
|
|
122
|
+
)
|
|
123
|
+
continue
|
|
124
|
+
|
|
125
|
+
# 3) Unknown keys policy (handled after build_in captured samples)
|
|
126
|
+
unknown = tuple(temp.get("in_unknown") or ())
|
|
127
|
+
if unknown and _reject_unknown(ctx):
|
|
128
|
+
logger.debug("Rejecting unknown fields: %s", unknown)
|
|
129
|
+
for k in unknown:
|
|
130
|
+
errors.append(_err(k, "unknown_field", "Unknown input key."))
|
|
131
|
+
|
|
132
|
+
# Persist results + diagnostics
|
|
133
|
+
temp["in_values"] = in_values
|
|
134
|
+
if coerced:
|
|
135
|
+
temp["in_coerced"] = tuple(coerced)
|
|
136
|
+
|
|
137
|
+
if errors:
|
|
138
|
+
temp["in_errors"] = errors
|
|
139
|
+
temp["in_invalid"] = True
|
|
140
|
+
logger.debug("Validation errors found: %s", errors)
|
|
141
|
+
raise HTTPException(
|
|
142
|
+
status_code=_status.HTTP_422_UNPROCESSABLE_ENTITY, detail=errors
|
|
143
|
+
)
|
|
144
|
+
else:
|
|
145
|
+
temp["in_invalid"] = False
|
|
146
|
+
logger.debug("Inbound payload validated successfully")
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
150
|
+
# Internals
|
|
151
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _reject_unknown(ctx: Any) -> bool:
|
|
155
|
+
"""
|
|
156
|
+
Check cfg for a 'reject_unknown_fields' (bool); default False.
|
|
157
|
+
"""
|
|
158
|
+
cfg = getattr(ctx, "cfg", None)
|
|
159
|
+
val = getattr(cfg, "reject_unknown_fields", None)
|
|
160
|
+
return bool(val) if isinstance(val, bool) else False
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _err(field: str, code: str, msg: str) -> Dict[str, Any]:
|
|
164
|
+
return {"field": field, "code": code, "message": msg}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _type_name(t: type) -> str:
|
|
168
|
+
return getattr(t, "__name__", str(t))
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
# ── coercion helpers ──────────────────────────────────────────────────────────
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _coerce_if_needed(
|
|
175
|
+
value: Any, target: type, *, allow: bool
|
|
176
|
+
) -> Tuple[bool, Any, Optional[str]]:
|
|
177
|
+
"""Return (ok, new_value, error_message). Only coerces when allow=True."""
|
|
178
|
+
if isinstance(value, target):
|
|
179
|
+
return True, value, None
|
|
180
|
+
if not allow:
|
|
181
|
+
return False, value, f"Expected {_type_name(target)}."
|
|
182
|
+
try:
|
|
183
|
+
coerced = _coerce(value, target)
|
|
184
|
+
return True, coerced, None
|
|
185
|
+
except Exception:
|
|
186
|
+
return False, value, f"Could not convert to {_type_name(target)}."
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _coerce(value: Any, target: type) -> Any:
|
|
190
|
+
if target is str:
|
|
191
|
+
return str(value)
|
|
192
|
+
if target is int:
|
|
193
|
+
if isinstance(value, bool):
|
|
194
|
+
return int(value)
|
|
195
|
+
return int(str(value).strip())
|
|
196
|
+
if target is float:
|
|
197
|
+
return float(str(value).strip())
|
|
198
|
+
if target is bool:
|
|
199
|
+
if isinstance(value, bool):
|
|
200
|
+
return value
|
|
201
|
+
s = str(value).strip().lower()
|
|
202
|
+
if s in {"true", "1", "yes", "y", "on"}:
|
|
203
|
+
return True
|
|
204
|
+
if s in {"false", "0", "no", "n", "off"}:
|
|
205
|
+
return False
|
|
206
|
+
raise ValueError("not a boolean")
|
|
207
|
+
if target is _dc.Decimal:
|
|
208
|
+
return _dc.Decimal(str(value).strip())
|
|
209
|
+
if target is _uuid.UUID:
|
|
210
|
+
return _uuid.UUID(str(value))
|
|
211
|
+
if target is _dt.datetime:
|
|
212
|
+
# allow both date-time and date-only (promote to midnight)
|
|
213
|
+
s = str(value).strip()
|
|
214
|
+
try:
|
|
215
|
+
return _dt.datetime.fromisoformat(s)
|
|
216
|
+
except Exception:
|
|
217
|
+
d = _dt.date.fromisoformat(s)
|
|
218
|
+
return _dt.datetime.combine(d, _dt.time())
|
|
219
|
+
if target is _dt.date:
|
|
220
|
+
return _dt.date.fromisoformat(str(value).strip())
|
|
221
|
+
if target is _dt.time:
|
|
222
|
+
return _dt.time.fromisoformat(str(value).strip())
|
|
223
|
+
# Fallback: try direct construction
|
|
224
|
+
return target(value)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
__all__ = ["ANCHOR", "run"]
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# tigrbl/v3/runtime/context.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import datetime as _dt
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any, Dict, Mapping, Optional, Sequence
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _canon_op(op: Optional[str]) -> str:
|
|
10
|
+
return (op or "").strip().lower() or "unknown"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class Context:
|
|
15
|
+
"""
|
|
16
|
+
Canonical runtime context shared by the kernel and atoms.
|
|
17
|
+
|
|
18
|
+
Minimal contract (consumed by atoms we’ve written so far):
|
|
19
|
+
- op: operation name (e.g., 'create' | 'update' | 'read' | 'list' | custom)
|
|
20
|
+
- persist: write vs. read (affects pruning of persist-tied anchors)
|
|
21
|
+
- specs: mapping of field -> ColumnSpec (frozen at bind time)
|
|
22
|
+
- cfg: read-only config view (see config.resolver.CfgView)
|
|
23
|
+
- temp: dict scratchpad used by atoms to exchange data
|
|
24
|
+
|
|
25
|
+
Optional adapter slots:
|
|
26
|
+
- model: owning model type / class
|
|
27
|
+
- obj: hydrated ORM instance (if any)
|
|
28
|
+
- session: DB session / unit-of-work handle
|
|
29
|
+
- user, tenant, now: identity/time hints
|
|
30
|
+
- row/values/current_values: mapping fallbacks (for read paths)
|
|
31
|
+
- in_data / payload / data / body: inbound payload staging (for build_in)
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
# core
|
|
35
|
+
op: str
|
|
36
|
+
persist: bool
|
|
37
|
+
specs: Mapping[str, Any]
|
|
38
|
+
cfg: Any
|
|
39
|
+
|
|
40
|
+
# shared scratchpad
|
|
41
|
+
temp: Dict[str, Any] = field(default_factory=dict)
|
|
42
|
+
|
|
43
|
+
# optional context
|
|
44
|
+
model: Any | None = None
|
|
45
|
+
obj: Any | None = None
|
|
46
|
+
session: Any | None = None
|
|
47
|
+
|
|
48
|
+
# identity/time
|
|
49
|
+
user: Any | None = None
|
|
50
|
+
tenant: Any | None = None
|
|
51
|
+
now: _dt.datetime | None = None
|
|
52
|
+
|
|
53
|
+
# read-path fallbacks
|
|
54
|
+
row: Mapping[str, Any] | None = None
|
|
55
|
+
values: Mapping[str, Any] | None = None
|
|
56
|
+
current_values: Mapping[str, Any] | None = None
|
|
57
|
+
|
|
58
|
+
# inbound staging (router/adapters may set any one of these)
|
|
59
|
+
in_data: Any | None = None
|
|
60
|
+
payload: Any | None = None
|
|
61
|
+
data: Any | None = None
|
|
62
|
+
body: Any | None = None
|
|
63
|
+
|
|
64
|
+
def __post_init__(self) -> None:
|
|
65
|
+
self.op = _canon_op(self.op)
|
|
66
|
+
# Normalize now to a timezone-aware UTC timestamp when not provided
|
|
67
|
+
if self.now is None:
|
|
68
|
+
try:
|
|
69
|
+
self.now = _dt.datetime.now(_dt.timezone.utc)
|
|
70
|
+
except Exception: # pragma: no cover
|
|
71
|
+
self.now = _dt.datetime.utcnow().replace(tzinfo=None)
|
|
72
|
+
|
|
73
|
+
# Ensure temp is a dict (atoms rely on it)
|
|
74
|
+
if not isinstance(self.temp, dict):
|
|
75
|
+
self.temp = dict(self.temp)
|
|
76
|
+
|
|
77
|
+
# ── convenience flags ─────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def is_write(self) -> bool:
|
|
81
|
+
"""Alias for persist; reads better in some call sites."""
|
|
82
|
+
return bool(self.persist)
|
|
83
|
+
|
|
84
|
+
# ── safe read-only view for user callables (generators, default_factory) ──
|
|
85
|
+
|
|
86
|
+
def safe_view(
|
|
87
|
+
self,
|
|
88
|
+
*,
|
|
89
|
+
include_temp: bool = False,
|
|
90
|
+
temp_keys: Optional[Sequence[str]] = None,
|
|
91
|
+
) -> Mapping[str, Any]:
|
|
92
|
+
"""
|
|
93
|
+
Return a small, read-only mapping exposing only safe, frequently useful keys.
|
|
94
|
+
|
|
95
|
+
By default, temp is NOT included (to avoid leaking internals like paired raw values).
|
|
96
|
+
If include_temp=True, only exposes the keys listed in 'temp_keys' (if provided),
|
|
97
|
+
otherwise exposes a conservative subset.
|
|
98
|
+
|
|
99
|
+
This method is intended to be passed into author callables such as
|
|
100
|
+
default_factory(ctx_view) or paired token generators.
|
|
101
|
+
"""
|
|
102
|
+
base = {
|
|
103
|
+
"op": self.op,
|
|
104
|
+
"persist": self.persist,
|
|
105
|
+
"model": self.model,
|
|
106
|
+
"specs": self.specs,
|
|
107
|
+
"user": self.user,
|
|
108
|
+
"tenant": self.tenant,
|
|
109
|
+
"now": self.now,
|
|
110
|
+
}
|
|
111
|
+
if include_temp:
|
|
112
|
+
allowed = set(temp_keys or ("assembled_values", "virtual_in"))
|
|
113
|
+
exposed: Dict[str, Any] = {}
|
|
114
|
+
for k in allowed:
|
|
115
|
+
if k in self.temp:
|
|
116
|
+
exposed[k] = self.temp[k]
|
|
117
|
+
base = {**base, "temp": MappingProxy(exposed)}
|
|
118
|
+
return MappingProxy(base)
|
|
119
|
+
|
|
120
|
+
# ── tiny helpers used by atoms / kernel ───────────────────────────────────
|
|
121
|
+
|
|
122
|
+
def mark_used_returning(self, value: bool = True) -> None:
|
|
123
|
+
"""Flag that DB RETURNING already hydrated values."""
|
|
124
|
+
self.temp["used_returning"] = bool(value)
|
|
125
|
+
|
|
126
|
+
def merge_hydrated_values(
|
|
127
|
+
self, mapping: Mapping[str, Any], *, replace: bool = False
|
|
128
|
+
) -> None:
|
|
129
|
+
"""
|
|
130
|
+
Save values hydrated from DB (RETURNING/refresh). If replace=False (default),
|
|
131
|
+
performs a shallow merge into any existing 'hydrated_values'.
|
|
132
|
+
"""
|
|
133
|
+
if not isinstance(mapping, Mapping):
|
|
134
|
+
return
|
|
135
|
+
hv = self.temp.get("hydrated_values")
|
|
136
|
+
if replace or not isinstance(hv, dict):
|
|
137
|
+
self.temp["hydrated_values"] = dict(mapping)
|
|
138
|
+
else:
|
|
139
|
+
hv.update(mapping)
|
|
140
|
+
|
|
141
|
+
def add_response_extras(
|
|
142
|
+
self, extras: Mapping[str, Any], *, overwrite: Optional[bool] = None
|
|
143
|
+
) -> Sequence[str]:
|
|
144
|
+
"""
|
|
145
|
+
Merge alias extras into temp['response_extras'].
|
|
146
|
+
Returns a tuple of conflicting keys that were skipped when overwrite=False.
|
|
147
|
+
"""
|
|
148
|
+
if not isinstance(extras, Mapping) or not extras:
|
|
149
|
+
return ()
|
|
150
|
+
buf = self.temp.get("response_extras")
|
|
151
|
+
if not isinstance(buf, dict):
|
|
152
|
+
buf = {}
|
|
153
|
+
self.temp["response_extras"] = buf
|
|
154
|
+
if overwrite is None:
|
|
155
|
+
# fall back to cfg; atoms call wire:dump to honor final overwrite policy
|
|
156
|
+
overwrite = bool(getattr(self.cfg, "response_extras_overwrite", False))
|
|
157
|
+
conflicts: list[str] = []
|
|
158
|
+
for k, v in extras.items():
|
|
159
|
+
if (k in buf) and not overwrite:
|
|
160
|
+
conflicts.append(k)
|
|
161
|
+
continue
|
|
162
|
+
buf[k] = v
|
|
163
|
+
return tuple(conflicts)
|
|
164
|
+
|
|
165
|
+
def get_response_payload(self) -> Any:
|
|
166
|
+
"""Return the payload assembled by wire:dump (or None if not yet available)."""
|
|
167
|
+
return self.temp.get("response_payload")
|
|
168
|
+
|
|
169
|
+
# ── representation (avoid leaking large/sensitive temp contents) ──────────
|
|
170
|
+
|
|
171
|
+
def __repr__(self) -> str: # pragma: no cover
|
|
172
|
+
model_name = getattr(self.model, "__name__", None) or str(self.model)
|
|
173
|
+
return (
|
|
174
|
+
f"Context(op={self.op!r}, persist={self.persist}, model={model_name!r}, "
|
|
175
|
+
f"user={(getattr(self.user, 'id', None) or None)!r}, temp_keys={sorted(self.temp.keys())})"
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# ── tiny immutable mapping proxy (local; no external deps) ────────────────────
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class MappingProxy(Mapping[str, Any]):
|
|
183
|
+
"""A lightweight, read-only mapping wrapper."""
|
|
184
|
+
|
|
185
|
+
__slots__ = ("_d",)
|
|
186
|
+
|
|
187
|
+
def __init__(self, data: Mapping[str, Any]):
|
|
188
|
+
self._d = dict(data)
|
|
189
|
+
|
|
190
|
+
def __getitem__(self, k: str) -> Any:
|
|
191
|
+
return self._d[k]
|
|
192
|
+
|
|
193
|
+
def __iter__(self):
|
|
194
|
+
return iter(self._d)
|
|
195
|
+
|
|
196
|
+
def __len__(self) -> int:
|
|
197
|
+
return len(self._d)
|
|
198
|
+
|
|
199
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
200
|
+
return self._d.get(key, default)
|
|
201
|
+
|
|
202
|
+
def __repr__(self) -> str: # pragma: no cover
|
|
203
|
+
return f"MappingProxy({self._d!r})"
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
__all__ = ["Context", "MappingProxy"]
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from .utils import HTTPException, status
|
|
4
|
+
from .mappings import (
|
|
5
|
+
HTTP_ERROR_MESSAGES,
|
|
6
|
+
ERROR_MESSAGES,
|
|
7
|
+
_HTTP_TO_RPC,
|
|
8
|
+
_RPC_TO_HTTP,
|
|
9
|
+
)
|
|
10
|
+
from .converters import (
|
|
11
|
+
http_exc_to_rpc,
|
|
12
|
+
rpc_error_to_http,
|
|
13
|
+
_http_exc_to_rpc,
|
|
14
|
+
_rpc_error_to_http,
|
|
15
|
+
create_standardized_error,
|
|
16
|
+
create_standardized_error_from_status,
|
|
17
|
+
to_rpc_error_payload,
|
|
18
|
+
)
|
|
19
|
+
from .exceptions import (
|
|
20
|
+
TigrblError,
|
|
21
|
+
PlanningError,
|
|
22
|
+
LabelError,
|
|
23
|
+
ConfigError,
|
|
24
|
+
SystemStepError,
|
|
25
|
+
ValidationError,
|
|
26
|
+
TransformError,
|
|
27
|
+
DeriveError,
|
|
28
|
+
KernelAbort,
|
|
29
|
+
coerce_runtime_error,
|
|
30
|
+
raise_for_in_errors,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"HTTPException",
|
|
35
|
+
"status",
|
|
36
|
+
# maps & messages
|
|
37
|
+
"HTTP_ERROR_MESSAGES",
|
|
38
|
+
"ERROR_MESSAGES",
|
|
39
|
+
"_HTTP_TO_RPC",
|
|
40
|
+
"_RPC_TO_HTTP",
|
|
41
|
+
# conversions
|
|
42
|
+
"http_exc_to_rpc",
|
|
43
|
+
"rpc_error_to_http",
|
|
44
|
+
"_http_exc_to_rpc",
|
|
45
|
+
"_rpc_error_to_http",
|
|
46
|
+
"create_standardized_error",
|
|
47
|
+
"create_standardized_error_from_status",
|
|
48
|
+
"to_rpc_error_payload",
|
|
49
|
+
# typed errors + helpers
|
|
50
|
+
"TigrblError",
|
|
51
|
+
"PlanningError",
|
|
52
|
+
"LabelError",
|
|
53
|
+
"ConfigError",
|
|
54
|
+
"SystemStepError",
|
|
55
|
+
"ValidationError",
|
|
56
|
+
"TransformError",
|
|
57
|
+
"DeriveError",
|
|
58
|
+
"KernelAbort",
|
|
59
|
+
"coerce_runtime_error",
|
|
60
|
+
"raise_for_in_errors",
|
|
61
|
+
]
|