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,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"]
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# tigrbl/v3/runtime/atoms/storage/to_stored.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
from typing import Any, Dict, Mapping, MutableMapping, Optional
|
|
6
|
+
|
|
7
|
+
from ... import events as _ev
|
|
8
|
+
from ...opview import opview_from_ctx, ensure_schema_in, _ensure_temp
|
|
9
|
+
|
|
10
|
+
# Runs right before the handler flushes to the DB.
|
|
11
|
+
ANCHOR = _ev.PRE_FLUSH # "pre:flush"
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger("uvicorn")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def run(obj: Optional[object], ctx: Any) -> None:
|
|
17
|
+
"""
|
|
18
|
+
storage:to_stored@pre:flush
|
|
19
|
+
|
|
20
|
+
Transform inbound values into their persisted representation.
|
|
21
|
+
|
|
22
|
+
- For *paired/secret-once* columns, derive the stored value from the prepared raw
|
|
23
|
+
and assign it to BOTH ctx.temp["assembled_values"][field] AND the ORM object.
|
|
24
|
+
Also handles a fallback when 'persist_from_paired' wasn't queued but a paired raw
|
|
25
|
+
exists (planner tolerance).
|
|
26
|
+
- Otherwise, apply an optional per-column inbound→stored transform and mirror the
|
|
27
|
+
transformed value onto the ORM instance.
|
|
28
|
+
|
|
29
|
+
On failure to derive for paired, raise ValueError (mapped by higher layers) to
|
|
30
|
+
avoid leaking DB IntegrityErrors.
|
|
31
|
+
"""
|
|
32
|
+
logger.debug("Running storage:to_stored")
|
|
33
|
+
if getattr(ctx, "persist", True) is False:
|
|
34
|
+
logger.debug("Skipping storage:to_stored; ctx.persist is False")
|
|
35
|
+
return
|
|
36
|
+
|
|
37
|
+
ov = opview_from_ctx(ctx)
|
|
38
|
+
schema_in = ensure_schema_in(ctx, ov)
|
|
39
|
+
temp = _ensure_temp(ctx)
|
|
40
|
+
assembled = _ensure_dict(temp, "assembled_values")
|
|
41
|
+
paired_values = _ensure_dict(temp, "paired_values")
|
|
42
|
+
pf_paired = _ensure_dict(temp, "persist_from_paired")
|
|
43
|
+
slog = _ensure_list(temp, "storage_log")
|
|
44
|
+
serr = _ensure_list(temp, "storage_errors")
|
|
45
|
+
|
|
46
|
+
# Prefer explicit obj (hydrated instance), else ctx.model if adapter provided it
|
|
47
|
+
target_obj = obj or getattr(ctx, "model", None)
|
|
48
|
+
|
|
49
|
+
# Ensure paired fields are considered even when absent from inbound schema.
|
|
50
|
+
# schema_in["fields"] may omit columns like "digest" that generate their
|
|
51
|
+
# values server-side via IO(...).paired. To derive their stored values, merge
|
|
52
|
+
# the explicit schema fields with the paired index keys.
|
|
53
|
+
all_fields = set(schema_in["fields"]) | set(ov.paired_index.keys())
|
|
54
|
+
|
|
55
|
+
for field in sorted(all_fields):
|
|
56
|
+
if field in ov.paired_index:
|
|
57
|
+
if field in pf_paired or field in paired_values:
|
|
58
|
+
raw = None
|
|
59
|
+
if field in pf_paired:
|
|
60
|
+
raw = _resolve_from_pointer(
|
|
61
|
+
pf_paired[field].get("source"), paired_values, field
|
|
62
|
+
)
|
|
63
|
+
if raw is None:
|
|
64
|
+
raw = paired_values.get(field, {}).get("raw")
|
|
65
|
+
if raw is None:
|
|
66
|
+
serr.append({"field": field, "error": "missing_paired_raw"})
|
|
67
|
+
logger.debug("Missing paired raw for field %s", field)
|
|
68
|
+
raise RuntimeError(f"paired_raw_missing:{field}")
|
|
69
|
+
deriver = ov.paired_index[field].get("store")
|
|
70
|
+
try:
|
|
71
|
+
stored = deriver(raw, ctx) if callable(deriver) else raw
|
|
72
|
+
except Exception as e:
|
|
73
|
+
serr.append(
|
|
74
|
+
{"field": field, "error": f"deriver_failed:{type(e).__name__}"}
|
|
75
|
+
)
|
|
76
|
+
logger.debug("Deriver failed for field %s: %s", field, e)
|
|
77
|
+
raise
|
|
78
|
+
assembled[field] = stored
|
|
79
|
+
_assign_to_model(target_obj, field, stored)
|
|
80
|
+
slog.append({"field": field, "action": "derived_from_paired"})
|
|
81
|
+
logger.debug("Derived stored value for paired field %s", field)
|
|
82
|
+
continue
|
|
83
|
+
|
|
84
|
+
nullable = schema_in["by_field"].get(field, {}).get("nullable", True)
|
|
85
|
+
if (
|
|
86
|
+
not nullable
|
|
87
|
+
and field not in assembled
|
|
88
|
+
and not _has_attr_with_value(target_obj, field)
|
|
89
|
+
):
|
|
90
|
+
serr.append({"field": field, "error": "paired_missing_before_flush"})
|
|
91
|
+
logger.debug("Paired field %s missing before flush", field)
|
|
92
|
+
raise RuntimeError(f"paired_missing_before_flush:{field}")
|
|
93
|
+
continue
|
|
94
|
+
|
|
95
|
+
if field in assembled:
|
|
96
|
+
transform = ov.to_stored_transforms.get(field)
|
|
97
|
+
if transform is None:
|
|
98
|
+
logger.debug("No transform for field %s; using assembled value", field)
|
|
99
|
+
_assign_to_model(target_obj, field, assembled[field])
|
|
100
|
+
continue
|
|
101
|
+
try:
|
|
102
|
+
stored_val = transform(assembled[field], ctx)
|
|
103
|
+
assembled[field] = stored_val
|
|
104
|
+
_assign_to_model(target_obj, field, stored_val)
|
|
105
|
+
slog.append({"field": field, "action": "transformed"})
|
|
106
|
+
logger.debug("Transformed field %s", field)
|
|
107
|
+
except Exception as e:
|
|
108
|
+
serr.append(
|
|
109
|
+
{"field": field, "error": f"transform_failed:{type(e).__name__}"}
|
|
110
|
+
)
|
|
111
|
+
logger.debug("Transform failed for field %s: %s", field, e)
|
|
112
|
+
raise
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
116
|
+
# Internals (tolerant to spec shapes)
|
|
117
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _assign_to_model(target: Optional[object], field: str, value: Any) -> None:
|
|
121
|
+
"""Safely assign value onto the hydrated ORM object so SQLAlchemy flushes it."""
|
|
122
|
+
if target is None:
|
|
123
|
+
return
|
|
124
|
+
try:
|
|
125
|
+
setattr(target, field, value)
|
|
126
|
+
except Exception:
|
|
127
|
+
# Non-fatal: some adapters may not expose an assignable object here.
|
|
128
|
+
pass
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _has_attr_with_value(target: Optional[object], field: str) -> bool:
|
|
132
|
+
if target is None or not hasattr(target, field):
|
|
133
|
+
return False
|
|
134
|
+
try:
|
|
135
|
+
return getattr(target, field) is not None
|
|
136
|
+
except Exception:
|
|
137
|
+
return False
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _ensure_dict(temp: MutableMapping[str, Any], key: str) -> Dict[str, Any]:
|
|
141
|
+
d = temp.get(key)
|
|
142
|
+
if not isinstance(d, dict):
|
|
143
|
+
d = {}
|
|
144
|
+
temp[key] = d
|
|
145
|
+
return d # type: ignore[return-value]
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _ensure_list(temp: MutableMapping[str, Any], key: str) -> list:
|
|
149
|
+
lst = temp.get(key)
|
|
150
|
+
if not isinstance(lst, list):
|
|
151
|
+
lst = []
|
|
152
|
+
temp[key] = lst
|
|
153
|
+
return lst # type: ignore[return-value]
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _resolve_from_pointer(
|
|
157
|
+
source: Any, pv: Mapping[str, Dict[str, Any]], field: str
|
|
158
|
+
) -> Optional[Any]:
|
|
159
|
+
"""Resolve ('paired_values', field, 'raw') pointer, with fallback to pv[field]['raw']."""
|
|
160
|
+
if isinstance(source, (tuple, list)) and len(source) == 3:
|
|
161
|
+
base, fld, key = source
|
|
162
|
+
if base == "paired_values" and isinstance(fld, str) and key == "raw":
|
|
163
|
+
return pv.get(fld, {}).get("raw")
|
|
164
|
+
return pv.get(field, {}).get("raw")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
__all__ = ["ANCHOR", "run"]
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# tigrbl/v3/runtime/atoms/wire/__init__.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Any, Callable, Dict, Optional, Tuple
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
# Atom implementations (per-field)
|
|
8
|
+
from . import build_in as _build_in
|
|
9
|
+
from . import validate_in as _validate_in
|
|
10
|
+
from . import build_out as _build_out
|
|
11
|
+
from . import dump as _dump
|
|
12
|
+
|
|
13
|
+
# Runner signature: (obj|None, ctx) -> None
|
|
14
|
+
RunFn = Callable[[Optional[object], Any], None]
|
|
15
|
+
|
|
16
|
+
#: Domain-scoped registry consumed by the kernel plan (and aggregated at atoms/__init__.py).
|
|
17
|
+
#: Keys are (domain, subject); values are (anchor, runner).
|
|
18
|
+
#: Canonical subjects mirror filenames; we keep "validate_in" (not "validate") to avoid duplicates.
|
|
19
|
+
REGISTRY: Dict[Tuple[str, str], Tuple[str, RunFn]] = {
|
|
20
|
+
("wire", "build_in"): (_build_in.ANCHOR, _build_in.run),
|
|
21
|
+
("wire", "validate_in"): (_validate_in.ANCHOR, _validate_in.run),
|
|
22
|
+
("wire", "build_out"): (_build_out.ANCHOR, _build_out.run),
|
|
23
|
+
("wire", "dump"): (_dump.ANCHOR, _dump.run),
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger("uvicorn")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def subjects() -> Tuple[str, ...]:
|
|
30
|
+
"""Return the subject names exported by this domain."""
|
|
31
|
+
subjects = tuple(s for (_, s) in REGISTRY.keys())
|
|
32
|
+
logger.debug("Listing 'wire' subjects: %s", subjects)
|
|
33
|
+
return subjects
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def get(subject: str) -> Tuple[str, RunFn]:
|
|
37
|
+
"""Return (anchor, runner) for a subject in the 'wire' domain."""
|
|
38
|
+
key = ("wire", subject)
|
|
39
|
+
if key not in REGISTRY:
|
|
40
|
+
raise KeyError(f"Unknown wire atom subject: {subject!r}")
|
|
41
|
+
logger.debug("Retrieving 'wire' subject %s", subject)
|
|
42
|
+
return REGISTRY[key]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
__all__ = ["REGISTRY", "RunFn", "subjects", "get"]
|