tigrbl 0.0.1.dev1__py3-none-any.whl → 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tigrbl/README.md +94 -0
- tigrbl/__init__.py +139 -14
- tigrbl/api/__init__.py +6 -0
- tigrbl/api/_api.py +97 -0
- tigrbl/api/api_spec.py +30 -0
- tigrbl/api/mro_collect.py +43 -0
- tigrbl/api/shortcuts.py +56 -0
- tigrbl/api/tigrbl_api.py +291 -0
- tigrbl/app/__init__.py +0 -0
- tigrbl/app/_app.py +86 -0
- tigrbl/app/_model_registry.py +41 -0
- tigrbl/app/app_spec.py +42 -0
- tigrbl/app/mro_collect.py +67 -0
- tigrbl/app/shortcuts.py +65 -0
- tigrbl/app/tigrbl_app.py +319 -0
- tigrbl/bindings/__init__.py +73 -0
- tigrbl/bindings/api/__init__.py +12 -0
- tigrbl/bindings/api/common.py +109 -0
- tigrbl/bindings/api/include.py +256 -0
- tigrbl/bindings/api/resource_proxy.py +149 -0
- tigrbl/bindings/api/rpc.py +111 -0
- tigrbl/bindings/columns.py +49 -0
- tigrbl/bindings/handlers/__init__.py +11 -0
- tigrbl/bindings/handlers/builder.py +119 -0
- tigrbl/bindings/handlers/ctx.py +74 -0
- tigrbl/bindings/handlers/identifiers.py +228 -0
- tigrbl/bindings/handlers/namespaces.py +51 -0
- tigrbl/bindings/handlers/steps.py +276 -0
- tigrbl/bindings/hooks.py +311 -0
- tigrbl/bindings/model.py +194 -0
- tigrbl/bindings/model_helpers.py +139 -0
- tigrbl/bindings/model_registry.py +77 -0
- tigrbl/bindings/rest/__init__.py +7 -0
- tigrbl/bindings/rest/attach.py +34 -0
- tigrbl/bindings/rest/collection.py +286 -0
- tigrbl/bindings/rest/common.py +120 -0
- tigrbl/bindings/rest/fastapi.py +76 -0
- tigrbl/bindings/rest/helpers.py +119 -0
- tigrbl/bindings/rest/io.py +317 -0
- tigrbl/bindings/rest/io_headers.py +49 -0
- tigrbl/bindings/rest/member.py +386 -0
- tigrbl/bindings/rest/router.py +296 -0
- tigrbl/bindings/rest/routing.py +153 -0
- tigrbl/bindings/rpc.py +364 -0
- tigrbl/bindings/schemas/__init__.py +11 -0
- tigrbl/bindings/schemas/builder.py +348 -0
- tigrbl/bindings/schemas/defaults.py +260 -0
- tigrbl/bindings/schemas/utils.py +193 -0
- tigrbl/column/README.md +62 -0
- tigrbl/column/__init__.py +72 -0
- tigrbl/column/_column.py +96 -0
- tigrbl/column/column_spec.py +40 -0
- tigrbl/column/field_spec.py +31 -0
- tigrbl/column/infer/__init__.py +25 -0
- tigrbl/column/infer/core.py +92 -0
- tigrbl/column/infer/jsonhints.py +44 -0
- tigrbl/column/infer/planning.py +133 -0
- tigrbl/column/infer/types.py +102 -0
- tigrbl/column/infer/utils.py +59 -0
- tigrbl/column/io_spec.py +136 -0
- tigrbl/column/mro_collect.py +59 -0
- tigrbl/column/shortcuts.py +89 -0
- tigrbl/column/storage_spec.py +65 -0
- tigrbl/config/__init__.py +19 -0
- tigrbl/config/constants.py +224 -0
- tigrbl/config/defaults.py +29 -0
- tigrbl/config/resolver.py +295 -0
- tigrbl/core/__init__.py +47 -0
- tigrbl/core/crud/__init__.py +36 -0
- tigrbl/core/crud/bulk.py +168 -0
- tigrbl/core/crud/helpers/__init__.py +76 -0
- tigrbl/core/crud/helpers/db.py +92 -0
- tigrbl/core/crud/helpers/enum.py +86 -0
- tigrbl/core/crud/helpers/filters.py +162 -0
- tigrbl/core/crud/helpers/model.py +123 -0
- tigrbl/core/crud/helpers/normalize.py +99 -0
- tigrbl/core/crud/ops.py +235 -0
- tigrbl/ddl/__init__.py +344 -0
- tigrbl/decorators.py +17 -0
- tigrbl/deps/__init__.py +20 -0
- tigrbl/deps/fastapi.py +45 -0
- tigrbl/deps/favicon.svg +4 -0
- tigrbl/deps/jinja.py +27 -0
- tigrbl/deps/pydantic.py +10 -0
- tigrbl/deps/sqlalchemy.py +94 -0
- tigrbl/deps/starlette.py +36 -0
- tigrbl/engine/__init__.py +45 -0
- tigrbl/engine/_engine.py +144 -0
- tigrbl/engine/bind.py +33 -0
- tigrbl/engine/builders.py +236 -0
- tigrbl/engine/capabilities.py +29 -0
- tigrbl/engine/collect.py +111 -0
- tigrbl/engine/decorators.py +110 -0
- tigrbl/engine/docs/PLUGINS.md +49 -0
- tigrbl/engine/engine_spec.py +355 -0
- tigrbl/engine/plugins.py +52 -0
- tigrbl/engine/registry.py +36 -0
- tigrbl/engine/resolver.py +224 -0
- tigrbl/engine/shortcuts.py +216 -0
- tigrbl/hook/__init__.py +21 -0
- tigrbl/hook/_hook.py +22 -0
- tigrbl/hook/decorators.py +28 -0
- tigrbl/hook/hook_spec.py +24 -0
- tigrbl/hook/mro_collect.py +98 -0
- tigrbl/hook/shortcuts.py +44 -0
- tigrbl/hook/types.py +76 -0
- tigrbl/op/__init__.py +50 -0
- tigrbl/op/_op.py +31 -0
- tigrbl/op/canonical.py +31 -0
- tigrbl/op/collect.py +11 -0
- tigrbl/op/decorators.py +238 -0
- tigrbl/op/model_registry.py +301 -0
- tigrbl/op/mro_collect.py +99 -0
- tigrbl/op/resolver.py +216 -0
- tigrbl/op/types.py +136 -0
- tigrbl/orm/__init__.py +1 -0
- tigrbl/orm/mixins/_RowBound.py +83 -0
- tigrbl/orm/mixins/__init__.py +95 -0
- tigrbl/orm/mixins/bootstrappable.py +113 -0
- tigrbl/orm/mixins/bound.py +47 -0
- tigrbl/orm/mixins/edges.py +40 -0
- tigrbl/orm/mixins/fields.py +165 -0
- tigrbl/orm/mixins/hierarchy.py +54 -0
- tigrbl/orm/mixins/key_digest.py +44 -0
- tigrbl/orm/mixins/lifecycle.py +115 -0
- tigrbl/orm/mixins/locks.py +51 -0
- tigrbl/orm/mixins/markers.py +16 -0
- tigrbl/orm/mixins/operations.py +57 -0
- tigrbl/orm/mixins/ownable.py +337 -0
- tigrbl/orm/mixins/principals.py +98 -0
- tigrbl/orm/mixins/tenant_bound.py +301 -0
- tigrbl/orm/mixins/upsertable.py +118 -0
- tigrbl/orm/mixins/utils.py +49 -0
- tigrbl/orm/tables/__init__.py +72 -0
- tigrbl/orm/tables/_base.py +8 -0
- tigrbl/orm/tables/audit.py +56 -0
- tigrbl/orm/tables/client.py +25 -0
- tigrbl/orm/tables/group.py +29 -0
- tigrbl/orm/tables/org.py +30 -0
- tigrbl/orm/tables/rbac.py +76 -0
- tigrbl/orm/tables/status.py +106 -0
- tigrbl/orm/tables/tenant.py +22 -0
- tigrbl/orm/tables/user.py +39 -0
- tigrbl/response/README.md +34 -0
- tigrbl/response/__init__.py +33 -0
- tigrbl/response/bind.py +12 -0
- tigrbl/response/decorators.py +37 -0
- tigrbl/response/resolver.py +83 -0
- tigrbl/response/shortcuts.py +171 -0
- tigrbl/response/types.py +49 -0
- tigrbl/rest/__init__.py +27 -0
- tigrbl/runtime/README.md +129 -0
- tigrbl/runtime/__init__.py +20 -0
- tigrbl/runtime/atoms/__init__.py +102 -0
- tigrbl/runtime/atoms/emit/__init__.py +42 -0
- tigrbl/runtime/atoms/emit/paired_post.py +158 -0
- tigrbl/runtime/atoms/emit/paired_pre.py +106 -0
- tigrbl/runtime/atoms/emit/readtime_alias.py +120 -0
- tigrbl/runtime/atoms/out/__init__.py +38 -0
- tigrbl/runtime/atoms/out/masking.py +135 -0
- tigrbl/runtime/atoms/refresh/__init__.py +38 -0
- tigrbl/runtime/atoms/refresh/demand.py +130 -0
- tigrbl/runtime/atoms/resolve/__init__.py +40 -0
- tigrbl/runtime/atoms/resolve/assemble.py +167 -0
- tigrbl/runtime/atoms/resolve/paired_gen.py +147 -0
- tigrbl/runtime/atoms/response/__init__.py +19 -0
- tigrbl/runtime/atoms/response/headers_from_payload.py +57 -0
- tigrbl/runtime/atoms/response/negotiate.py +30 -0
- tigrbl/runtime/atoms/response/negotiation.py +43 -0
- tigrbl/runtime/atoms/response/render.py +36 -0
- tigrbl/runtime/atoms/response/renderer.py +116 -0
- tigrbl/runtime/atoms/response/template.py +44 -0
- tigrbl/runtime/atoms/response/templates.py +88 -0
- tigrbl/runtime/atoms/schema/__init__.py +40 -0
- tigrbl/runtime/atoms/schema/collect_in.py +21 -0
- tigrbl/runtime/atoms/schema/collect_out.py +21 -0
- tigrbl/runtime/atoms/storage/__init__.py +38 -0
- tigrbl/runtime/atoms/storage/to_stored.py +167 -0
- tigrbl/runtime/atoms/wire/__init__.py +45 -0
- tigrbl/runtime/atoms/wire/build_in.py +166 -0
- tigrbl/runtime/atoms/wire/build_out.py +87 -0
- tigrbl/runtime/atoms/wire/dump.py +206 -0
- tigrbl/runtime/atoms/wire/validate_in.py +227 -0
- tigrbl/runtime/context.py +206 -0
- tigrbl/runtime/errors/__init__.py +61 -0
- tigrbl/runtime/errors/converters.py +214 -0
- tigrbl/runtime/errors/exceptions.py +124 -0
- tigrbl/runtime/errors/mappings.py +71 -0
- tigrbl/runtime/errors/utils.py +150 -0
- tigrbl/runtime/events.py +209 -0
- tigrbl/runtime/executor/__init__.py +6 -0
- tigrbl/runtime/executor/guards.py +132 -0
- tigrbl/runtime/executor/helpers.py +88 -0
- tigrbl/runtime/executor/invoke.py +150 -0
- tigrbl/runtime/executor/types.py +84 -0
- tigrbl/runtime/kernel.py +644 -0
- tigrbl/runtime/labels.py +353 -0
- tigrbl/runtime/opview.py +89 -0
- tigrbl/runtime/ordering.py +256 -0
- tigrbl/runtime/system.py +279 -0
- tigrbl/runtime/trace.py +330 -0
- tigrbl/schema/__init__.py +38 -0
- tigrbl/schema/_schema.py +27 -0
- tigrbl/schema/builder/__init__.py +17 -0
- tigrbl/schema/builder/build_schema.py +209 -0
- tigrbl/schema/builder/cache.py +24 -0
- tigrbl/schema/builder/compat.py +16 -0
- tigrbl/schema/builder/extras.py +85 -0
- tigrbl/schema/builder/helpers.py +51 -0
- tigrbl/schema/builder/list_params.py +117 -0
- tigrbl/schema/builder/strip_parent_fields.py +70 -0
- tigrbl/schema/collect.py +79 -0
- tigrbl/schema/decorators.py +68 -0
- tigrbl/schema/get_schema.py +86 -0
- tigrbl/schema/schema_spec.py +20 -0
- tigrbl/schema/shortcuts.py +42 -0
- tigrbl/schema/types.py +34 -0
- tigrbl/schema/utils.py +143 -0
- tigrbl/session/README.md +14 -0
- tigrbl/session/__init__.py +28 -0
- tigrbl/session/abc.py +76 -0
- tigrbl/session/base.py +151 -0
- tigrbl/session/decorators.py +43 -0
- tigrbl/session/default.py +118 -0
- tigrbl/session/shortcuts.py +50 -0
- tigrbl/session/spec.py +112 -0
- tigrbl/shortcuts.py +22 -0
- tigrbl/specs.py +44 -0
- tigrbl/system/__init__.py +13 -0
- tigrbl/system/diagnostics/__init__.py +24 -0
- tigrbl/system/diagnostics/compat.py +31 -0
- tigrbl/system/diagnostics/healthz.py +41 -0
- tigrbl/system/diagnostics/hookz.py +51 -0
- tigrbl/system/diagnostics/kernelz.py +20 -0
- tigrbl/system/diagnostics/methodz.py +43 -0
- tigrbl/system/diagnostics/router.py +73 -0
- tigrbl/system/diagnostics/utils.py +43 -0
- tigrbl/system/uvicorn.py +60 -0
- tigrbl/table/__init__.py +9 -0
- tigrbl/table/_base.py +260 -0
- tigrbl/table/_table.py +54 -0
- tigrbl/table/mro_collect.py +69 -0
- tigrbl/table/shortcuts.py +57 -0
- tigrbl/table/table_spec.py +28 -0
- tigrbl/transport/__init__.py +74 -0
- tigrbl/transport/jsonrpc/__init__.py +19 -0
- tigrbl/transport/jsonrpc/dispatcher.py +352 -0
- tigrbl/transport/jsonrpc/helpers.py +115 -0
- tigrbl/transport/jsonrpc/models.py +41 -0
- tigrbl/transport/rest/__init__.py +25 -0
- tigrbl/transport/rest/aggregator.py +132 -0
- tigrbl/types/__init__.py +170 -0
- tigrbl/types/allow_anon_provider.py +19 -0
- tigrbl/types/authn_abc.py +30 -0
- tigrbl/types/nested_path_provider.py +22 -0
- tigrbl/types/op.py +35 -0
- tigrbl/types/op_config_provider.py +17 -0
- tigrbl/types/op_verb_alias_provider.py +33 -0
- tigrbl/types/request_extras_provider.py +22 -0
- tigrbl/types/response_extras_provider.py +22 -0
- tigrbl/types/table_config_provider.py +13 -0
- tigrbl/types/uuid.py +55 -0
- tigrbl-0.3.0.dist-info/METADATA +516 -0
- tigrbl-0.3.0.dist-info/RECORD +266 -0
- {tigrbl-0.0.1.dev1.dist-info → tigrbl-0.3.0.dist-info}/WHEEL +1 -1
- tigrbl-0.3.0.dist-info/licenses/LICENSE +201 -0
- tigrbl/ExampleAgent.py +0 -1
- tigrbl-0.0.1.dev1.dist-info/METADATA +0 -18
- tigrbl-0.0.1.dev1.dist-info/RECORD +0 -5
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
# tigrbl/v3/bindings/handlers/steps.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
6
|
+
from functools import lru_cache
|
|
7
|
+
from typing import Any, Callable, Mapping, Optional
|
|
8
|
+
|
|
9
|
+
from ... import core as _core
|
|
10
|
+
from ...op import OpSpec
|
|
11
|
+
from ...op.types import StepFn
|
|
12
|
+
from ...runtime.executor import _Ctx
|
|
13
|
+
from .ctx import _ctx_db, _ctx_payload, _ctx_request
|
|
14
|
+
from .identifiers import _resolve_ident
|
|
15
|
+
|
|
16
|
+
logging.getLogger("uvicorn").setLevel(logging.DEBUG)
|
|
17
|
+
logger = logging.getLogger("uvicorn")
|
|
18
|
+
logger.debug("Loaded module v3/bindings/handlers/steps")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
async def _call_list_core(
|
|
22
|
+
fn: Callable[..., Any],
|
|
23
|
+
model: type,
|
|
24
|
+
payload: Mapping[str, Any],
|
|
25
|
+
ctx: Mapping[str, Any],
|
|
26
|
+
):
|
|
27
|
+
filters = dict(payload) if isinstance(payload, Mapping) else {}
|
|
28
|
+
skip = filters.pop("skip", None)
|
|
29
|
+
limit = filters.pop("limit", None)
|
|
30
|
+
filters_arg = filters if filters else None
|
|
31
|
+
|
|
32
|
+
db = _ctx_db(ctx)
|
|
33
|
+
req = _ctx_request(ctx)
|
|
34
|
+
|
|
35
|
+
candidates: list[tuple[tuple, dict]] = []
|
|
36
|
+
|
|
37
|
+
def add_candidate(
|
|
38
|
+
use_pos_filters: bool, use_pos_db: bool, with_req: bool, with_pag: bool
|
|
39
|
+
):
|
|
40
|
+
args: tuple = ()
|
|
41
|
+
kwargs: dict = {}
|
|
42
|
+
if use_pos_filters:
|
|
43
|
+
args += (filters_arg,)
|
|
44
|
+
else:
|
|
45
|
+
kwargs["filters"] = filters_arg
|
|
46
|
+
if use_pos_db:
|
|
47
|
+
args += (db,)
|
|
48
|
+
else:
|
|
49
|
+
kwargs["db"] = db
|
|
50
|
+
if with_req and req is not None:
|
|
51
|
+
kwargs["request"] = req
|
|
52
|
+
if with_pag:
|
|
53
|
+
if skip is not None:
|
|
54
|
+
kwargs["skip"] = skip
|
|
55
|
+
if limit is not None:
|
|
56
|
+
kwargs["limit"] = limit
|
|
57
|
+
candidates.append((args, kwargs))
|
|
58
|
+
|
|
59
|
+
add_candidate(False, False, True, True)
|
|
60
|
+
add_candidate(True, False, True, True)
|
|
61
|
+
add_candidate(True, True, True, True)
|
|
62
|
+
|
|
63
|
+
add_candidate(False, False, False, True)
|
|
64
|
+
add_candidate(True, False, False, True)
|
|
65
|
+
add_candidate(True, True, False, True)
|
|
66
|
+
|
|
67
|
+
add_candidate(False, False, True, False)
|
|
68
|
+
add_candidate(True, False, True, False)
|
|
69
|
+
add_candidate(True, True, True, False)
|
|
70
|
+
|
|
71
|
+
add_candidate(False, False, False, False)
|
|
72
|
+
add_candidate(True, False, False, False)
|
|
73
|
+
add_candidate(True, True, False, False)
|
|
74
|
+
|
|
75
|
+
last_err: Optional[BaseException] = None
|
|
76
|
+
for args, kwargs in candidates:
|
|
77
|
+
try:
|
|
78
|
+
logger.debug("Trying list core call with args=%s kwargs=%s", args, kwargs)
|
|
79
|
+
rv = fn(model, *args, **kwargs)
|
|
80
|
+
if inspect.isawaitable(rv):
|
|
81
|
+
logger.debug("Awaiting async result for list core")
|
|
82
|
+
return await rv
|
|
83
|
+
return rv
|
|
84
|
+
except TypeError as e:
|
|
85
|
+
logger.debug("Candidate failed with TypeError: %s", e)
|
|
86
|
+
last_err = e
|
|
87
|
+
continue
|
|
88
|
+
if last_err:
|
|
89
|
+
logger.debug("Reraising last TypeError from list core resolution")
|
|
90
|
+
raise last_err
|
|
91
|
+
raise RuntimeError("list() call resolution failed unexpectedly")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _accepted_kw(handler: Callable[..., Any]) -> set[str]:
|
|
95
|
+
try:
|
|
96
|
+
sig = inspect.signature(handler)
|
|
97
|
+
except Exception as exc:
|
|
98
|
+
logger.debug("Failed to inspect handler %r: %s", handler, exc)
|
|
99
|
+
return {"ctx"}
|
|
100
|
+
|
|
101
|
+
names: set[str] = set()
|
|
102
|
+
for p in sig.parameters.values():
|
|
103
|
+
if p.kind in (p.VAR_KEYWORD, p.VAR_POSITIONAL):
|
|
104
|
+
logger.debug("Handler %r accepts arbitrary params", handler)
|
|
105
|
+
return {"ctx", "db", "payload", "request", "model", "op", "spec", "alias"}
|
|
106
|
+
names.add(p.name)
|
|
107
|
+
logger.debug("Handler %r accepts keywords %s", handler, names)
|
|
108
|
+
return names
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _wrap_custom(model: type, sp: OpSpec, user_handler: Callable[..., Any]) -> StepFn:
|
|
112
|
+
async def step(ctx: Any) -> Any:
|
|
113
|
+
db = _ctx_db(ctx)
|
|
114
|
+
payload = _ctx_payload(ctx)
|
|
115
|
+
request = _ctx_request(ctx)
|
|
116
|
+
isolated = _Ctx.ensure(request=request, db=db, seed=ctx)
|
|
117
|
+
bound = getattr(model, getattr(user_handler, "__name__", ""), user_handler)
|
|
118
|
+
wanted = _accepted_kw(bound)
|
|
119
|
+
|
|
120
|
+
kw = {}
|
|
121
|
+
if "ctx" in wanted:
|
|
122
|
+
kw["ctx"] = isolated
|
|
123
|
+
if "db" in wanted:
|
|
124
|
+
kw["db"] = db
|
|
125
|
+
if "payload" in wanted:
|
|
126
|
+
kw["payload"] = payload
|
|
127
|
+
if "request" in wanted:
|
|
128
|
+
kw["request"] = request
|
|
129
|
+
if "model" in wanted:
|
|
130
|
+
kw["model"] = model
|
|
131
|
+
if "op" in wanted:
|
|
132
|
+
kw["op"] = sp
|
|
133
|
+
if "spec" in wanted:
|
|
134
|
+
kw["spec"] = sp
|
|
135
|
+
if "alias" in wanted:
|
|
136
|
+
kw["alias"] = sp.alias
|
|
137
|
+
logger.debug("Calling custom handler %r with kw=%s", bound, kw)
|
|
138
|
+
rv = bound(**kw) # type: ignore[misc]
|
|
139
|
+
if inspect.isawaitable(rv):
|
|
140
|
+
logger.debug("Awaiting async custom handler")
|
|
141
|
+
return await rv
|
|
142
|
+
return rv
|
|
143
|
+
|
|
144
|
+
step.__name__ = getattr(user_handler, "__name__", step.__name__)
|
|
145
|
+
step.__qualname__ = getattr(user_handler, "__qualname__", step.__name__)
|
|
146
|
+
step.__module__ = getattr(user_handler, "__module__", step.__module__)
|
|
147
|
+
return step
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@lru_cache(maxsize=None)
|
|
151
|
+
def _wrap_core(model: type, target: str) -> StepFn:
|
|
152
|
+
logger.debug("Creating core wrapper for %s.%s", model.__name__, target)
|
|
153
|
+
|
|
154
|
+
async def create_step(ctx: Any) -> Any:
|
|
155
|
+
db = _ctx_db(ctx)
|
|
156
|
+
payload = _ctx_payload(ctx)
|
|
157
|
+
logger.debug("Dispatching to core.create")
|
|
158
|
+
return await _core.create(model, payload, db=db)
|
|
159
|
+
|
|
160
|
+
async def read_step(ctx: Any) -> Any:
|
|
161
|
+
db = _ctx_db(ctx)
|
|
162
|
+
ident = _resolve_ident(model, ctx)
|
|
163
|
+
logger.debug("Dispatching to core.read with ident=%r", ident)
|
|
164
|
+
return await _core.read(model, ident, db=db)
|
|
165
|
+
|
|
166
|
+
async def update_step(ctx: Any) -> Any:
|
|
167
|
+
db = _ctx_db(ctx)
|
|
168
|
+
ident = _resolve_ident(model, ctx)
|
|
169
|
+
payload = _ctx_payload(ctx)
|
|
170
|
+
logger.debug("Dispatching to core.update with ident=%r", ident)
|
|
171
|
+
return await _core.update(model, ident, payload, db=db)
|
|
172
|
+
|
|
173
|
+
async def replace_step(ctx: Any) -> Any:
|
|
174
|
+
db = _ctx_db(ctx)
|
|
175
|
+
ident = _resolve_ident(model, ctx)
|
|
176
|
+
payload = _ctx_payload(ctx)
|
|
177
|
+
logger.debug("Dispatching to core.replace with ident=%r", ident)
|
|
178
|
+
return await _core.replace(model, ident, payload, db=db)
|
|
179
|
+
|
|
180
|
+
async def merge_step(ctx: Any) -> Any:
|
|
181
|
+
db = _ctx_db(ctx)
|
|
182
|
+
ident = _resolve_ident(model, ctx)
|
|
183
|
+
payload = _ctx_payload(ctx)
|
|
184
|
+
logger.debug("Dispatching to core.merge with ident=%r", ident)
|
|
185
|
+
return await _core.merge(model, ident, payload, db=db)
|
|
186
|
+
|
|
187
|
+
async def delete_step(ctx: Any) -> Any:
|
|
188
|
+
db = _ctx_db(ctx)
|
|
189
|
+
ident = _resolve_ident(model, ctx)
|
|
190
|
+
logger.debug("Dispatching to core.delete with ident=%r", ident)
|
|
191
|
+
return await _core.delete(model, ident, db=db)
|
|
192
|
+
|
|
193
|
+
async def list_step(ctx: Any) -> Any:
|
|
194
|
+
payload = _ctx_payload(ctx)
|
|
195
|
+
logger.debug("Dispatching to core.list")
|
|
196
|
+
return await _call_list_core(_core.list, model, payload, ctx)
|
|
197
|
+
|
|
198
|
+
async def clear_step(ctx: Any) -> Any:
|
|
199
|
+
db = _ctx_db(ctx)
|
|
200
|
+
logger.debug("Dispatching to core.clear")
|
|
201
|
+
return await _core.clear(model, {}, db=db)
|
|
202
|
+
|
|
203
|
+
async def bulk_create_step(ctx: Any) -> Any:
|
|
204
|
+
db = _ctx_db(ctx)
|
|
205
|
+
payload = _ctx_payload(ctx)
|
|
206
|
+
logger.debug("Dispatching to core.bulk_create")
|
|
207
|
+
if not isinstance(payload, list):
|
|
208
|
+
raise TypeError("bulk_create expects a list payload")
|
|
209
|
+
return await _core.bulk_create(model, payload, db=db)
|
|
210
|
+
|
|
211
|
+
async def bulk_update_step(ctx: Any) -> Any:
|
|
212
|
+
db = _ctx_db(ctx)
|
|
213
|
+
payload = _ctx_payload(ctx)
|
|
214
|
+
logger.debug("Dispatching to core.bulk_update")
|
|
215
|
+
if not isinstance(payload, list):
|
|
216
|
+
raise TypeError("bulk_update expects a list payload")
|
|
217
|
+
return await _core.bulk_update(model, payload, db=db)
|
|
218
|
+
|
|
219
|
+
async def bulk_replace_step(ctx: Any) -> Any:
|
|
220
|
+
db = _ctx_db(ctx)
|
|
221
|
+
payload = _ctx_payload(ctx)
|
|
222
|
+
logger.debug("Dispatching to core.bulk_replace")
|
|
223
|
+
if not isinstance(payload, list):
|
|
224
|
+
raise TypeError("bulk_replace expects a list payload")
|
|
225
|
+
return await _core.bulk_replace(model, payload, db=db)
|
|
226
|
+
|
|
227
|
+
async def bulk_merge_step(ctx: Any) -> Any:
|
|
228
|
+
db = _ctx_db(ctx)
|
|
229
|
+
payload = _ctx_payload(ctx)
|
|
230
|
+
logger.debug("Dispatching to core.bulk_merge")
|
|
231
|
+
if not isinstance(payload, list):
|
|
232
|
+
raise TypeError("bulk_merge expects a list payload")
|
|
233
|
+
return await _core.bulk_merge(model, payload, db=db)
|
|
234
|
+
|
|
235
|
+
async def bulk_delete_step(ctx: Any) -> Any:
|
|
236
|
+
db = _ctx_db(ctx)
|
|
237
|
+
payload = _ctx_payload(ctx)
|
|
238
|
+
logger.debug("Dispatching to core.bulk_delete")
|
|
239
|
+
ids = payload.get("ids") if isinstance(payload, Mapping) else []
|
|
240
|
+
return await _core.bulk_delete(model, ids, db=db)
|
|
241
|
+
|
|
242
|
+
async def default_step(ctx: Any) -> Any:
|
|
243
|
+
logger.debug("No core operation matched; returning payload")
|
|
244
|
+
return _ctx_payload(ctx)
|
|
245
|
+
|
|
246
|
+
steps: dict[str, StepFn] = {
|
|
247
|
+
"create": create_step,
|
|
248
|
+
"read": read_step,
|
|
249
|
+
"update": update_step,
|
|
250
|
+
"replace": replace_step,
|
|
251
|
+
"merge": merge_step,
|
|
252
|
+
"delete": delete_step,
|
|
253
|
+
"list": list_step,
|
|
254
|
+
"clear": clear_step,
|
|
255
|
+
"bulk_create": bulk_create_step,
|
|
256
|
+
"bulk_update": bulk_update_step,
|
|
257
|
+
"bulk_replace": bulk_replace_step,
|
|
258
|
+
"bulk_merge": bulk_merge_step,
|
|
259
|
+
"bulk_delete": bulk_delete_step,
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
step = steps.get(target, default_step)
|
|
263
|
+
|
|
264
|
+
fn = getattr(_core, target, None)
|
|
265
|
+
step.__name__ = getattr(fn, "__name__", step.__name__)
|
|
266
|
+
step.__qualname__ = getattr(fn, "__qualname__", step.__name__)
|
|
267
|
+
step.__module__ = getattr(fn, "__module__", step.__module__)
|
|
268
|
+
return step
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
__all__ = [
|
|
272
|
+
"_call_list_core",
|
|
273
|
+
"_accepted_kw",
|
|
274
|
+
"_wrap_custom",
|
|
275
|
+
"_wrap_core",
|
|
276
|
+
]
|
tigrbl/bindings/hooks.py
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
# tigrbl/v3/bindings/hooks.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
|
+
Callable,
|
|
10
|
+
Dict,
|
|
11
|
+
Iterable,
|
|
12
|
+
List,
|
|
13
|
+
Mapping,
|
|
14
|
+
Optional,
|
|
15
|
+
Sequence,
|
|
16
|
+
Tuple,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
from ..op import OpSpec
|
|
20
|
+
from ..hook import HookSpec
|
|
21
|
+
from ..hook.types import PHASES, StepFn
|
|
22
|
+
from ..config.constants import (
|
|
23
|
+
TIGRBL_API_HOOKS_ATTR,
|
|
24
|
+
TIGRBL_HOOKS_ATTR,
|
|
25
|
+
CTX_SKIP_PERSIST_FLAG,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger("uvicorn")
|
|
29
|
+
logger.debug("Loaded module v3/bindings/hooks")
|
|
30
|
+
|
|
31
|
+
_Key = Tuple[str, str] # (alias, target)
|
|
32
|
+
|
|
33
|
+
# ───────────────────────────────────────────────────────────────────────────────
|
|
34
|
+
# Phase groupings (v2-compatible precedence)
|
|
35
|
+
# pre-like: API → MODEL → OP
|
|
36
|
+
# post/error: OP → MODEL → API
|
|
37
|
+
# ───────────────────────────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
_PRE_LIKE = frozenset({"PRE_TX_BEGIN", "START_TX", "PRE_HANDLER", "PRE_COMMIT"})
|
|
40
|
+
_POST_LIKE = frozenset({"POST_HANDLER", "POST_COMMIT", "POST_RESPONSE", "FINAL"})
|
|
41
|
+
_ERROR_LIKE = frozenset(
|
|
42
|
+
{
|
|
43
|
+
"ON_ROLLBACK",
|
|
44
|
+
"ON_PRE_HANDLER_ERROR",
|
|
45
|
+
"ON_HANDLER_ERROR",
|
|
46
|
+
"ON_POST_HANDLER_ERROR",
|
|
47
|
+
"ON_PRE_COMMIT_ERROR",
|
|
48
|
+
# v3 uses END_TX; map v2's ON_COMMIT_ERROR → ON_END_TX_ERROR
|
|
49
|
+
"ON_END_TX_ERROR",
|
|
50
|
+
"ON_POST_COMMIT_ERROR",
|
|
51
|
+
"ON_POST_RESPONSE_ERROR",
|
|
52
|
+
"ON_ERROR",
|
|
53
|
+
}
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _is_pre_like(p: str) -> bool:
|
|
58
|
+
return p in _PRE_LIKE
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _is_post_or_error(p: str) -> bool:
|
|
62
|
+
return p in _POST_LIKE or p in _ERROR_LIKE
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# ───────────────────────────────────────────────────────────────────────────────
|
|
66
|
+
# ctx helpers
|
|
67
|
+
# ───────────────────────────────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _ctx_get(ctx: Mapping[str, Any], key: str, default: Any = None) -> Any:
|
|
71
|
+
try:
|
|
72
|
+
return ctx[key]
|
|
73
|
+
except Exception:
|
|
74
|
+
return getattr(ctx, key, default)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _ctx_db(ctx: Mapping[str, Any]) -> Any:
|
|
78
|
+
return _ctx_get(ctx, "db")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _ctx_payload(ctx: Mapping[str, Any]) -> Mapping[str, Any]:
|
|
82
|
+
v = _ctx_get(ctx, "payload", None)
|
|
83
|
+
return v if v is not None else {}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# ───────────────────────────────────────────────────────────────────────────────
|
|
87
|
+
# System step helpers
|
|
88
|
+
# ───────────────────────────────────────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _mark_skip_persist() -> StepFn:
|
|
92
|
+
async def _step(ctx: Any) -> None:
|
|
93
|
+
try:
|
|
94
|
+
ctx[CTX_SKIP_PERSIST_FLAG] = True
|
|
95
|
+
except Exception:
|
|
96
|
+
setattr(ctx, CTX_SKIP_PERSIST_FLAG, True)
|
|
97
|
+
|
|
98
|
+
_step.__name__ = "mark_skip_persist"
|
|
99
|
+
_step.__qualname__ = "mark_skip_persist"
|
|
100
|
+
return _step
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# ───────────────────────────────────────────────────────────────────────────────
|
|
104
|
+
# Step wrappers
|
|
105
|
+
# ───────────────────────────────────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _wrap_hook(h: HookSpec) -> StepFn:
|
|
109
|
+
fn = h.fn
|
|
110
|
+
pred = h.when
|
|
111
|
+
|
|
112
|
+
async def _step(ctx: Any) -> Any:
|
|
113
|
+
if pred is not None:
|
|
114
|
+
payload = _ctx_payload(ctx)
|
|
115
|
+
|
|
116
|
+
# Evaluate predicate without ever boolean-testing SQLAlchemy clauses.
|
|
117
|
+
def _as_bool(val: object) -> bool:
|
|
118
|
+
if isinstance(val, bool):
|
|
119
|
+
return val
|
|
120
|
+
try:
|
|
121
|
+
return bool(val)
|
|
122
|
+
except TypeError:
|
|
123
|
+
# e.g., SQLAlchemy ClauseElement: no boolean value → treat as pass
|
|
124
|
+
return True
|
|
125
|
+
|
|
126
|
+
try:
|
|
127
|
+
res = pred(payload)
|
|
128
|
+
except TypeError:
|
|
129
|
+
# Signature mismatch? Try with ctx.
|
|
130
|
+
try:
|
|
131
|
+
res = pred(ctx) # type: ignore[misc]
|
|
132
|
+
except Exception:
|
|
133
|
+
res = True
|
|
134
|
+
except Exception:
|
|
135
|
+
res = True
|
|
136
|
+
if not _as_bool(res):
|
|
137
|
+
return None
|
|
138
|
+
# Pass the context explicitly as a keyword so wrapped hooks expecting a
|
|
139
|
+
# ``ctx`` parameter receive the correct seed. Positional invocation
|
|
140
|
+
# treated the context as the first positional argument (often the
|
|
141
|
+
# ``value`` parameter), resulting in a new empty context being created
|
|
142
|
+
# inside the wrapper and missing executor-provided keys like
|
|
143
|
+
# ``response``.
|
|
144
|
+
rv = fn(ctx=ctx)
|
|
145
|
+
if inspect.isawaitable(rv):
|
|
146
|
+
return await rv
|
|
147
|
+
return rv
|
|
148
|
+
|
|
149
|
+
_step.__name__ = getattr(fn, "__name__", _step.__name__)
|
|
150
|
+
_step.__qualname__ = getattr(fn, "__qualname__", _step.__name__)
|
|
151
|
+
return _step
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _wrap_step_fn(fn: Callable[..., Any]) -> StepFn:
|
|
155
|
+
async def _step(ctx: Any) -> Any:
|
|
156
|
+
# Similar to :func:`_wrap_hook`, pass the context as a keyword argument to
|
|
157
|
+
# support wrappers produced by ``@hook_ctx`` which expect ``ctx`` as a
|
|
158
|
+
# kw-only parameter.
|
|
159
|
+
rv = fn(ctx=ctx)
|
|
160
|
+
if inspect.isawaitable(rv):
|
|
161
|
+
return await rv
|
|
162
|
+
return rv
|
|
163
|
+
|
|
164
|
+
_step.__name__ = getattr(fn, "__name__", _step.__name__)
|
|
165
|
+
_step.__qualname__ = getattr(fn, "__qualname__", _step.__name__)
|
|
166
|
+
return _step
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
# ───────────────────────────────────────────────────────────────────────────────
|
|
170
|
+
# Source collection (API / MODEL / OP) for a single alias
|
|
171
|
+
# Accepted shapes for API/MODEL sources:
|
|
172
|
+
# • { phase: Iterable[callable] } (applies to all aliases)
|
|
173
|
+
# • { alias: { phase: Iterable[callable] } } (per-alias)
|
|
174
|
+
# ───────────────────────────────────────────────────────────────────────────────
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _to_phase_map_for_alias(source: Any, alias: str) -> Dict[str, List[StepFn]]:
|
|
178
|
+
"""
|
|
179
|
+
Normalize a user-provided hooks source to { phase: [StepFn, ...], ... } for the given alias.
|
|
180
|
+
"""
|
|
181
|
+
out: Dict[str, List[StepFn]] = {}
|
|
182
|
+
if not source:
|
|
183
|
+
return out
|
|
184
|
+
|
|
185
|
+
maybe = None
|
|
186
|
+
if isinstance(source, Mapping):
|
|
187
|
+
if alias in source:
|
|
188
|
+
maybe = source.get(alias)
|
|
189
|
+
elif "*" in source:
|
|
190
|
+
maybe = source.get("*")
|
|
191
|
+
else:
|
|
192
|
+
maybe = source # flat {phase: iterable}
|
|
193
|
+
|
|
194
|
+
if isinstance(maybe, Mapping):
|
|
195
|
+
for ph, items in (maybe or {}).items():
|
|
196
|
+
steps = out.setdefault(str(ph), [])
|
|
197
|
+
if isinstance(items, Iterable):
|
|
198
|
+
for fn in items:
|
|
199
|
+
if callable(fn):
|
|
200
|
+
steps.append(_wrap_step_fn(fn))
|
|
201
|
+
return out
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
# ───────────────────────────────────────────────────────────────────────────────
|
|
205
|
+
# Precedence merge (API/MODEL/OP only; no imperative source)
|
|
206
|
+
# ───────────────────────────────────────────────────────────────────────────────
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _merge_for_phase(
|
|
210
|
+
phase: str,
|
|
211
|
+
*,
|
|
212
|
+
api_map: Mapping[str, List[StepFn]] | None,
|
|
213
|
+
model_map: Mapping[str, List[StepFn]] | None,
|
|
214
|
+
op_map: Mapping[str, List[StepFn]] | None,
|
|
215
|
+
) -> List[StepFn]:
|
|
216
|
+
"""
|
|
217
|
+
Merge lists from sources for one phase:
|
|
218
|
+
• pre-like → API + MODEL + OP
|
|
219
|
+
• post/error→ OP + MODEL + API
|
|
220
|
+
"""
|
|
221
|
+
|
|
222
|
+
def _get(m: Mapping[str, List[StepFn]] | None) -> List[StepFn]:
|
|
223
|
+
if not m:
|
|
224
|
+
return []
|
|
225
|
+
return list(m.get(phase, []) or [])
|
|
226
|
+
|
|
227
|
+
if _is_pre_like(phase):
|
|
228
|
+
return _get(api_map) + _get(model_map) + _get(op_map)
|
|
229
|
+
return _get(op_map) + _get(model_map) + _get(api_map)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
# ───────────────────────────────────────────────────────────────────────────────
|
|
233
|
+
# Alias namespace helper
|
|
234
|
+
# ───────────────────────────────────────────────────────────────────────────────
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _ensure_alias_hooks_ns(model: type, alias: str) -> SimpleNamespace:
|
|
238
|
+
root = getattr(model, "hooks", None)
|
|
239
|
+
if root is None:
|
|
240
|
+
root = SimpleNamespace()
|
|
241
|
+
setattr(model, "hooks", root)
|
|
242
|
+
ns = getattr(root, alias, None)
|
|
243
|
+
if ns is None:
|
|
244
|
+
ns = SimpleNamespace()
|
|
245
|
+
setattr(root, alias, ns)
|
|
246
|
+
# Ensure all known phases exist as lists
|
|
247
|
+
for ph in PHASES:
|
|
248
|
+
if not hasattr(ns, ph):
|
|
249
|
+
setattr(ns, ph, [])
|
|
250
|
+
return ns
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
# ───────────────────────────────────────────────────────────────────────────────
|
|
254
|
+
# Build / attach (with precedence)
|
|
255
|
+
# ───────────────────────────────────────────────────────────────────────────────
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _attach_one(model: type, sp: OpSpec) -> None:
|
|
259
|
+
alias = sp.alias
|
|
260
|
+
ns = _ensure_alias_hooks_ns(model, alias)
|
|
261
|
+
|
|
262
|
+
# Reset existing chains for a clean rebuild
|
|
263
|
+
for ph in PHASES:
|
|
264
|
+
setattr(ns, ph, [])
|
|
265
|
+
|
|
266
|
+
# Resolve source maps for this alias
|
|
267
|
+
api_src = getattr(model, TIGRBL_API_HOOKS_ATTR, None)
|
|
268
|
+
model_src = getattr(model, TIGRBL_HOOKS_ATTR, None)
|
|
269
|
+
|
|
270
|
+
api_map = _to_phase_map_for_alias(api_src, alias)
|
|
271
|
+
model_map = _to_phase_map_for_alias(model_src, alias)
|
|
272
|
+
|
|
273
|
+
# Op-level (from OpSpec.hooks)
|
|
274
|
+
op_map: Dict[str, List[StepFn]] = {}
|
|
275
|
+
for h in sp.hooks or ():
|
|
276
|
+
phase = str(h.phase)
|
|
277
|
+
op_map.setdefault(phase, []).append(_wrap_hook(h))
|
|
278
|
+
|
|
279
|
+
# Build per-phase chains via precedence merge
|
|
280
|
+
for ph in PHASES:
|
|
281
|
+
merged = _merge_for_phase(
|
|
282
|
+
ph, api_map=api_map, model_map=model_map, op_map=op_map
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
# Ephemeral operations: mark skip in PRE_TX_BEGIN
|
|
286
|
+
if sp.persist == "skip" and ph == "PRE_TX_BEGIN":
|
|
287
|
+
merged = [_mark_skip_persist()] + merged
|
|
288
|
+
|
|
289
|
+
setattr(ns, ph, merged)
|
|
290
|
+
|
|
291
|
+
logger.debug("hooks: %s.%s merged (persist=%s)", model.__name__, alias, sp.persist)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def normalize_and_attach(
|
|
295
|
+
model: type, specs: Sequence[OpSpec], *, only_keys: Optional[Sequence[_Key]] = None
|
|
296
|
+
) -> None:
|
|
297
|
+
"""
|
|
298
|
+
Build sequential phase chains for each OpSpec and attach them to model.hooks.<alias>.
|
|
299
|
+
Sources merged per phase (in precedence order):
|
|
300
|
+
• PRE-like: API → MODEL → OP
|
|
301
|
+
• POST/ERROR: OP → MODEL → API
|
|
302
|
+
"""
|
|
303
|
+
wanted = set(only_keys or ())
|
|
304
|
+
for sp in specs:
|
|
305
|
+
key = (sp.alias, sp.target)
|
|
306
|
+
if wanted and key not in wanted:
|
|
307
|
+
continue
|
|
308
|
+
_attach_one(model, sp)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
__all__ = ["normalize_and_attach"]
|