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,119 @@
|
|
|
1
|
+
# tigrbl/v3/bindings/handlers/builder.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
from typing import Optional, Sequence, Tuple
|
|
6
|
+
|
|
7
|
+
from ...op import OpSpec
|
|
8
|
+
from ...op.types import StepFn
|
|
9
|
+
from .namespaces import _ensure_alias_handlers_ns, _ensure_alias_hooks_ns
|
|
10
|
+
from .steps import _wrap_core, _wrap_custom
|
|
11
|
+
|
|
12
|
+
logging.getLogger("uvicorn").setLevel(logging.DEBUG)
|
|
13
|
+
logger = logging.getLogger("uvicorn")
|
|
14
|
+
logger.debug("Loaded module v3/bindings/handlers/builder")
|
|
15
|
+
|
|
16
|
+
_Key = Tuple[str, str]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _attach_one(model: type, sp: OpSpec) -> None:
|
|
20
|
+
alias = sp.alias
|
|
21
|
+
handlers_ns = _ensure_alias_handlers_ns(model, alias)
|
|
22
|
+
hooks_ns = _ensure_alias_hooks_ns(model, alias)
|
|
23
|
+
chain: list[StepFn] = getattr(hooks_ns, "HANDLER")
|
|
24
|
+
|
|
25
|
+
custom_step = _wrap_custom(model, sp, sp.handler) if sp.handler else None
|
|
26
|
+
core_step: Optional[StepFn] = (
|
|
27
|
+
None if sp.target == "custom" else _wrap_core(model, sp.target)
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
placement = sp.persist
|
|
31
|
+
raw_step: Optional[StepFn] = None
|
|
32
|
+
|
|
33
|
+
logger.debug(
|
|
34
|
+
"Attaching handler for %s.%s target=%s placement=%s",
|
|
35
|
+
model.__name__,
|
|
36
|
+
alias,
|
|
37
|
+
sp.target,
|
|
38
|
+
placement,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
if placement == "skip":
|
|
42
|
+
logger.debug("Placement 'skip' selected")
|
|
43
|
+
if custom_step is not None:
|
|
44
|
+
logger.debug("Appending custom step for alias %s", alias)
|
|
45
|
+
chain.append(custom_step)
|
|
46
|
+
raw_step = custom_step
|
|
47
|
+
elif sp.target == "custom":
|
|
48
|
+
logger.debug("Target 'custom' with alias %s", alias)
|
|
49
|
+
if custom_step is not None:
|
|
50
|
+
logger.debug("Appending custom step for alias %s", alias)
|
|
51
|
+
chain.append(custom_step)
|
|
52
|
+
raw_step = custom_step
|
|
53
|
+
elif custom_step is not None and core_step is not None:
|
|
54
|
+
logger.debug("Both custom and core steps present")
|
|
55
|
+
if placement == "append":
|
|
56
|
+
logger.debug("Appending core then custom step")
|
|
57
|
+
chain.extend([core_step, custom_step])
|
|
58
|
+
raw_step = core_step
|
|
59
|
+
elif placement == "override":
|
|
60
|
+
logger.debug("Overriding core step with custom step")
|
|
61
|
+
chain.append(custom_step)
|
|
62
|
+
raw_step = custom_step
|
|
63
|
+
core_step = None
|
|
64
|
+
else:
|
|
65
|
+
logger.debug("Prepending custom then core step")
|
|
66
|
+
chain.extend([custom_step, core_step])
|
|
67
|
+
raw_step = core_step
|
|
68
|
+
elif core_step is not None:
|
|
69
|
+
logger.debug("Only core step present; appending")
|
|
70
|
+
chain.append(core_step)
|
|
71
|
+
raw_step = core_step
|
|
72
|
+
elif custom_step is not None:
|
|
73
|
+
logger.debug("Only custom step present; appending")
|
|
74
|
+
chain.append(custom_step)
|
|
75
|
+
raw_step = custom_step
|
|
76
|
+
|
|
77
|
+
if raw_step is None:
|
|
78
|
+
logger.debug("No raw step produced; exiting")
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
setattr(handlers_ns, "raw", raw_step)
|
|
82
|
+
setattr(handlers_ns, "handler", raw_step)
|
|
83
|
+
|
|
84
|
+
if core_step is not None:
|
|
85
|
+
object.__setattr__(sp, "core", core_step)
|
|
86
|
+
object.__setattr__(sp, "core_raw", core_step)
|
|
87
|
+
setattr(handlers_ns, "core", core_step)
|
|
88
|
+
setattr(handlers_ns, "core_raw", core_step)
|
|
89
|
+
logger.debug("Core step registered for %s.%s", model.__name__, alias)
|
|
90
|
+
else:
|
|
91
|
+
object.__setattr__(sp, "core", raw_step)
|
|
92
|
+
object.__setattr__(sp, "core_raw", raw_step)
|
|
93
|
+
setattr(handlers_ns, "core", raw_step)
|
|
94
|
+
setattr(handlers_ns, "core_raw", raw_step)
|
|
95
|
+
logger.debug("Raw step registered as core for %s.%s", model.__name__, alias)
|
|
96
|
+
|
|
97
|
+
logger.debug(
|
|
98
|
+
"handlers: %s.%s → handler chain updated (persist=%s)",
|
|
99
|
+
model.__name__,
|
|
100
|
+
alias,
|
|
101
|
+
sp.persist,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def build_and_attach(
|
|
106
|
+
model: type, specs: Sequence[OpSpec], *, only_keys: Optional[Sequence[_Key]] = None
|
|
107
|
+
) -> None:
|
|
108
|
+
wanted = set(only_keys or ())
|
|
109
|
+
logger.debug("Building handlers for %s (only_keys=%s)", model.__name__, wanted)
|
|
110
|
+
for sp in specs:
|
|
111
|
+
key = (sp.alias, sp.target)
|
|
112
|
+
if wanted and key not in wanted:
|
|
113
|
+
logger.debug("Skipping spec %s due to only_keys filter", key)
|
|
114
|
+
continue
|
|
115
|
+
logger.debug("Processing spec %s", key)
|
|
116
|
+
_attach_one(model, sp)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
__all__ = ["build_and_attach"]
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# tigrbl/v3/bindings/handlers/ctx.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from typing import Any, Mapping, Sequence
|
|
6
|
+
|
|
7
|
+
logging.getLogger("uvicorn").setLevel(logging.DEBUG)
|
|
8
|
+
logger = logging.getLogger("uvicorn")
|
|
9
|
+
logger.debug("Loaded module v3/bindings/handlers/ctx")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _ctx_get(ctx: Mapping[str, Any], key: str, default: Any = None) -> Any:
|
|
13
|
+
logger.debug("_ctx_get retrieving key '%s'", key)
|
|
14
|
+
try:
|
|
15
|
+
value = ctx[key]
|
|
16
|
+
logger.debug("Key '%s' found via mapping access", key)
|
|
17
|
+
return value
|
|
18
|
+
except Exception:
|
|
19
|
+
logger.debug("Key '%s' not found; using getattr fallback", key)
|
|
20
|
+
return getattr(ctx, key, default)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _ctx_payload(ctx: Mapping[str, Any]) -> Any:
|
|
24
|
+
temp = _ctx_get(ctx, "temp", None)
|
|
25
|
+
raw = _ctx_get(ctx, "payload", None)
|
|
26
|
+
if isinstance(temp, Mapping):
|
|
27
|
+
av = temp.get("assembled_values")
|
|
28
|
+
if isinstance(av, Mapping) and isinstance(raw, Mapping):
|
|
29
|
+
merged = dict(raw)
|
|
30
|
+
merged.update(av)
|
|
31
|
+
logger.debug("Payload from assembled values: %s", merged)
|
|
32
|
+
return merged
|
|
33
|
+
|
|
34
|
+
v = raw
|
|
35
|
+
if isinstance(v, Mapping):
|
|
36
|
+
logger.debug("Payload is a mapping")
|
|
37
|
+
logger.debug("Payload: %s", v)
|
|
38
|
+
return v
|
|
39
|
+
if isinstance(v, Sequence) and not isinstance(v, (str, bytes)):
|
|
40
|
+
logger.debug("Payload is a non-string sequence")
|
|
41
|
+
logger.debug("Payload: %s", v)
|
|
42
|
+
return v
|
|
43
|
+
logger.debug("Payload absent or unsupported; defaulting to empty dict")
|
|
44
|
+
v = {}
|
|
45
|
+
logger.debug("Payload: %s", v)
|
|
46
|
+
return v
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _ctx_db(ctx: Mapping[str, Any]) -> Any:
|
|
50
|
+
logger.debug("Retrieving 'db' from context")
|
|
51
|
+
return _ctx_get(ctx, "db")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _ctx_request(ctx: Mapping[str, Any]) -> Any:
|
|
55
|
+
logger.debug("Retrieving 'request' from context")
|
|
56
|
+
return _ctx_get(ctx, "request")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _ctx_path_params(ctx: Mapping[str, Any]) -> Mapping[str, Any]:
|
|
60
|
+
v = _ctx_get(ctx, "path_params", None)
|
|
61
|
+
if isinstance(v, Mapping):
|
|
62
|
+
logger.debug("Path params found: %s", list(v.keys()))
|
|
63
|
+
return v
|
|
64
|
+
logger.debug("No path params found; returning empty mapping")
|
|
65
|
+
return {}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
__all__ = [
|
|
69
|
+
"_ctx_get",
|
|
70
|
+
"_ctx_payload",
|
|
71
|
+
"_ctx_db",
|
|
72
|
+
"_ctx_request",
|
|
73
|
+
"_ctx_path_params",
|
|
74
|
+
]
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# tigrbl/v3/bindings/handlers/identifiers.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
import uuid
|
|
6
|
+
from typing import Any, Mapping, Optional
|
|
7
|
+
|
|
8
|
+
from .ctx import _ctx_payload, _ctx_path_params
|
|
9
|
+
|
|
10
|
+
logging.getLogger("uvicorn").setLevel(logging.DEBUG)
|
|
11
|
+
logger = logging.getLogger("uvicorn")
|
|
12
|
+
logger.debug("Loaded module v3/bindings/handlers/identifiers")
|
|
13
|
+
|
|
14
|
+
try: # pragma: no cover
|
|
15
|
+
from sqlalchemy.inspection import inspect as _sa_inspect # type: ignore
|
|
16
|
+
except Exception: # pragma: no cover
|
|
17
|
+
_sa_inspect = None # type: ignore
|
|
18
|
+
try: # pragma: no cover
|
|
19
|
+
from sqlalchemy.sql import ClauseElement as SAClause # type: ignore
|
|
20
|
+
except Exception: # pragma: no cover
|
|
21
|
+
SAClause = None # type: ignore
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _pk_name(model: type) -> str:
|
|
25
|
+
"""Best-effort primary-key column name."""
|
|
26
|
+
logger.debug("Resolving PK name for %s", model.__name__)
|
|
27
|
+
if _sa_inspect is not None:
|
|
28
|
+
try:
|
|
29
|
+
mapper = _sa_inspect(model)
|
|
30
|
+
pk_cols = list(getattr(mapper, "primary_key", []) or [])
|
|
31
|
+
logger.debug("SQLAlchemy mapper found %d PK cols", len(pk_cols))
|
|
32
|
+
if len(pk_cols) == 1:
|
|
33
|
+
col = pk_cols[0]
|
|
34
|
+
name = getattr(col, "key", None) or getattr(col, "name", None)
|
|
35
|
+
logger.debug("PK name via mapper: %s", name)
|
|
36
|
+
if isinstance(name, str) and name:
|
|
37
|
+
return name
|
|
38
|
+
except Exception as exc:
|
|
39
|
+
logger.debug("SQLAlchemy inspection failed: %s", exc)
|
|
40
|
+
|
|
41
|
+
table = getattr(model, "__table__", None)
|
|
42
|
+
if table is not None:
|
|
43
|
+
try:
|
|
44
|
+
pk = getattr(table, "primary_key", None)
|
|
45
|
+
cols_iter = getattr(pk, "columns", None)
|
|
46
|
+
cols = [c for c in cols_iter] if cols_iter is not None else []
|
|
47
|
+
logger.debug("Table inspection found %d PK cols", len(cols))
|
|
48
|
+
if len(cols) == 1:
|
|
49
|
+
col = cols[0]
|
|
50
|
+
name = getattr(col, "key", None) or getattr(col, "name", None)
|
|
51
|
+
logger.debug("PK name via table: %s", name)
|
|
52
|
+
if isinstance(name, str) and name:
|
|
53
|
+
return name
|
|
54
|
+
except Exception as exc:
|
|
55
|
+
logger.debug("Table inspection failed: %s", exc)
|
|
56
|
+
|
|
57
|
+
logger.debug("Falling back to default PK name 'id'")
|
|
58
|
+
return "id"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _pk_type_info(model: type) -> tuple[Optional[type], Optional[Any]]:
|
|
62
|
+
"""Return (python_type, sqlatype_instance) for the PK column if discoverable."""
|
|
63
|
+
logger.debug("Resolving PK type info for %s", model.__name__)
|
|
64
|
+
col = None
|
|
65
|
+
if _sa_inspect is not None:
|
|
66
|
+
try:
|
|
67
|
+
mapper = _sa_inspect(model)
|
|
68
|
+
pk_cols = list(getattr(mapper, "primary_key", []) or [])
|
|
69
|
+
logger.debug("Mapper returned %d PK cols", len(pk_cols))
|
|
70
|
+
if len(pk_cols) == 1:
|
|
71
|
+
col = pk_cols[0]
|
|
72
|
+
except Exception as exc:
|
|
73
|
+
logger.debug("Mapper inspection failed: %s", exc)
|
|
74
|
+
col = None
|
|
75
|
+
|
|
76
|
+
if col is None:
|
|
77
|
+
table = getattr(model, "__table__", None)
|
|
78
|
+
if table is not None:
|
|
79
|
+
try:
|
|
80
|
+
pk = getattr(table, "primary_key", None)
|
|
81
|
+
cols_iter = getattr(pk, "columns", None)
|
|
82
|
+
cols = [c for c in cols_iter] if cols_iter is not None else []
|
|
83
|
+
logger.debug("Table inspection yielded %d PK cols", len(cols))
|
|
84
|
+
if len(cols) == 1:
|
|
85
|
+
col = cols[0]
|
|
86
|
+
except Exception as exc:
|
|
87
|
+
logger.debug("Table inspection failed: %s", exc)
|
|
88
|
+
col = None
|
|
89
|
+
|
|
90
|
+
if col is None:
|
|
91
|
+
logger.debug("PK column not found; returning (None, None)")
|
|
92
|
+
return (None, None)
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
coltype = getattr(col, "type", None)
|
|
96
|
+
except Exception as exc:
|
|
97
|
+
logger.debug("Failed to get column type: %s", exc)
|
|
98
|
+
coltype = None
|
|
99
|
+
|
|
100
|
+
py_t = None
|
|
101
|
+
try:
|
|
102
|
+
py_t = getattr(coltype, "python_type", None)
|
|
103
|
+
except Exception as exc:
|
|
104
|
+
logger.debug("Failed to get python_type: %s", exc)
|
|
105
|
+
py_t = None
|
|
106
|
+
|
|
107
|
+
logger.debug("Resolved PK type info py_t=%s sa_type=%s", py_t, coltype)
|
|
108
|
+
return (py_t, coltype)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _looks_like_uuid_string(s: str) -> bool:
|
|
112
|
+
if not isinstance(s, str):
|
|
113
|
+
logger.debug("Value %r is not a string; cannot be UUID", s)
|
|
114
|
+
return False
|
|
115
|
+
try:
|
|
116
|
+
uuid.UUID(s)
|
|
117
|
+
logger.debug("Value %s looks like a UUID string", s)
|
|
118
|
+
return True
|
|
119
|
+
except Exception:
|
|
120
|
+
logger.debug("Value %s is not a valid UUID string", s)
|
|
121
|
+
return False
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _is_uuid_type(py_t: Optional[type], sa_type: Optional[Any]) -> bool:
|
|
125
|
+
if py_t is uuid.UUID:
|
|
126
|
+
logger.debug("PK python type is UUID")
|
|
127
|
+
return True
|
|
128
|
+
try:
|
|
129
|
+
if getattr(sa_type, "as_uuid", False):
|
|
130
|
+
logger.debug("SQLAlchemy type %r uses as_uuid", sa_type)
|
|
131
|
+
return True
|
|
132
|
+
except Exception:
|
|
133
|
+
pass
|
|
134
|
+
try:
|
|
135
|
+
tname = type(sa_type).__name__.lower() if sa_type is not None else ""
|
|
136
|
+
if "uuid" in tname:
|
|
137
|
+
logger.debug("SQLAlchemy type name contains 'uuid': %s", tname)
|
|
138
|
+
return True
|
|
139
|
+
except Exception:
|
|
140
|
+
pass
|
|
141
|
+
return False
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _coerce_ident_to_pk_type(model: type, value: Any) -> Any:
|
|
145
|
+
py_t, sa_t = _pk_type_info(model)
|
|
146
|
+
logger.debug("Coercing identifier %r to PK type %s", value, py_t)
|
|
147
|
+
if SAClause is not None and isinstance(value, SAClause): # pragma: no cover
|
|
148
|
+
logger.debug("Value is SAClause; returning as-is")
|
|
149
|
+
return value
|
|
150
|
+
if _is_uuid_type(py_t, sa_t):
|
|
151
|
+
logger.debug("PK type identified as UUID")
|
|
152
|
+
if isinstance(value, uuid.UUID):
|
|
153
|
+
return value
|
|
154
|
+
if isinstance(value, str):
|
|
155
|
+
return uuid.UUID(value)
|
|
156
|
+
if isinstance(value, (bytes, bytearray)) and len(value) == 16:
|
|
157
|
+
return uuid.UUID(bytes=bytes(value))
|
|
158
|
+
if _looks_like_uuid_string(str(value)):
|
|
159
|
+
return uuid.UUID(str(value))
|
|
160
|
+
return value
|
|
161
|
+
if py_t is int:
|
|
162
|
+
logger.debug("PK type identified as int")
|
|
163
|
+
if isinstance(value, int):
|
|
164
|
+
return value
|
|
165
|
+
if isinstance(value, str):
|
|
166
|
+
return int(value)
|
|
167
|
+
try:
|
|
168
|
+
return int(value) # type: ignore[arg-type]
|
|
169
|
+
except Exception as exc:
|
|
170
|
+
logger.debug("Failed int coercion: %s", exc)
|
|
171
|
+
return value
|
|
172
|
+
return value
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _is_clause(x: Any) -> bool:
|
|
176
|
+
return SAClause is not None and isinstance(x, SAClause) # type: ignore[truthy-bool]
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _resolve_ident(model: type, ctx: Mapping[str, Any]) -> Any:
|
|
180
|
+
payload = _ctx_payload(ctx)
|
|
181
|
+
path = _ctx_path_params(ctx)
|
|
182
|
+
pk = _pk_name(model)
|
|
183
|
+
logger.debug("Resolving identifier for %s using pk %s", model.__name__, pk)
|
|
184
|
+
|
|
185
|
+
candidates_keys = [
|
|
186
|
+
(path, pk),
|
|
187
|
+
(payload, pk),
|
|
188
|
+
(path, "id"),
|
|
189
|
+
(payload, "id"),
|
|
190
|
+
(path, "item_id"),
|
|
191
|
+
(payload, "item_id"),
|
|
192
|
+
]
|
|
193
|
+
if pk != "id":
|
|
194
|
+
candidates_keys.extend(
|
|
195
|
+
[
|
|
196
|
+
(path, f"{pk}_id"),
|
|
197
|
+
(payload, f"{pk}_id"),
|
|
198
|
+
]
|
|
199
|
+
)
|
|
200
|
+
candidates_keys.append((payload, "ident"))
|
|
201
|
+
|
|
202
|
+
for source, key in candidates_keys:
|
|
203
|
+
try:
|
|
204
|
+
v = source.get(key) # type: ignore[call-arg]
|
|
205
|
+
except Exception:
|
|
206
|
+
v = None
|
|
207
|
+
logger.debug("Checking candidate key '%s' → %r", key, v)
|
|
208
|
+
if v is None:
|
|
209
|
+
continue
|
|
210
|
+
if _is_clause(v):
|
|
211
|
+
logger.debug("Value for key '%s' is a clause; skipping", key)
|
|
212
|
+
continue
|
|
213
|
+
try:
|
|
214
|
+
return _coerce_ident_to_pk_type(model, v)
|
|
215
|
+
except Exception:
|
|
216
|
+
logger.debug("Invalid identifier %r for pk %s", v, pk)
|
|
217
|
+
raise TypeError(f"Invalid identifier for '{pk}': {v!r}")
|
|
218
|
+
|
|
219
|
+
logger.debug("Identifier for pk %s not found", pk)
|
|
220
|
+
raise TypeError(f"Missing identifier '{pk}' in path or payload")
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
__all__ = [
|
|
224
|
+
"_pk_name",
|
|
225
|
+
"_pk_type_info",
|
|
226
|
+
"_resolve_ident",
|
|
227
|
+
"_coerce_ident_to_pk_type",
|
|
228
|
+
]
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# tigrbl/v3/bindings/handlers/namespaces.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from types import SimpleNamespace
|
|
6
|
+
|
|
7
|
+
logging.getLogger("uvicorn").setLevel(logging.DEBUG)
|
|
8
|
+
logger = logging.getLogger("uvicorn")
|
|
9
|
+
logger.debug("Loaded module v3/bindings/handlers/namespaces")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _ensure_alias_hooks_ns(model: type, alias: str) -> SimpleNamespace:
|
|
13
|
+
hooks_root = getattr(model, "hooks", None)
|
|
14
|
+
if hooks_root is None:
|
|
15
|
+
logger.debug("Creating hooks namespace for %s", model.__name__)
|
|
16
|
+
hooks_root = SimpleNamespace()
|
|
17
|
+
setattr(model, "hooks", hooks_root)
|
|
18
|
+
|
|
19
|
+
ns = getattr(hooks_root, alias, None)
|
|
20
|
+
if ns is None:
|
|
21
|
+
logger.debug("Creating hooks alias namespace '%s'", alias)
|
|
22
|
+
ns = SimpleNamespace()
|
|
23
|
+
setattr(hooks_root, alias, ns)
|
|
24
|
+
|
|
25
|
+
if not hasattr(ns, "HANDLER"):
|
|
26
|
+
logger.debug("Initializing HANDLER list for hooks '%s'", alias)
|
|
27
|
+
setattr(ns, "HANDLER", [])
|
|
28
|
+
return ns
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _ensure_alias_handlers_ns(model: type, alias: str) -> SimpleNamespace:
|
|
32
|
+
handlers_root = getattr(model, "handlers", None)
|
|
33
|
+
if handlers_root is None:
|
|
34
|
+
logger.debug("Creating handlers namespace for %s", model.__name__)
|
|
35
|
+
handlers_root = SimpleNamespace()
|
|
36
|
+
setattr(model, "handlers", handlers_root)
|
|
37
|
+
|
|
38
|
+
ns = getattr(handlers_root, alias, None)
|
|
39
|
+
if ns is None:
|
|
40
|
+
logger.debug("Creating handlers alias namespace '%s'", alias)
|
|
41
|
+
ns = SimpleNamespace()
|
|
42
|
+
setattr(handlers_root, alias, ns)
|
|
43
|
+
|
|
44
|
+
hooks_ns = _ensure_alias_hooks_ns(model, alias)
|
|
45
|
+
if not hasattr(ns, "HANDLER"):
|
|
46
|
+
logger.debug("Linking HANDLER list from hooks namespace for '%s'", alias)
|
|
47
|
+
setattr(ns, "HANDLER", getattr(hooks_ns, "HANDLER"))
|
|
48
|
+
return ns
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
__all__ = ["_ensure_alias_hooks_ns", "_ensure_alias_handlers_ns"]
|