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,130 @@
|
|
|
1
|
+
# tigrbl/v3/runtime/atoms/refresh/demand.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Any, Iterable, Optional
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
from ... import events as _ev
|
|
8
|
+
from ...opview import opview_from_ctx, _ensure_temp
|
|
9
|
+
|
|
10
|
+
# After the handler flushes changes; decide whether to hydrate DB-generated values.
|
|
11
|
+
ANCHOR = _ev.POST_FLUSH # "post:flush"
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger("uvicorn")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def run(obj: Optional[object], ctx: Any) -> None:
|
|
17
|
+
"""
|
|
18
|
+
refresh:demand@post:flush
|
|
19
|
+
|
|
20
|
+
Purpose
|
|
21
|
+
-------
|
|
22
|
+
Decide whether to hydrate refreshed values from the DB after a write (INSERT/UPDATE).
|
|
23
|
+
We do NOT perform the refresh here; we only mark intent on the context so the
|
|
24
|
+
executor (or handler) can perform the vendor-specific action (RETURNING/refresh()).
|
|
25
|
+
|
|
26
|
+
Inputs (conventions)
|
|
27
|
+
--------------------
|
|
28
|
+
- ctx.persist : bool → write op? (anchor is persist-tied but we guard)
|
|
29
|
+
- ctx.temp["used_returning"] : bool → a prior step already satisfied hydration
|
|
30
|
+
- ctx.temp["hydrated_values"] : Mapping[str, Any]→ values captured from RETURNING
|
|
31
|
+
- ctx.cfg.refresh_after_write : Optional[bool] → policy override (true/false)
|
|
32
|
+
(also checks ctx.cfg.refresh_policy.{always,never,auto})
|
|
33
|
+
- ctx.bulk : Optional[bool] → bulk write; executor may choose a different strategy
|
|
34
|
+
|
|
35
|
+
Effects
|
|
36
|
+
-------
|
|
37
|
+
- ctx.temp["refresh_demand"] : bool
|
|
38
|
+
- ctx.temp["refresh_fields"] : tuple[str, ...] (hint: which fields likely changed in DB)
|
|
39
|
+
- ctx.temp["refresh_reason"] : str (diagnostic only)
|
|
40
|
+
"""
|
|
41
|
+
logger.debug("Running refresh:demand")
|
|
42
|
+
if getattr(ctx, "persist", True) is False:
|
|
43
|
+
logger.debug("Skipping refresh:demand; ctx.persist is False")
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
temp = _ensure_temp(ctx)
|
|
47
|
+
ov = opview_from_ctx(ctx)
|
|
48
|
+
refresh_hints = tuple(ov.refresh_hints)
|
|
49
|
+
|
|
50
|
+
# If RETURNING already produced hydrated values, skip unless policy forces refresh.
|
|
51
|
+
returning_satisfied = bool(temp.get("used_returning")) or bool(
|
|
52
|
+
temp.get("hydrated_values")
|
|
53
|
+
)
|
|
54
|
+
logger.debug("Returning satisfied: %s", returning_satisfied)
|
|
55
|
+
|
|
56
|
+
# Policy: cfg.refresh_after_write wins if explicitly set; otherwise "auto".
|
|
57
|
+
policy = _get_refresh_policy(ctx)
|
|
58
|
+
logger.debug("Refresh policy: %s", policy)
|
|
59
|
+
# auto → infer from specs (db-generated signals) OR absence of returning values
|
|
60
|
+
needs_by_specs = bool(refresh_hints)
|
|
61
|
+
logger.debug("Refresh hints: %s; fields=%s", needs_by_specs, refresh_hints)
|
|
62
|
+
need_refresh = _decide(policy, returning_satisfied, needs_by_specs)
|
|
63
|
+
logger.debug("Refresh decision: %s", need_refresh)
|
|
64
|
+
|
|
65
|
+
temp["refresh_demand"] = bool(need_refresh)
|
|
66
|
+
temp["refresh_fields"] = refresh_hints
|
|
67
|
+
|
|
68
|
+
if need_refresh:
|
|
69
|
+
temp["refresh_reason"] = _reason(
|
|
70
|
+
policy, returning_satisfied, needs_by_specs, refresh_hints
|
|
71
|
+
)
|
|
72
|
+
logger.debug("Refresh scheduled: %s", temp["refresh_reason"])
|
|
73
|
+
else:
|
|
74
|
+
temp["refresh_reason"] = "skipped: returning_satisfied or policy=false"
|
|
75
|
+
logger.debug("Refresh skipped: %s", temp["refresh_reason"])
|
|
76
|
+
|
|
77
|
+
# Executor/handler will look at ctx.temp["refresh_demand"] and act accordingly.
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
81
|
+
# Internals
|
|
82
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _get_refresh_policy(ctx: Any) -> str:
|
|
86
|
+
"""
|
|
87
|
+
Return 'always' | 'never' | 'auto'.
|
|
88
|
+
Accepts any of the following on ctx.cfg (first hit wins):
|
|
89
|
+
- cfg.refresh_after_write: bool → True:'always' / False:'never'
|
|
90
|
+
- cfg.refresh_policy: str in {'always','never','auto'}
|
|
91
|
+
Defaults to 'auto'.
|
|
92
|
+
"""
|
|
93
|
+
cfg = getattr(ctx, "cfg", None)
|
|
94
|
+
if cfg is not None:
|
|
95
|
+
val = getattr(cfg, "refresh_after_write", None)
|
|
96
|
+
if isinstance(val, bool):
|
|
97
|
+
return "always" if val else "never"
|
|
98
|
+
pol = getattr(getattr(cfg, "refresh_policy", None), "value", None) or getattr(
|
|
99
|
+
cfg, "refresh_policy", None
|
|
100
|
+
)
|
|
101
|
+
if isinstance(pol, str) and pol in {"always", "never", "auto"}:
|
|
102
|
+
return pol
|
|
103
|
+
return "auto"
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _decide(policy: str, returning_satisfied: bool, needs_by_specs: bool) -> bool:
|
|
107
|
+
if policy == "always":
|
|
108
|
+
return True
|
|
109
|
+
if policy == "never":
|
|
110
|
+
return False
|
|
111
|
+
# auto
|
|
112
|
+
if returning_satisfied:
|
|
113
|
+
# RETURNING already hydrated values; only refresh if specs strongly indicate more work.
|
|
114
|
+
return bool(needs_by_specs)
|
|
115
|
+
# No returning: default to refresh to honor "hydrate after flush" decision.
|
|
116
|
+
return True
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _reason(
|
|
120
|
+
policy: str, returning_satisfied: bool, needs_by_specs: bool, fields: Iterable[str]
|
|
121
|
+
) -> str:
|
|
122
|
+
parts = [f"policy={policy}"]
|
|
123
|
+
parts.append(f"returning_satisfied={bool(returning_satisfied)}")
|
|
124
|
+
parts.append(f"specs_need={bool(needs_by_specs)}")
|
|
125
|
+
if fields:
|
|
126
|
+
parts.append(f"fields={','.join(fields)}")
|
|
127
|
+
return "; ".join(parts)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
__all__ = ["ANCHOR", "run"]
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# tigrbl/v3/runtime/atoms/resolve/__init__.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Any, Callable, Dict, Optional, Tuple
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
# Atom implementations (model-scoped)
|
|
8
|
+
from . import assemble as _assemble
|
|
9
|
+
from . import paired_gen as _paired_gen
|
|
10
|
+
|
|
11
|
+
# Runner signature: (obj|None, ctx) -> None
|
|
12
|
+
RunFn = Callable[[Optional[object], Any], None]
|
|
13
|
+
|
|
14
|
+
#: Domain-scoped registry consumed by the kernel plan (and aggregated at atoms/__init__.py).
|
|
15
|
+
#: Keys are (domain, subject); values are (anchor, runner).
|
|
16
|
+
REGISTRY: Dict[Tuple[str, str], Tuple[str, RunFn]] = {
|
|
17
|
+
("resolve", "assemble"): (_assemble.ANCHOR, _assemble.run),
|
|
18
|
+
("resolve", "paired_gen"): (_paired_gen.ANCHOR, _paired_gen.run),
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger("uvicorn")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def subjects() -> Tuple[str, ...]:
|
|
25
|
+
"""Return the subject names exported by this domain."""
|
|
26
|
+
subjects = tuple(s for (_, s) in REGISTRY.keys())
|
|
27
|
+
logger.debug("Listing 'resolve' subjects: %s", subjects)
|
|
28
|
+
return subjects
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get(subject: str) -> Tuple[str, RunFn]:
|
|
32
|
+
"""Return (anchor, runner) for a subject in the 'resolve' domain."""
|
|
33
|
+
key = ("resolve", subject)
|
|
34
|
+
if key not in REGISTRY:
|
|
35
|
+
raise KeyError(f"Unknown resolve atom subject: {subject!r}")
|
|
36
|
+
logger.debug("Retrieving 'resolve' subject %s", subject)
|
|
37
|
+
return REGISTRY[key]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
__all__ = ["REGISTRY", "RunFn", "subjects", "get"]
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# tigrbl/v3/runtime/atoms/resolve/assemble.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Any, Mapping, Optional, Dict, Tuple
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
from ... import events as _ev
|
|
8
|
+
from ...opview import opview_from_ctx, ensure_schema_in, _ensure_temp
|
|
9
|
+
|
|
10
|
+
# Runs in HANDLER phase, before pre:flush and any storage transforms.
|
|
11
|
+
ANCHOR = _ev.RESOLVE_VALUES # "resolve:values"
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger("uvicorn")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def run(obj: Optional[object], ctx: Any) -> None:
|
|
17
|
+
"""
|
|
18
|
+
resolve:assemble@resolve:values
|
|
19
|
+
|
|
20
|
+
Purpose
|
|
21
|
+
-------
|
|
22
|
+
Build a normalized dict of inbound values to apply on the model (assembled_values).
|
|
23
|
+
- Prefer client-provided, validated values (from wire:build_in/validate).
|
|
24
|
+
- For fields that are **ABSENT** (not present in inbound), apply ColumnSpec.default_factory(ctx)
|
|
25
|
+
for simple server-side defaults (non-paired path).
|
|
26
|
+
- Virtual columns (storage=None) are never persisted; if present in inbound, stash them
|
|
27
|
+
under temp["virtual_in"] for downstream wire/out logic.
|
|
28
|
+
|
|
29
|
+
Inputs (conventions)
|
|
30
|
+
--------------------
|
|
31
|
+
- ctx.temp["in_values"] OR ctx.in_data / ctx.payload / ctx.data / ctx.body :
|
|
32
|
+
dict-like or Pydantic model; used as inbound source
|
|
33
|
+
- ctx.persist : bool (writes only; non-persist ops typically prune this anchor)
|
|
34
|
+
- ctx.op : str (verb name; optional, only for diagnostics)
|
|
35
|
+
|
|
36
|
+
Effects
|
|
37
|
+
-------
|
|
38
|
+
- ctx.temp["assembled_values"] : dict[field -> value] (only persisted fields)
|
|
39
|
+
- ctx.temp["virtual_in"] : dict[field -> value] (for storage=None)
|
|
40
|
+
- ctx.temp["absent_fields"] : tuple[str, ...] (those not in inbound)
|
|
41
|
+
- ctx.temp["used_default_factory"] : tuple[str, ...] (fields defaulted here)
|
|
42
|
+
"""
|
|
43
|
+
# Non-persisting ops should have pruned this anchor; retain guard for safety.
|
|
44
|
+
if getattr(ctx, "persist", True) is False:
|
|
45
|
+
logger.debug("Skipping resolve:assemble; ctx.persist is False")
|
|
46
|
+
return
|
|
47
|
+
|
|
48
|
+
logger.debug("Running resolve:assemble")
|
|
49
|
+
ov = opview_from_ctx(ctx)
|
|
50
|
+
schema_in = ensure_schema_in(ctx, ov)
|
|
51
|
+
inbound = _coerce_inbound(getattr(ctx, "temp", {}).get("in_values", None), ctx)
|
|
52
|
+
|
|
53
|
+
temp = _ensure_temp(ctx)
|
|
54
|
+
assembled: Dict[str, Any] = {}
|
|
55
|
+
virtual_in: Dict[str, Any] = {}
|
|
56
|
+
absent: list[str] = []
|
|
57
|
+
used_default: list[str] = []
|
|
58
|
+
|
|
59
|
+
# Iterate fields in a stable order
|
|
60
|
+
for field in sorted(schema_in["fields"]):
|
|
61
|
+
meta = schema_in["by_field"].get(field, {})
|
|
62
|
+
in_enabled = meta.get("in_enabled", True)
|
|
63
|
+
is_virtual = meta.get("virtual", False)
|
|
64
|
+
|
|
65
|
+
present, value = _try_read_inbound(inbound, field)
|
|
66
|
+
if present:
|
|
67
|
+
if is_virtual:
|
|
68
|
+
virtual_in[field] = value
|
|
69
|
+
logger.debug("Captured virtual inbound %s=%s", field, value)
|
|
70
|
+
elif in_enabled:
|
|
71
|
+
assembled[field] = value
|
|
72
|
+
logger.debug("Assembled inbound %s=%s", field, value)
|
|
73
|
+
continue
|
|
74
|
+
|
|
75
|
+
absent.append(field)
|
|
76
|
+
logger.debug("Field %s absent from inbound", field)
|
|
77
|
+
|
|
78
|
+
default_fn = meta.get("default_factory")
|
|
79
|
+
if callable(default_fn) and in_enabled and not is_virtual:
|
|
80
|
+
try:
|
|
81
|
+
default_val = default_fn(_ctx_view(ctx))
|
|
82
|
+
assembled[field] = default_val
|
|
83
|
+
used_default.append(field)
|
|
84
|
+
logger.debug("Applied default for field %s", field)
|
|
85
|
+
except Exception:
|
|
86
|
+
logger.debug("Default factory failed for field %s", field)
|
|
87
|
+
|
|
88
|
+
# Stash results on ctx.temp
|
|
89
|
+
temp["assembled_values"] = assembled
|
|
90
|
+
temp["virtual_in"] = virtual_in
|
|
91
|
+
temp["absent_fields"] = tuple(absent)
|
|
92
|
+
temp["used_default_factory"] = tuple(used_default)
|
|
93
|
+
logger.debug(
|
|
94
|
+
"Assembled values: %s, virtual_in: %s, absent: %s, defaults: %s",
|
|
95
|
+
assembled,
|
|
96
|
+
virtual_in,
|
|
97
|
+
absent,
|
|
98
|
+
used_default,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
103
|
+
# Internals
|
|
104
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _coerce_inbound(candidate: Any, ctx: Any) -> Mapping[str, Any]:
|
|
108
|
+
"""
|
|
109
|
+
Return a dict-like for inbound values.
|
|
110
|
+
Priority: ctx.temp["in_values"] → ctx.in_data → ctx.payload → ctx.data → ctx.body.
|
|
111
|
+
Accepts Pydantic v1/v2 models (model_dump()/dict()).
|
|
112
|
+
"""
|
|
113
|
+
for name in ("in_values",):
|
|
114
|
+
if isinstance(candidate, Mapping):
|
|
115
|
+
return candidate
|
|
116
|
+
# fallbacks on ctx
|
|
117
|
+
for attr in ("in_data", "payload", "data", "body"):
|
|
118
|
+
val = getattr(ctx, attr, None)
|
|
119
|
+
if isinstance(val, Mapping):
|
|
120
|
+
return val
|
|
121
|
+
# Pydantic v2
|
|
122
|
+
if hasattr(val, "model_dump") and callable(getattr(val, "model_dump")):
|
|
123
|
+
try:
|
|
124
|
+
return dict(val.model_dump())
|
|
125
|
+
except Exception:
|
|
126
|
+
pass
|
|
127
|
+
# Pydantic v1
|
|
128
|
+
if hasattr(val, "dict") and callable(getattr(val, "dict")):
|
|
129
|
+
try:
|
|
130
|
+
return dict(val.dict())
|
|
131
|
+
except Exception:
|
|
132
|
+
pass
|
|
133
|
+
# default empty
|
|
134
|
+
return {}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _try_read_inbound(inbound: Mapping[str, Any], field: str) -> Tuple[bool, Any]:
|
|
138
|
+
"""
|
|
139
|
+
Distinguish ABSENT vs present(None).
|
|
140
|
+
"""
|
|
141
|
+
if field in inbound:
|
|
142
|
+
return True, inbound.get(field, None)
|
|
143
|
+
# Be tolerant to alias-style inputs (if present)
|
|
144
|
+
for alt in (field.lower(), field.upper()):
|
|
145
|
+
if alt in inbound:
|
|
146
|
+
return True, inbound.get(alt)
|
|
147
|
+
return False, None
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _ctx_view(ctx: Any) -> Dict[str, Any]:
|
|
151
|
+
"""
|
|
152
|
+
Provide a small read-only view for default_factory functions
|
|
153
|
+
without exposing the entire executor context.
|
|
154
|
+
"""
|
|
155
|
+
view = {
|
|
156
|
+
"op": getattr(ctx, "op", None),
|
|
157
|
+
"persist": getattr(ctx, "persist", True),
|
|
158
|
+
"temp": getattr(ctx, "temp", None),
|
|
159
|
+
# optional hints the executor might set
|
|
160
|
+
"tenant": getattr(ctx, "tenant", None),
|
|
161
|
+
"user": getattr(ctx, "user", None),
|
|
162
|
+
"now": getattr(ctx, "now", None),
|
|
163
|
+
}
|
|
164
|
+
return view
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
__all__ = ["ANCHOR", "run"]
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# tigrbl/v3/runtime/atoms/resolve/paired_gen.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import secrets
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Any, Dict, MutableMapping, Optional
|
|
7
|
+
|
|
8
|
+
from ... import events as _ev
|
|
9
|
+
from ...opview import opview_from_ctx, _ensure_temp
|
|
10
|
+
|
|
11
|
+
# Runs in HANDLER phase, before pre:flush (and before storage transforms).
|
|
12
|
+
ANCHOR = _ev.RESOLVE_VALUES # "resolve:values"
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger("uvicorn")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def run(obj: Optional[object], ctx: Any) -> None:
|
|
18
|
+
"""
|
|
19
|
+
resolve:paired_gen@resolve:values
|
|
20
|
+
|
|
21
|
+
Purpose
|
|
22
|
+
-------
|
|
23
|
+
Prepare *paired* raw values for columns marked as secret-once/paired.
|
|
24
|
+
The raw value is:
|
|
25
|
+
- taken from inbound virtual input (if provided), or
|
|
26
|
+
- generated securely (e.g., URL-safe token) when inbound is ABSENT.
|
|
27
|
+
|
|
28
|
+
Contracts / Conventions
|
|
29
|
+
-----------------------
|
|
30
|
+
- ctx.temp:
|
|
31
|
+
- "virtual_in" : dict of inbound virtual values (from resolve:assemble)
|
|
32
|
+
- "paired_values" : dict[field -> {"raw": <str>, "alias": <str>, "meta": {...}}]
|
|
33
|
+
- "persist_from_paired": dict[field -> {"source": ("paired_values", field, "raw")}]
|
|
34
|
+
- "assembled_values" : dict used for persistence (we DO NOT put raw into it)
|
|
35
|
+
- "generated_paired" : tuple of fields generated here (diagnostics)
|
|
36
|
+
- Secret-once guarantee is enforced later by emit atoms (post-refresh + readtime).
|
|
37
|
+
|
|
38
|
+
Policy
|
|
39
|
+
------
|
|
40
|
+
- We treat a column as "paired" if any of these flags exist (ColumnSpec or FieldSpec):
|
|
41
|
+
secret_once=True | paired=True | paired_input=True | generate_on_absent=True
|
|
42
|
+
OR if a generator callable is present on the spec (generator/paired_generator/secret_generator).
|
|
43
|
+
- If inbound virtual value for the alias exists, we adopt it as the raw.
|
|
44
|
+
- Otherwise we call the generator if provided, else generate a token.
|
|
45
|
+
- We place a *pointer* into ctx.temp["persist_from_paired"] for storage:to_stored
|
|
46
|
+
to derive the persisted representation right before flush.
|
|
47
|
+
"""
|
|
48
|
+
# Non-persisting ops should have pruned this anchor; keep guard for safety.
|
|
49
|
+
if getattr(ctx, "persist", True) is False:
|
|
50
|
+
logger.debug("Skipping resolve:paired_gen; ctx.persist is False")
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
logger.debug("Running resolve:paired_gen")
|
|
54
|
+
ov = opview_from_ctx(ctx)
|
|
55
|
+
temp = _ensure_temp(ctx)
|
|
56
|
+
assembled = _ensure_dict(temp, "assembled_values")
|
|
57
|
+
virtual_in = _ensure_dict(temp, "virtual_in")
|
|
58
|
+
paired_values = _ensure_dict(temp, "paired_values")
|
|
59
|
+
persist_from_paired = _ensure_dict(temp, "persist_from_paired")
|
|
60
|
+
|
|
61
|
+
generated: list[str] = []
|
|
62
|
+
|
|
63
|
+
for field, desc in ov.paired_index.items():
|
|
64
|
+
if field in assembled:
|
|
65
|
+
logger.debug(
|
|
66
|
+
"Field %s already has assembled value; skipping generation", field
|
|
67
|
+
)
|
|
68
|
+
continue
|
|
69
|
+
|
|
70
|
+
alias_name = desc.get("alias") or field
|
|
71
|
+
raw = None
|
|
72
|
+
if alias_name in virtual_in:
|
|
73
|
+
raw = virtual_in.get(alias_name)
|
|
74
|
+
logger.debug(
|
|
75
|
+
"Using client-provided raw for field %s via alias %s", field, alias_name
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
if raw is None:
|
|
79
|
+
logger.debug("Generating raw value for field %s", field)
|
|
80
|
+
gen = desc.get("gen")
|
|
81
|
+
if callable(gen):
|
|
82
|
+
try:
|
|
83
|
+
raw = gen(_ctx_view(ctx))
|
|
84
|
+
except Exception:
|
|
85
|
+
logger.debug("Generator failed for field %s", field)
|
|
86
|
+
raw = None
|
|
87
|
+
if raw is None:
|
|
88
|
+
raw = _secure_token(desc.get("max_length", 0) or 0)
|
|
89
|
+
|
|
90
|
+
if raw is None:
|
|
91
|
+
raise RuntimeError(f"paired_raw_missing:{field}")
|
|
92
|
+
|
|
93
|
+
meta = {}
|
|
94
|
+
if "mask_last" in desc:
|
|
95
|
+
meta["mask_last"] = desc["mask_last"]
|
|
96
|
+
|
|
97
|
+
paired_values[field] = {"raw": raw, "alias": alias_name, "meta": meta}
|
|
98
|
+
logger.debug("Recorded paired raw for field %s", field)
|
|
99
|
+
|
|
100
|
+
persist_from_paired[field] = {"source": ("paired_values", field, "raw")}
|
|
101
|
+
|
|
102
|
+
generated.append(field)
|
|
103
|
+
|
|
104
|
+
if generated:
|
|
105
|
+
temp["generated_paired"] = tuple(generated)
|
|
106
|
+
logger.debug("Generated paired fields: %s", generated)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
110
|
+
# Internals
|
|
111
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _ensure_dict(temp: MutableMapping[str, Any], key: str) -> Dict[str, Any]:
|
|
115
|
+
d = temp.get(key)
|
|
116
|
+
if not isinstance(d, dict):
|
|
117
|
+
d = {}
|
|
118
|
+
temp[key] = d
|
|
119
|
+
return d # type: ignore[return-value]
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _ctx_view(ctx: Any) -> Dict[str, Any]:
|
|
123
|
+
"""Small read-only view for generator callables."""
|
|
124
|
+
return {
|
|
125
|
+
"op": getattr(ctx, "op", None),
|
|
126
|
+
"persist": getattr(ctx, "persist", True),
|
|
127
|
+
"temp": getattr(ctx, "temp", None),
|
|
128
|
+
"tenant": getattr(ctx, "tenant", None),
|
|
129
|
+
"user": getattr(ctx, "user", None),
|
|
130
|
+
"now": getattr(ctx, "now", None),
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _secure_token(max_len: int) -> str:
|
|
135
|
+
"""
|
|
136
|
+
Generate a URL-safe token. If max_len > 0, keep within that bound.
|
|
137
|
+
We aim for ~32 bytes entropy by default.
|
|
138
|
+
"""
|
|
139
|
+
# 32 bytes → ~43 chars base64-url
|
|
140
|
+
token = secrets.token_urlsafe(32)
|
|
141
|
+
if max_len and max_len > 0 and len(token) > max_len:
|
|
142
|
+
# Trim conservatively; if extremely small, ensure we still return something.
|
|
143
|
+
token = token[: max(8, max_len)]
|
|
144
|
+
return token
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
__all__ = ["ANCHOR", "run"]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Any, Callable, Dict, Optional, Tuple
|
|
3
|
+
|
|
4
|
+
from ... import events as _ev
|
|
5
|
+
from .template import run as _template
|
|
6
|
+
from .negotiate import run as _negotiate
|
|
7
|
+
from .render import run as _render
|
|
8
|
+
from . import headers_from_payload as _hdr_payload
|
|
9
|
+
|
|
10
|
+
RunFn = Callable[[Optional[object], Any], Any]
|
|
11
|
+
|
|
12
|
+
REGISTRY: Dict[Tuple[str, str], Tuple[str, RunFn]] = {
|
|
13
|
+
("response", "template"): (_ev.OUT_DUMP, _template),
|
|
14
|
+
("response", "negotiate"): (_ev.OUT_DUMP, _negotiate),
|
|
15
|
+
("response", "headers_from_payload"): (_hdr_payload.ANCHOR, _hdr_payload.run),
|
|
16
|
+
("response", "render"): (_ev.OUT_DUMP, _render),
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
__all__ = ["REGISTRY", "RunFn"]
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from ... import events as _ev
|
|
4
|
+
from typing import Mapping
|
|
5
|
+
|
|
6
|
+
ANCHOR = _ev.OUT_BUILD # run after payload is prepared, before render
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def run(_, ctx) -> None:
|
|
10
|
+
"""Mirror fields configured with ``io.header_out`` into HTTP response headers.
|
|
11
|
+
|
|
12
|
+
- Does NOT remove fields from the response body (no header-only behavior).
|
|
13
|
+
- Honors op-specific exposure via ``io.out_verbs``.
|
|
14
|
+
Complexity: O(#fields in opview).
|
|
15
|
+
"""
|
|
16
|
+
from tigrbl.response import ResponseHints
|
|
17
|
+
|
|
18
|
+
resp = getattr(ctx, "response", None)
|
|
19
|
+
if resp is None:
|
|
20
|
+
return
|
|
21
|
+
|
|
22
|
+
hints = getattr(resp, "hints", None)
|
|
23
|
+
if hints is None:
|
|
24
|
+
hints = ResponseHints()
|
|
25
|
+
resp.hints = hints
|
|
26
|
+
|
|
27
|
+
payload = getattr(resp, "result", None)
|
|
28
|
+
if payload is None:
|
|
29
|
+
temp = getattr(ctx, "temp", None)
|
|
30
|
+
if isinstance(temp, Mapping):
|
|
31
|
+
payload = temp.get("response_payload")
|
|
32
|
+
if not isinstance(payload, dict):
|
|
33
|
+
return
|
|
34
|
+
|
|
35
|
+
specs = getattr(ctx, "specs", None)
|
|
36
|
+
if not isinstance(specs, Mapping):
|
|
37
|
+
model = getattr(ctx, "model", None)
|
|
38
|
+
specs = getattr(model, "__tigrbl_cols__", {}) if model is not None else {}
|
|
39
|
+
|
|
40
|
+
op = getattr(ctx, "op", None)
|
|
41
|
+
for field_name, spec in specs.items():
|
|
42
|
+
io = getattr(spec, "io", None)
|
|
43
|
+
if not io:
|
|
44
|
+
continue
|
|
45
|
+
|
|
46
|
+
header_name = getattr(io, "header_out", None)
|
|
47
|
+
if not header_name:
|
|
48
|
+
continue
|
|
49
|
+
|
|
50
|
+
out_verbs = set(getattr(io, "out_verbs", ()) or ())
|
|
51
|
+
if op not in out_verbs:
|
|
52
|
+
continue
|
|
53
|
+
|
|
54
|
+
if field_name in payload:
|
|
55
|
+
value = payload[field_name]
|
|
56
|
+
if value is not None:
|
|
57
|
+
hints.headers[header_name] = str(value)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Any, Optional
|
|
3
|
+
|
|
4
|
+
from ... import events as _ev
|
|
5
|
+
from .negotiation import negotiate_media_type
|
|
6
|
+
from .renderer import ResponseHints
|
|
7
|
+
|
|
8
|
+
ANCHOR = _ev.OUT_DUMP # "out:dump"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def run(obj: Optional[object], ctx: Any) -> None:
|
|
12
|
+
"""response:negotiate@out:dump
|
|
13
|
+
|
|
14
|
+
Determine the response media type if not already set.
|
|
15
|
+
"""
|
|
16
|
+
resp_ns = getattr(ctx, "response", None)
|
|
17
|
+
req = getattr(ctx, "request", None)
|
|
18
|
+
if resp_ns is None or req is None:
|
|
19
|
+
return
|
|
20
|
+
hints = getattr(resp_ns, "hints", None)
|
|
21
|
+
if hints is None:
|
|
22
|
+
hints = ResponseHints()
|
|
23
|
+
resp_ns.hints = hints
|
|
24
|
+
if not hints.media_type:
|
|
25
|
+
accept = getattr(req, "headers", {}).get("accept", "*/*")
|
|
26
|
+
default_media = getattr(resp_ns, "default_media", "application/json")
|
|
27
|
+
hints.media_type = negotiate_media_type(accept, default_media)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
__all__ = ["ANCHOR", "run"]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import List
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _parse(accept: str) -> List[str]:
|
|
7
|
+
parts = [p.strip() for p in accept.split(",") if p.strip()]
|
|
8
|
+
|
|
9
|
+
def q(p: str) -> float:
|
|
10
|
+
for seg in p.split(";"):
|
|
11
|
+
seg = seg.strip()
|
|
12
|
+
if seg.startswith("q="):
|
|
13
|
+
try:
|
|
14
|
+
return float(seg[2:])
|
|
15
|
+
except Exception:
|
|
16
|
+
return 1.0
|
|
17
|
+
return 1.0
|
|
18
|
+
|
|
19
|
+
return sorted(parts, key=q, reverse=True)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger("uvicorn")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def negotiate_media_type(accept: str, default_media: str) -> str:
|
|
26
|
+
logger.debug("Negotiating media type from Accept header %s", accept)
|
|
27
|
+
if not accept or accept == "*/*":
|
|
28
|
+
return default_media
|
|
29
|
+
for cand in _parse(accept):
|
|
30
|
+
mt = cand.split(";")[0].strip()
|
|
31
|
+
if mt in (
|
|
32
|
+
"application/json",
|
|
33
|
+
"text/html",
|
|
34
|
+
"text/plain",
|
|
35
|
+
"application/octet-stream",
|
|
36
|
+
):
|
|
37
|
+
return mt
|
|
38
|
+
if mt.startswith("text/"):
|
|
39
|
+
return "text/plain"
|
|
40
|
+
return default_media
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
__all__ = ["negotiate_media_type"]
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Any, Optional
|
|
3
|
+
|
|
4
|
+
from ... import events as _ev
|
|
5
|
+
from .renderer import render
|
|
6
|
+
|
|
7
|
+
ANCHOR = _ev.OUT_DUMP # "out:dump"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def run(obj: Optional[object], ctx: Any) -> Any:
|
|
11
|
+
"""response:render@out:dump
|
|
12
|
+
|
|
13
|
+
Render ``ctx.response.result`` into a concrete Response object.
|
|
14
|
+
"""
|
|
15
|
+
resp_ns = getattr(ctx, "response", None)
|
|
16
|
+
req = getattr(ctx, "request", None)
|
|
17
|
+
if resp_ns is None or req is None:
|
|
18
|
+
return None
|
|
19
|
+
result = getattr(resp_ns, "result", None)
|
|
20
|
+
if result is None:
|
|
21
|
+
return None
|
|
22
|
+
hints = getattr(resp_ns, "hints", None)
|
|
23
|
+
default_media = getattr(resp_ns, "default_media", "application/json")
|
|
24
|
+
envelope_default = getattr(resp_ns, "envelope_default", False)
|
|
25
|
+
resp = render(
|
|
26
|
+
req,
|
|
27
|
+
result,
|
|
28
|
+
hints=hints,
|
|
29
|
+
default_media=default_media,
|
|
30
|
+
envelope_default=envelope_default,
|
|
31
|
+
)
|
|
32
|
+
resp_ns.result = resp
|
|
33
|
+
return resp
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
__all__ = ["ANCHOR", "run"]
|