tigrbl 0.0.1.dev1__py3-none-any.whl → 0.3.0.dev2__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.dev2.dist-info/LICENSE +201 -0
- tigrbl-0.3.0.dev2.dist-info/METADATA +501 -0
- tigrbl-0.3.0.dev2.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.dev2.dist-info}/WHEEL +0 -0
|
@@ -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,17 @@
|
|
|
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
|
+
|
|
9
|
+
RunFn = Callable[[Optional[object], Any], Any]
|
|
10
|
+
|
|
11
|
+
REGISTRY: Dict[Tuple[str, str], Tuple[str, RunFn]] = {
|
|
12
|
+
("response", "template"): (_ev.OUT_DUMP, _template),
|
|
13
|
+
("response", "negotiate"): (_ev.OUT_DUMP, _negotiate),
|
|
14
|
+
("response", "render"): (_ev.OUT_DUMP, _render),
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
__all__ = ["REGISTRY", "RunFn"]
|
|
@@ -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"]
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any, AsyncIterable, Iterable, Mapping, Optional, Union, cast
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
from ....deps.starlette import BackgroundTask, Response
|
|
8
|
+
|
|
9
|
+
from ....response.shortcuts import (
|
|
10
|
+
as_file,
|
|
11
|
+
as_html,
|
|
12
|
+
as_json,
|
|
13
|
+
as_stream,
|
|
14
|
+
as_text,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
JSON = Mapping[str, Any]
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger("uvicorn")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class ResponseHints:
|
|
24
|
+
media_type: Optional[str] = None
|
|
25
|
+
status_code: int = 200
|
|
26
|
+
headers: dict[str, str] = field(default_factory=dict)
|
|
27
|
+
filename: Optional[str] = None
|
|
28
|
+
download: bool = False
|
|
29
|
+
etag: Optional[str] = None
|
|
30
|
+
last_modified: Optional[Any] = None
|
|
31
|
+
background: Optional[BackgroundTask] = None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ResponseKind:
|
|
35
|
+
JSON = "application/json"
|
|
36
|
+
HTML = "text/html"
|
|
37
|
+
TEXT = "text/plain"
|
|
38
|
+
FILE = "application/file"
|
|
39
|
+
STREAM = "application/stream"
|
|
40
|
+
REDIRECT = "application/redirect"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
ResponseLike = Union[
|
|
44
|
+
Response,
|
|
45
|
+
bytes,
|
|
46
|
+
bytearray,
|
|
47
|
+
memoryview,
|
|
48
|
+
str,
|
|
49
|
+
Path,
|
|
50
|
+
JSON,
|
|
51
|
+
Iterable[bytes],
|
|
52
|
+
AsyncIterable[bytes],
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def render(
|
|
57
|
+
request: Any,
|
|
58
|
+
payload: ResponseLike,
|
|
59
|
+
*,
|
|
60
|
+
hints: Optional[ResponseHints] = None,
|
|
61
|
+
default_media: str = "application/json",
|
|
62
|
+
envelope_default: bool = False,
|
|
63
|
+
) -> Response:
|
|
64
|
+
logger.debug("Rendering response with payload type %s", type(payload))
|
|
65
|
+
if isinstance(payload, Response):
|
|
66
|
+
return payload
|
|
67
|
+
|
|
68
|
+
hints = hints or ResponseHints()
|
|
69
|
+
chosen = hints.media_type or default_media
|
|
70
|
+
|
|
71
|
+
if isinstance(payload, Path):
|
|
72
|
+
return as_file(
|
|
73
|
+
payload,
|
|
74
|
+
filename=hints.filename,
|
|
75
|
+
download=hints.download,
|
|
76
|
+
status=hints.status_code,
|
|
77
|
+
headers=hints.headers,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
if isinstance(payload, (bytes, bytearray, memoryview)):
|
|
81
|
+
return as_stream(
|
|
82
|
+
iter((bytes(payload),)),
|
|
83
|
+
media_type="application/octet-stream",
|
|
84
|
+
status=hints.status_code,
|
|
85
|
+
headers=hints.headers,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
if hasattr(payload, "__aiter__") or (
|
|
89
|
+
hasattr(payload, "__iter__") and not isinstance(payload, (str, dict, list))
|
|
90
|
+
):
|
|
91
|
+
return as_stream(
|
|
92
|
+
cast(Union[Iterable[bytes], AsyncIterable[bytes]], payload),
|
|
93
|
+
media_type="application/octet-stream",
|
|
94
|
+
status=hints.status_code,
|
|
95
|
+
headers=hints.headers,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
if isinstance(payload, str):
|
|
99
|
+
if payload.lstrip().startswith("<") or chosen == "text/html":
|
|
100
|
+
return as_html(payload, status=hints.status_code, headers=hints.headers)
|
|
101
|
+
return as_text(payload, status=hints.status_code, headers=hints.headers)
|
|
102
|
+
|
|
103
|
+
return as_json(
|
|
104
|
+
payload,
|
|
105
|
+
status=hints.status_code,
|
|
106
|
+
headers=hints.headers,
|
|
107
|
+
envelope=envelope_default,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
__all__ = [
|
|
112
|
+
"ResponseHints",
|
|
113
|
+
"ResponseKind",
|
|
114
|
+
"ResponseLike",
|
|
115
|
+
"render",
|
|
116
|
+
]
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Any, Optional
|
|
3
|
+
|
|
4
|
+
from ... import events as _ev
|
|
5
|
+
from .templates import render_template
|
|
6
|
+
from .renderer import ResponseHints
|
|
7
|
+
|
|
8
|
+
ANCHOR = _ev.OUT_DUMP # "out:dump"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
async def run(obj: Optional[object], ctx: Any) -> None:
|
|
12
|
+
"""response:template@out:dump
|
|
13
|
+
|
|
14
|
+
Render a template if configured on ``ctx.response``.
|
|
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
|
+
tmpl = getattr(resp_ns, "template", None)
|
|
21
|
+
if not tmpl:
|
|
22
|
+
return
|
|
23
|
+
result = getattr(resp_ns, "result", None)
|
|
24
|
+
context = result if isinstance(result, dict) else {"data": result}
|
|
25
|
+
html = await render_template(
|
|
26
|
+
name=tmpl.name,
|
|
27
|
+
context=context,
|
|
28
|
+
search_paths=tmpl.search_paths,
|
|
29
|
+
package=tmpl.package,
|
|
30
|
+
auto_reload=bool(tmpl.auto_reload),
|
|
31
|
+
filters=tmpl.filters,
|
|
32
|
+
globals_=tmpl.globals,
|
|
33
|
+
request=req,
|
|
34
|
+
)
|
|
35
|
+
resp_ns.result = html
|
|
36
|
+
hints = getattr(resp_ns, "hints", None)
|
|
37
|
+
if hints is None:
|
|
38
|
+
hints = ResponseHints()
|
|
39
|
+
resp_ns.hints = hints
|
|
40
|
+
if not hints.media_type:
|
|
41
|
+
hints.media_type = "text/html"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
__all__ = ["ANCHOR", "run"]
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from functools import lru_cache
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any, Dict, Iterable, Optional, Tuple
|
|
5
|
+
|
|
6
|
+
from ....deps.starlette import Request
|
|
7
|
+
from ....deps.jinja import (
|
|
8
|
+
Environment,
|
|
9
|
+
FileSystemLoader,
|
|
10
|
+
PackageLoader,
|
|
11
|
+
ChoiceLoader,
|
|
12
|
+
select_autoescape,
|
|
13
|
+
TemplateNotFound,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger("uvicorn")
|
|
17
|
+
if Environment is None: # pragma: no cover - jinja2 not installed
|
|
18
|
+
|
|
19
|
+
async def render_template(
|
|
20
|
+
*,
|
|
21
|
+
name: str,
|
|
22
|
+
context: Dict[str, Any],
|
|
23
|
+
search_paths: Iterable[str] = (),
|
|
24
|
+
package: Optional[str] = None,
|
|
25
|
+
auto_reload: bool = False,
|
|
26
|
+
filters: Optional[Dict[str, Any]] = None,
|
|
27
|
+
globals_: Optional[Dict[str, Any]] = None,
|
|
28
|
+
request: Optional[Request] = None,
|
|
29
|
+
) -> str:
|
|
30
|
+
logger.debug("Rendering template %s", name)
|
|
31
|
+
raise RuntimeError("jinja2 is required for template rendering")
|
|
32
|
+
|
|
33
|
+
else:
|
|
34
|
+
|
|
35
|
+
def _mk_loader(search_paths: Iterable[str], package: Optional[str]) -> ChoiceLoader:
|
|
36
|
+
loaders = []
|
|
37
|
+
if search_paths:
|
|
38
|
+
loaders.append(FileSystemLoader(list(search_paths)))
|
|
39
|
+
if package:
|
|
40
|
+
loaders.append(PackageLoader(package_name=package))
|
|
41
|
+
if not loaders:
|
|
42
|
+
loaders.append(FileSystemLoader(["."]))
|
|
43
|
+
return ChoiceLoader(loaders)
|
|
44
|
+
|
|
45
|
+
@lru_cache(maxsize=64)
|
|
46
|
+
def _get_env(
|
|
47
|
+
search_paths_key: Tuple[str, ...],
|
|
48
|
+
package: Optional[str],
|
|
49
|
+
auto_reload: bool,
|
|
50
|
+
) -> Environment:
|
|
51
|
+
env = Environment(
|
|
52
|
+
loader=_mk_loader(search_paths_key, package),
|
|
53
|
+
autoescape=select_autoescape(["html", "xml"]),
|
|
54
|
+
auto_reload=auto_reload,
|
|
55
|
+
enable_async=True,
|
|
56
|
+
)
|
|
57
|
+
return env
|
|
58
|
+
|
|
59
|
+
async def render_template(
|
|
60
|
+
*,
|
|
61
|
+
name: str,
|
|
62
|
+
context: Dict[str, Any],
|
|
63
|
+
search_paths: Iterable[str] = (),
|
|
64
|
+
package: Optional[str] = None,
|
|
65
|
+
auto_reload: bool = False,
|
|
66
|
+
filters: Optional[Dict[str, Any]] = None,
|
|
67
|
+
globals_: Optional[Dict[str, Any]] = None,
|
|
68
|
+
request: Optional[Request] = None,
|
|
69
|
+
) -> str:
|
|
70
|
+
logger.debug("Rendering template %s", name)
|
|
71
|
+
env = _get_env(tuple(search_paths), package, auto_reload)
|
|
72
|
+
if filters:
|
|
73
|
+
env.filters.update(filters)
|
|
74
|
+
if globals_:
|
|
75
|
+
env.globals.update(globals_)
|
|
76
|
+
if request is not None:
|
|
77
|
+
env.globals.setdefault("url_for", request.url_for)
|
|
78
|
+
env.globals.setdefault("request", request)
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
tmpl = env.get_template(name)
|
|
82
|
+
except TemplateNotFound as e: # pragma: no cover - passthrough
|
|
83
|
+
raise FileNotFoundError(f"Template not found: {name}") from e
|
|
84
|
+
|
|
85
|
+
return await tmpl.render_async(**context)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
__all__ = ["render_template"]
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# tigrbl/v3/runtime/atoms/schema/__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 collect_in as _collect_in
|
|
9
|
+
from . import collect_out as _collect_out
|
|
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
|
+
("schema", "collect_in"): (_collect_in.ANCHOR, _collect_in.run),
|
|
18
|
+
("schema", "collect_out"): (_collect_out.ANCHOR, _collect_out.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 'schema' 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 'schema' domain."""
|
|
33
|
+
key = ("schema", subject)
|
|
34
|
+
if key not in REGISTRY:
|
|
35
|
+
raise KeyError(f"Unknown schema atom subject: {subject!r}")
|
|
36
|
+
logger.debug("Retrieving 'schema' subject %s", subject)
|
|
37
|
+
return REGISTRY[key]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
__all__ = ["REGISTRY", "RunFn", "subjects", "get"]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
import logging
|
|
5
|
+
|
|
6
|
+
from ... import events as _ev
|
|
7
|
+
from ...opview import opview_from_ctx, ensure_schema_in
|
|
8
|
+
|
|
9
|
+
# Runs at the very beginning of the lifecycle, before in-model build/validation.
|
|
10
|
+
ANCHOR = _ev.SCHEMA_COLLECT_IN # "schema:collect_in"
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger("uvicorn")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def run(obj: Optional[object], ctx: Any) -> None:
|
|
16
|
+
"""Load precompiled inbound schema into ctx.temp."""
|
|
17
|
+
ov = opview_from_ctx(ctx)
|
|
18
|
+
ensure_schema_in(ctx, ov)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
__all__ = ["ANCHOR", "run"]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
import logging
|
|
5
|
+
|
|
6
|
+
from ... import events as _ev
|
|
7
|
+
from ...opview import opview_from_ctx, ensure_schema_out
|
|
8
|
+
|
|
9
|
+
# Runs late in POST_HANDLER, before out model build and dumping.
|
|
10
|
+
ANCHOR = _ev.SCHEMA_COLLECT_OUT # "schema:collect_out"
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger("uvicorn")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def run(obj: Optional[object], ctx: Any) -> None:
|
|
16
|
+
"""Load precompiled outbound schema into ctx.temp."""
|
|
17
|
+
ov = opview_from_ctx(ctx)
|
|
18
|
+
ensure_schema_out(ctx, ov)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
__all__ = ["ANCHOR", "run"]
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# tigrbl/v3/runtime/atoms/storage/__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 to_stored as _to_stored
|
|
9
|
+
|
|
10
|
+
# Runner signature: (obj|None, ctx) -> None
|
|
11
|
+
RunFn = Callable[[Optional[object], Any], None]
|
|
12
|
+
|
|
13
|
+
#: Domain-scoped registry consumed by the kernel plan (and aggregated at atoms/__init__.py).
|
|
14
|
+
#: Keys are (domain, subject); values are (anchor, runner).
|
|
15
|
+
REGISTRY: Dict[Tuple[str, str], Tuple[str, RunFn]] = {
|
|
16
|
+
("storage", "to_stored"): (_to_stored.ANCHOR, _to_stored.run),
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger("uvicorn")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def subjects() -> Tuple[str, ...]:
|
|
23
|
+
"""Return the subject names exported by this domain."""
|
|
24
|
+
subjects = tuple(s for (_, s) in REGISTRY.keys())
|
|
25
|
+
logger.debug("Listing 'storage' subjects: %s", subjects)
|
|
26
|
+
return subjects
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get(subject: str) -> Tuple[str, RunFn]:
|
|
30
|
+
"""Return (anchor, runner) for a subject in the 'storage' domain."""
|
|
31
|
+
key = ("storage", subject)
|
|
32
|
+
if key not in REGISTRY:
|
|
33
|
+
raise KeyError(f"Unknown storage atom subject: {subject!r}")
|
|
34
|
+
logger.debug("Retrieving 'storage' subject %s", subject)
|
|
35
|
+
return REGISTRY[key]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
__all__ = ["REGISTRY", "RunFn", "subjects", "get"]
|