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
tigrbl/bindings/rpc.py
ADDED
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
# tigrbl/v3/bindings/rpc.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import inspect
|
|
5
|
+
import logging
|
|
6
|
+
from types import SimpleNamespace
|
|
7
|
+
from typing import (
|
|
8
|
+
Any,
|
|
9
|
+
Awaitable,
|
|
10
|
+
Callable,
|
|
11
|
+
Dict,
|
|
12
|
+
Mapping,
|
|
13
|
+
Optional,
|
|
14
|
+
Sequence,
|
|
15
|
+
Tuple,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
from pydantic import BaseModel
|
|
19
|
+
|
|
20
|
+
from ..op import OpSpec
|
|
21
|
+
from ..op.types import PHASES
|
|
22
|
+
from ..runtime import executor as _executor # expects _invoke(request, db, phases, ctx)
|
|
23
|
+
|
|
24
|
+
# Prefer Kernel phase-chains if available (atoms + system steps + hooks)
|
|
25
|
+
try:
|
|
26
|
+
from ..runtime.kernel import build_phase_chains as _kernel_build_phase_chains # type: ignore
|
|
27
|
+
except Exception: # pragma: no cover
|
|
28
|
+
_kernel_build_phase_chains = None # type: ignore
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger("uvicorn")
|
|
31
|
+
logger.debug("Loaded module v3/bindings/rpc")
|
|
32
|
+
|
|
33
|
+
_Key = Tuple[str, str] # (alias, target)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# Mapping with attribute-style access
|
|
37
|
+
class AttrDict(dict):
|
|
38
|
+
def __getattr__(self, item: str) -> Any: # pragma: no cover - trivial
|
|
39
|
+
try:
|
|
40
|
+
return self[item]
|
|
41
|
+
except KeyError as e: # pragma: no cover - debug helper
|
|
42
|
+
raise AttributeError(item) from e
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# ───────────────────────────────────────────────────────────────────────────────
|
|
46
|
+
# Helpers
|
|
47
|
+
# ───────────────────────────────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _ns(obj: Any, name: str) -> Any:
|
|
51
|
+
ns = getattr(obj, name, None)
|
|
52
|
+
if ns is None:
|
|
53
|
+
ns = SimpleNamespace()
|
|
54
|
+
setattr(obj, name, ns)
|
|
55
|
+
return ns
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _get_phase_chains(
|
|
59
|
+
model: type, alias: str
|
|
60
|
+
) -> Dict[str, Sequence[Callable[..., Awaitable[Any]]]]:
|
|
61
|
+
"""
|
|
62
|
+
Prefer building via runtime Kernel (atoms + system steps + hooks in one lifecycle).
|
|
63
|
+
Fallback: read the pre-built model.hooks.<alias> chains directly.
|
|
64
|
+
"""
|
|
65
|
+
if _kernel_build_phase_chains is not None:
|
|
66
|
+
try:
|
|
67
|
+
return _kernel_build_phase_chains(model, alias)
|
|
68
|
+
except Exception:
|
|
69
|
+
logger.exception(
|
|
70
|
+
"Kernel build_phase_chains failed for %s.%s; falling back to hooks",
|
|
71
|
+
getattr(model, "__name__", model),
|
|
72
|
+
alias,
|
|
73
|
+
)
|
|
74
|
+
hooks_root = _ns(model, "hooks")
|
|
75
|
+
alias_ns = getattr(hooks_root, alias, None)
|
|
76
|
+
out: Dict[str, Sequence[Callable[..., Awaitable[Any]]]] = {}
|
|
77
|
+
for ph in PHASES:
|
|
78
|
+
out[ph] = list(getattr(alias_ns, ph, []) or [])
|
|
79
|
+
return out
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _coerce_payload(payload: Any) -> Any:
|
|
83
|
+
"""Normalize common payload shapes.
|
|
84
|
+
|
|
85
|
+
``dict``-like and Pydantic models become plain ``dict``s. ``None`` becomes an
|
|
86
|
+
empty ``dict``. Sequence payloads (used by bulk operations) pass through as
|
|
87
|
+
lists of ``dict``s when possible; otherwise the original sequence is
|
|
88
|
+
returned. Any other type yields an empty ``dict``.
|
|
89
|
+
"""
|
|
90
|
+
if payload is None:
|
|
91
|
+
return {}
|
|
92
|
+
if isinstance(payload, BaseModel):
|
|
93
|
+
try:
|
|
94
|
+
return payload.model_dump(exclude_none=False)
|
|
95
|
+
except Exception:
|
|
96
|
+
return dict(payload.__dict__)
|
|
97
|
+
if isinstance(payload, Mapping):
|
|
98
|
+
return dict(payload)
|
|
99
|
+
if isinstance(payload, Sequence) and not isinstance(payload, (str, bytes)):
|
|
100
|
+
out: list[Any] = []
|
|
101
|
+
for item in payload:
|
|
102
|
+
if isinstance(item, Mapping):
|
|
103
|
+
out.append(dict(item))
|
|
104
|
+
else:
|
|
105
|
+
out.append(item)
|
|
106
|
+
return out
|
|
107
|
+
return {}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _ensure_jsonable(obj: Any) -> Any:
|
|
111
|
+
"""Best-effort conversion of DB rows or ORM objects to primitives."""
|
|
112
|
+
if isinstance(obj, (list, tuple)):
|
|
113
|
+
return [_ensure_jsonable(x) for x in obj]
|
|
114
|
+
if isinstance(obj, Mapping):
|
|
115
|
+
try:
|
|
116
|
+
return AttrDict({k: _ensure_jsonable(v) for k, v in dict(obj).items()})
|
|
117
|
+
except Exception:
|
|
118
|
+
pass
|
|
119
|
+
try:
|
|
120
|
+
data = vars(obj)
|
|
121
|
+
except TypeError:
|
|
122
|
+
return obj
|
|
123
|
+
return AttrDict(
|
|
124
|
+
{k: _ensure_jsonable(v) for k, v in data.items() if not k.startswith("_")}
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _validate_input(
|
|
129
|
+
model: type, alias: str, target: str, payload: Mapping[str, Any]
|
|
130
|
+
) -> Mapping[str, Any]:
|
|
131
|
+
"""Choose the appropriate request schema (if any) and validate/normalize payload."""
|
|
132
|
+
schemas_root = getattr(model, "schemas", None)
|
|
133
|
+
if not schemas_root:
|
|
134
|
+
return payload
|
|
135
|
+
alias_ns = getattr(schemas_root, alias, None)
|
|
136
|
+
if not alias_ns:
|
|
137
|
+
return payload
|
|
138
|
+
|
|
139
|
+
in_model = getattr(alias_ns, "in_", None)
|
|
140
|
+
|
|
141
|
+
if in_model and inspect.isclass(in_model) and issubclass(in_model, BaseModel):
|
|
142
|
+
try:
|
|
143
|
+
inst = in_model.model_validate(payload) # type: ignore[arg-type]
|
|
144
|
+
return inst.model_dump(exclude_none=True)
|
|
145
|
+
except Exception as e:
|
|
146
|
+
# Let the executor/runtime error mappers standardize later; pass original payload
|
|
147
|
+
logger.debug(
|
|
148
|
+
"rpc input validation failed for %s.%s: %s",
|
|
149
|
+
model.__name__,
|
|
150
|
+
alias,
|
|
151
|
+
e,
|
|
152
|
+
exc_info=True,
|
|
153
|
+
)
|
|
154
|
+
return payload
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _serialize_output(model: type, alias: str, target: str, result: Any) -> Any:
|
|
158
|
+
"""Serialize result(s) if an OUT schema is available for the op.
|
|
159
|
+
|
|
160
|
+
For 'list', the OUT schema represents the element shape.
|
|
161
|
+
"""
|
|
162
|
+
schemas_root = getattr(model, "schemas", None)
|
|
163
|
+
if not schemas_root:
|
|
164
|
+
return _ensure_jsonable(result)
|
|
165
|
+
alias_ns = getattr(schemas_root, alias, None)
|
|
166
|
+
if not alias_ns:
|
|
167
|
+
return _ensure_jsonable(result)
|
|
168
|
+
|
|
169
|
+
if target in {"bulk_create", "bulk_update", "bulk_replace", "bulk_merge"}:
|
|
170
|
+
out_model = getattr(alias_ns, "out_item", None)
|
|
171
|
+
else:
|
|
172
|
+
out_model = getattr(alias_ns, "out", None)
|
|
173
|
+
|
|
174
|
+
if (
|
|
175
|
+
not out_model
|
|
176
|
+
or not inspect.isclass(out_model)
|
|
177
|
+
or not issubclass(out_model, BaseModel)
|
|
178
|
+
):
|
|
179
|
+
return _ensure_jsonable(result)
|
|
180
|
+
|
|
181
|
+
try:
|
|
182
|
+
if target == "list" and isinstance(result, (list, tuple)):
|
|
183
|
+
return [
|
|
184
|
+
out_model.model_validate(x).model_dump(
|
|
185
|
+
exclude_none=False, by_alias=True
|
|
186
|
+
)
|
|
187
|
+
for x in result
|
|
188
|
+
]
|
|
189
|
+
if target in {
|
|
190
|
+
"bulk_create",
|
|
191
|
+
"bulk_update",
|
|
192
|
+
"bulk_replace",
|
|
193
|
+
"bulk_merge",
|
|
194
|
+
} and isinstance(result, (list, tuple)):
|
|
195
|
+
return [
|
|
196
|
+
out_model.model_validate(x).model_dump(
|
|
197
|
+
exclude_none=False, by_alias=True
|
|
198
|
+
)
|
|
199
|
+
for x in result
|
|
200
|
+
]
|
|
201
|
+
# Single object case
|
|
202
|
+
return out_model.model_validate(result).model_dump(
|
|
203
|
+
exclude_none=False, by_alias=True
|
|
204
|
+
)
|
|
205
|
+
except Exception as e:
|
|
206
|
+
# If serialization fails, let raw result through rather than failing the call
|
|
207
|
+
logger.debug(
|
|
208
|
+
"rpc output serialization failed for %s.%s: %s",
|
|
209
|
+
model.__name__,
|
|
210
|
+
alias,
|
|
211
|
+
e,
|
|
212
|
+
exc_info=True,
|
|
213
|
+
)
|
|
214
|
+
return _ensure_jsonable(result)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
# ───────────────────────────────────────────────────────────────────────────────
|
|
218
|
+
# RPC wrapper builder
|
|
219
|
+
# ───────────────────────────────────────────────────────────────────────────────
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _build_rpc_callable(model: type, sp: OpSpec) -> Callable[..., Awaitable[Any]]:
|
|
223
|
+
"""
|
|
224
|
+
Create an async callable that:
|
|
225
|
+
1) validates payload (if schema present),
|
|
226
|
+
2) runs the executor with the model's phase chains (Kernel-preferred),
|
|
227
|
+
3) serializes the result to the expected return form.
|
|
228
|
+
|
|
229
|
+
Signature:
|
|
230
|
+
async def rpc_method(payload: Mapping | BaseModel | None = None, *, db, request=None, ctx=None) -> Any
|
|
231
|
+
"""
|
|
232
|
+
alias = sp.alias
|
|
233
|
+
target = sp.target
|
|
234
|
+
|
|
235
|
+
async def _rpc_method(
|
|
236
|
+
payload: Any = None,
|
|
237
|
+
*,
|
|
238
|
+
db: Any,
|
|
239
|
+
request: Any = None,
|
|
240
|
+
ctx: Optional[Dict[str, Any]] = None,
|
|
241
|
+
) -> Any:
|
|
242
|
+
# 1) normalize + validate input
|
|
243
|
+
schemas_root = getattr(model, "schemas", None)
|
|
244
|
+
alias_ns = getattr(schemas_root, alias, None)
|
|
245
|
+
item_in_model = getattr(alias_ns, "in_item", None)
|
|
246
|
+
|
|
247
|
+
raw_payload = _coerce_payload(payload)
|
|
248
|
+
if target == "bulk_delete" and not isinstance(raw_payload, Mapping):
|
|
249
|
+
raw_payload = {"ids": raw_payload}
|
|
250
|
+
if (
|
|
251
|
+
target.startswith("bulk_")
|
|
252
|
+
and target != "bulk_delete"
|
|
253
|
+
and isinstance(raw_payload, Sequence)
|
|
254
|
+
):
|
|
255
|
+
merged_payload = []
|
|
256
|
+
for item in raw_payload:
|
|
257
|
+
if item_in_model and isinstance(item, Mapping):
|
|
258
|
+
norm = item_in_model.model_validate(dict(item)).model_dump(
|
|
259
|
+
exclude_none=True
|
|
260
|
+
)
|
|
261
|
+
merged_payload.append({**dict(item), **norm})
|
|
262
|
+
elif item_in_model:
|
|
263
|
+
norm = item_in_model.model_validate(item).model_dump(
|
|
264
|
+
exclude_none=True
|
|
265
|
+
)
|
|
266
|
+
merged_payload.append(norm)
|
|
267
|
+
else:
|
|
268
|
+
merged_payload.append(item)
|
|
269
|
+
else:
|
|
270
|
+
norm_payload = _validate_input(model, alias, target, raw_payload)
|
|
271
|
+
merged_payload = dict(raw_payload)
|
|
272
|
+
for key, value in norm_payload.items():
|
|
273
|
+
merged_payload[key] = value
|
|
274
|
+
|
|
275
|
+
# 2) build executor context & phases
|
|
276
|
+
base_ctx: Dict[str, Any] = dict(ctx or {})
|
|
277
|
+
base_ctx.setdefault("payload", merged_payload)
|
|
278
|
+
base_ctx.setdefault("db", db)
|
|
279
|
+
if request is not None:
|
|
280
|
+
base_ctx.setdefault("request", request)
|
|
281
|
+
# surface contextual metadata for runtime atoms
|
|
282
|
+
app_ref = (
|
|
283
|
+
getattr(request, "app", None)
|
|
284
|
+
or base_ctx.get("app")
|
|
285
|
+
or getattr(model, "api", None)
|
|
286
|
+
or model
|
|
287
|
+
)
|
|
288
|
+
base_ctx.setdefault("app", app_ref)
|
|
289
|
+
base_ctx.setdefault("api", base_ctx.get("api") or app_ref)
|
|
290
|
+
base_ctx.setdefault("model", model)
|
|
291
|
+
base_ctx.setdefault("op", alias)
|
|
292
|
+
base_ctx.setdefault("method", alias)
|
|
293
|
+
base_ctx.setdefault("target", target)
|
|
294
|
+
# helpful env metadata
|
|
295
|
+
base_ctx.setdefault(
|
|
296
|
+
"env",
|
|
297
|
+
SimpleNamespace(
|
|
298
|
+
method=alias, params=merged_payload, target=target, model=model
|
|
299
|
+
),
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
phases = _get_phase_chains(model, alias)
|
|
303
|
+
# RPC methods should return raw data for JSON-RPC envelopes;
|
|
304
|
+
# remove response rendering atoms (which produce Starlette responses)
|
|
305
|
+
# JSON-RPC endpoints handle rendering at the transport layer. Filter
|
|
306
|
+
# out response rendering atoms but preserve any POST_RESPONSE hooks.
|
|
307
|
+
phases["POST_RESPONSE"] = [
|
|
308
|
+
fn
|
|
309
|
+
for fn in phases.get("POST_RESPONSE", [])
|
|
310
|
+
if not (
|
|
311
|
+
isinstance(getattr(fn, "__tigrbl_label", None), str)
|
|
312
|
+
and getattr(fn, "__tigrbl_label").endswith("@out:dump")
|
|
313
|
+
)
|
|
314
|
+
]
|
|
315
|
+
|
|
316
|
+
base_ctx["response_serializer"] = lambda r: _serialize_output(
|
|
317
|
+
model, alias, target, r
|
|
318
|
+
)
|
|
319
|
+
# 3) run executor
|
|
320
|
+
result = await _executor._invoke(
|
|
321
|
+
request=request,
|
|
322
|
+
db=db,
|
|
323
|
+
phases=phases,
|
|
324
|
+
ctx=base_ctx,
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
return result
|
|
328
|
+
|
|
329
|
+
# Give the callable a nice name for introspection/logging
|
|
330
|
+
_rpc_method.__name__ = f"rpc_{model.__name__}_{alias}"
|
|
331
|
+
_rpc_method.__qualname__ = _rpc_method.__name__
|
|
332
|
+
_rpc_method.__doc__ = f"RPC method for {model.__name__}.{alias} ({target})"
|
|
333
|
+
|
|
334
|
+
return _rpc_method
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def _attach_one(model: type, sp: OpSpec) -> None:
|
|
338
|
+
rpc_root = _ns(model, "rpc")
|
|
339
|
+
fn = _build_rpc_callable(model, sp)
|
|
340
|
+
setattr(rpc_root, sp.alias, fn)
|
|
341
|
+
logger.debug("rpc: %s.%s registered", model.__name__, sp.alias)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
# ───────────────────────────────────────────────────────────────────────────────
|
|
345
|
+
# Public API
|
|
346
|
+
# ───────────────────────────────────────────────────────────────────────────────
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def register_and_attach(
|
|
350
|
+
model: type, specs: Sequence[OpSpec], *, only_keys: Optional[Sequence[_Key]] = None
|
|
351
|
+
) -> None:
|
|
352
|
+
"""
|
|
353
|
+
Register async callables under `model.rpc.<alias>` for each OpSpec.
|
|
354
|
+
If `only_keys` is provided, limit work to those (alias,target) pairs.
|
|
355
|
+
"""
|
|
356
|
+
wanted = set(only_keys or ())
|
|
357
|
+
for sp in specs:
|
|
358
|
+
key = (sp.alias, sp.target)
|
|
359
|
+
if wanted and key not in wanted:
|
|
360
|
+
continue
|
|
361
|
+
_attach_one(model, sp)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
__all__ = ["register_and_attach"]
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# tigrbl/v3/bindings/schemas/__init__.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from .builder import build_and_attach
|
|
6
|
+
|
|
7
|
+
logging.getLogger("uvicorn").setLevel(logging.DEBUG)
|
|
8
|
+
logger = logging.getLogger("uvicorn")
|
|
9
|
+
logger.debug("Loaded module v3/bindings/schemas/__init__")
|
|
10
|
+
|
|
11
|
+
__all__ = ["build_and_attach"]
|