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,150 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, Iterable, List, Mapping, Optional
|
|
4
|
+
import logging
|
|
5
|
+
|
|
6
|
+
logger = logging.getLogger(__name__)
|
|
7
|
+
|
|
8
|
+
# Prefer FastAPI HTTPException/status; fall back to Starlette; finally a tiny shim.
|
|
9
|
+
try: # FastAPI present
|
|
10
|
+
from fastapi import HTTPException, status
|
|
11
|
+
except Exception: # pragma: no cover
|
|
12
|
+
try: # Starlette present
|
|
13
|
+
from starlette.exceptions import HTTPException # type: ignore
|
|
14
|
+
from starlette import status # type: ignore
|
|
15
|
+
except Exception: # pragma: no cover
|
|
16
|
+
|
|
17
|
+
class HTTPException(Exception): # minimal shim
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
status_code: int,
|
|
21
|
+
detail: Any = None,
|
|
22
|
+
headers: Optional[dict] = None,
|
|
23
|
+
) -> None:
|
|
24
|
+
super().__init__(detail)
|
|
25
|
+
self.status_code = status_code
|
|
26
|
+
self.detail = detail
|
|
27
|
+
self.headers = headers
|
|
28
|
+
|
|
29
|
+
class _Status:
|
|
30
|
+
HTTP_400_BAD_REQUEST = 400
|
|
31
|
+
HTTP_401_UNAUTHORIZED = 401
|
|
32
|
+
HTTP_403_FORBIDDEN = 403
|
|
33
|
+
HTTP_404_NOT_FOUND = 404
|
|
34
|
+
HTTP_409_CONFLICT = 409
|
|
35
|
+
HTTP_422_UNPROCESSABLE_ENTITY = 422
|
|
36
|
+
HTTP_500_INTERNAL_SERVER_ERROR = 500
|
|
37
|
+
HTTP_501_NOT_IMPLEMENTED = 501
|
|
38
|
+
HTTP_503_SERVICE_UNAVAILABLE = 503
|
|
39
|
+
HTTP_504_GATEWAY_TIMEOUT = 504
|
|
40
|
+
|
|
41
|
+
status = _Status() # type: ignore
|
|
42
|
+
|
|
43
|
+
# Optional imports – code must run even if these packages aren’t installed.
|
|
44
|
+
try:
|
|
45
|
+
from pydantic import ValidationError as PydanticValidationError # v2
|
|
46
|
+
except Exception: # pragma: no cover
|
|
47
|
+
PydanticValidationError = None # type: ignore
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
from fastapi.exceptions import (
|
|
51
|
+
RequestValidationError,
|
|
52
|
+
) # emitted by FastAPI input validation
|
|
53
|
+
except Exception: # pragma: no cover
|
|
54
|
+
RequestValidationError = None # type: ignore
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
# SQLAlchemy v1/v2 exception sets
|
|
58
|
+
from sqlalchemy.exc import IntegrityError, DBAPIError, OperationalError
|
|
59
|
+
from sqlalchemy.orm.exc import NoResultFound # type: ignore
|
|
60
|
+
except Exception: # pragma: no cover
|
|
61
|
+
IntegrityError = DBAPIError = OperationalError = NoResultFound = None # type: ignore
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# Detect asyncpg constraint errors without importing asyncpg (optional dep).
|
|
65
|
+
_ASYNCPG_CONSTRAINT_NAMES = {
|
|
66
|
+
"UniqueViolationError",
|
|
67
|
+
"ForeignKeyViolationError",
|
|
68
|
+
"NotNullViolationError",
|
|
69
|
+
"CheckViolationError",
|
|
70
|
+
"ExclusionViolationError",
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _is_asyncpg_constraint_error(exc: BaseException) -> bool:
|
|
75
|
+
cls = type(exc)
|
|
76
|
+
return (cls.__module__ or "").startswith("asyncpg") and (
|
|
77
|
+
cls.__name__ in _ASYNCPG_CONSTRAINT_NAMES
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _limit(s: str, n: int = 4000) -> str:
|
|
82
|
+
return s if len(s) <= n else s[: n - 3] + "..."
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _stringify_exc(exc: BaseException) -> str:
|
|
86
|
+
detail = getattr(exc, "detail", None)
|
|
87
|
+
if detail:
|
|
88
|
+
return _limit(str(detail))
|
|
89
|
+
return _limit(f"{exc.__class__!r}: {str(exc) or repr(exc)}")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _format_validation(err: Any) -> Any:
|
|
93
|
+
try:
|
|
94
|
+
items = err.errors() # pydantic / fastapi RequestValidationError
|
|
95
|
+
if isinstance(items, Iterable):
|
|
96
|
+
return list(items)
|
|
97
|
+
except Exception: # pragma: no cover
|
|
98
|
+
pass
|
|
99
|
+
return _limit(str(err))
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _get_temp(ctx: Any) -> Mapping[str, Any]:
|
|
103
|
+
tmp = getattr(ctx, "temp", None)
|
|
104
|
+
return tmp if isinstance(tmp, Mapping) else {}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _has_in_errors(ctx: Any) -> bool:
|
|
108
|
+
tmp = _get_temp(ctx)
|
|
109
|
+
if tmp.get("in_invalid") is True:
|
|
110
|
+
return True
|
|
111
|
+
errs = tmp.get("in_errors")
|
|
112
|
+
return isinstance(errs, (list, tuple)) and len(errs) > 0
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _read_in_errors(ctx: Any) -> List[Dict[str, Any]]:
|
|
116
|
+
tmp = _get_temp(ctx)
|
|
117
|
+
errs = tmp.get("in_errors")
|
|
118
|
+
if isinstance(errs, list):
|
|
119
|
+
norm: List[Dict[str, Any]] = []
|
|
120
|
+
for e in errs:
|
|
121
|
+
if isinstance(e, Mapping):
|
|
122
|
+
field = e.get("field")
|
|
123
|
+
code = e.get("code") or "invalid"
|
|
124
|
+
msg = e.get("message") or "Invalid value."
|
|
125
|
+
entry = {"field": field, "code": code, "message": msg}
|
|
126
|
+
for k, v in e.items():
|
|
127
|
+
if k not in entry:
|
|
128
|
+
entry[k] = v
|
|
129
|
+
norm.append(entry)
|
|
130
|
+
return norm
|
|
131
|
+
return []
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
__all__ = [
|
|
135
|
+
"HTTPException",
|
|
136
|
+
"status",
|
|
137
|
+
"PydanticValidationError",
|
|
138
|
+
"RequestValidationError",
|
|
139
|
+
"IntegrityError",
|
|
140
|
+
"DBAPIError",
|
|
141
|
+
"OperationalError",
|
|
142
|
+
"NoResultFound",
|
|
143
|
+
"_is_asyncpg_constraint_error",
|
|
144
|
+
"_limit",
|
|
145
|
+
"_stringify_exc",
|
|
146
|
+
"_format_validation",
|
|
147
|
+
"_get_temp",
|
|
148
|
+
"_has_in_errors",
|
|
149
|
+
"_read_in_errors",
|
|
150
|
+
]
|
tigrbl/runtime/events.py
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# tigrbl/v3/runtime/events.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Dict, Iterable, List, Literal, Tuple
|
|
6
|
+
|
|
7
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
8
|
+
# Phases
|
|
9
|
+
# - PRE_TX is a synthetic phase for security/dependency checks.
|
|
10
|
+
# - START_TX / END_TX are reserved for system steps (no atom anchors there).
|
|
11
|
+
# - Atoms bind only to the event anchors below.
|
|
12
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
Phase = Literal[
|
|
15
|
+
"PRE_TX",
|
|
16
|
+
"START_TX",
|
|
17
|
+
"PRE_HANDLER",
|
|
18
|
+
"HANDLER",
|
|
19
|
+
"POST_HANDLER",
|
|
20
|
+
"END_TX",
|
|
21
|
+
"POST_RESPONSE",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
PHASES: Tuple[Phase, ...] = (
|
|
25
|
+
"PRE_TX",
|
|
26
|
+
"START_TX", # system-only
|
|
27
|
+
"PRE_HANDLER",
|
|
28
|
+
"HANDLER",
|
|
29
|
+
"POST_HANDLER",
|
|
30
|
+
"END_TX", # system-only
|
|
31
|
+
"POST_RESPONSE",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
35
|
+
# Canonical anchors (events) — the only moments atoms can bind to
|
|
36
|
+
# Keep these names stable; labels use them directly: step_kind:domain:subject@ANCHOR
|
|
37
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
# PRE_HANDLER
|
|
40
|
+
SCHEMA_COLLECT_IN = "schema:collect_in"
|
|
41
|
+
IN_VALIDATE = "in:validate"
|
|
42
|
+
|
|
43
|
+
# HANDLER
|
|
44
|
+
RESOLVE_VALUES = "resolve:values"
|
|
45
|
+
PRE_FLUSH = "pre:flush"
|
|
46
|
+
EMIT_ALIASES_PRE = "emit:aliases:pre_flush"
|
|
47
|
+
|
|
48
|
+
# POST_HANDLER
|
|
49
|
+
POST_FLUSH = "post:flush"
|
|
50
|
+
EMIT_ALIASES_POST = "emit:aliases:post_refresh"
|
|
51
|
+
SCHEMA_COLLECT_OUT = "schema:collect_out"
|
|
52
|
+
OUT_BUILD = "out:build"
|
|
53
|
+
|
|
54
|
+
# POST_RESPONSE
|
|
55
|
+
EMIT_ALIASES_READ = "emit:aliases:readtime"
|
|
56
|
+
OUT_DUMP = "out:dump"
|
|
57
|
+
|
|
58
|
+
# Canonical order of event anchors within the request lifecycle.
|
|
59
|
+
# This ordering is global and stable; use it to produce deterministic plans/traces.
|
|
60
|
+
_EVENT_ORDER: Tuple[str, ...] = (
|
|
61
|
+
# PRE_HANDLER
|
|
62
|
+
SCHEMA_COLLECT_IN,
|
|
63
|
+
IN_VALIDATE,
|
|
64
|
+
# HANDLER
|
|
65
|
+
RESOLVE_VALUES,
|
|
66
|
+
PRE_FLUSH,
|
|
67
|
+
EMIT_ALIASES_PRE,
|
|
68
|
+
# POST_HANDLER
|
|
69
|
+
POST_FLUSH,
|
|
70
|
+
EMIT_ALIASES_POST,
|
|
71
|
+
SCHEMA_COLLECT_OUT,
|
|
72
|
+
OUT_BUILD,
|
|
73
|
+
# POST_RESPONSE
|
|
74
|
+
EMIT_ALIASES_READ,
|
|
75
|
+
OUT_DUMP,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# Map each anchor to its phase and persistence tie.
|
|
80
|
+
# "persist_tied=True" means the anchor is pruned for non-persisting ops
|
|
81
|
+
# (e.g., read/list) and whenever an op is executed with persist=False.
|
|
82
|
+
@dataclass(frozen=True)
|
|
83
|
+
class AnchorInfo:
|
|
84
|
+
name: str
|
|
85
|
+
phase: Phase
|
|
86
|
+
ordinal: int
|
|
87
|
+
persist_tied: bool
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
_ANCHORS: Dict[str, AnchorInfo] = {
|
|
91
|
+
# PRE_HANDLER (not persist-tied)
|
|
92
|
+
SCHEMA_COLLECT_IN: AnchorInfo(SCHEMA_COLLECT_IN, "PRE_HANDLER", 0, False),
|
|
93
|
+
IN_VALIDATE: AnchorInfo(IN_VALIDATE, "PRE_HANDLER", 1, False),
|
|
94
|
+
RESOLVE_VALUES: AnchorInfo(RESOLVE_VALUES, "PRE_HANDLER", 2, True),
|
|
95
|
+
PRE_FLUSH: AnchorInfo(PRE_FLUSH, "PRE_HANDLER", 3, True),
|
|
96
|
+
EMIT_ALIASES_PRE: AnchorInfo(EMIT_ALIASES_PRE, "PRE_HANDLER", 4, True),
|
|
97
|
+
# POST_HANDLER (mixed)
|
|
98
|
+
POST_FLUSH: AnchorInfo(POST_FLUSH, "POST_HANDLER", 5, True),
|
|
99
|
+
EMIT_ALIASES_POST: AnchorInfo(EMIT_ALIASES_POST, "POST_HANDLER", 6, True),
|
|
100
|
+
SCHEMA_COLLECT_OUT: AnchorInfo(SCHEMA_COLLECT_OUT, "POST_HANDLER", 7, False),
|
|
101
|
+
OUT_BUILD: AnchorInfo(OUT_BUILD, "POST_HANDLER", 8, False),
|
|
102
|
+
# POST_RESPONSE (not persist-tied)
|
|
103
|
+
EMIT_ALIASES_READ: AnchorInfo(EMIT_ALIASES_READ, "POST_RESPONSE", 9, False),
|
|
104
|
+
OUT_DUMP: AnchorInfo(OUT_DUMP, "POST_RESPONSE", 10, False),
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
108
|
+
# Public helpers
|
|
109
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def is_valid_event(anchor: str) -> bool:
|
|
113
|
+
"""True if the given anchor is one of the canonical events."""
|
|
114
|
+
return anchor in _ANCHORS
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def phase_for_event(anchor: str) -> Phase:
|
|
118
|
+
"""Return the Phase for a canonical event; raises on unknown anchors."""
|
|
119
|
+
try:
|
|
120
|
+
return _ANCHORS[anchor].phase
|
|
121
|
+
except KeyError as e:
|
|
122
|
+
raise ValueError(f"Unknown event anchor: {anchor!r}") from e
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def is_persist_tied(anchor: str) -> bool:
|
|
126
|
+
"""Return True if this event is pruned for non-persisting ops."""
|
|
127
|
+
try:
|
|
128
|
+
return _ANCHORS[anchor].persist_tied
|
|
129
|
+
except KeyError as e:
|
|
130
|
+
raise ValueError(f"Unknown event anchor: {anchor!r}") from e
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def get_anchor_info(anchor: str) -> AnchorInfo:
|
|
134
|
+
"""Return the full :class:`AnchorInfo` for a canonical event."""
|
|
135
|
+
try:
|
|
136
|
+
return _ANCHORS[anchor]
|
|
137
|
+
except KeyError as e:
|
|
138
|
+
raise ValueError(f"Unknown event anchor: {anchor!r}") from e
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def all_events_ordered() -> List[str]:
|
|
142
|
+
"""Return all canonical events in deterministic, lifecycle order."""
|
|
143
|
+
return list(_EVENT_ORDER)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def events_for_phase(phase: Phase) -> List[str]:
|
|
147
|
+
"""Return the subset of events that belong to the given phase."""
|
|
148
|
+
if phase not in PHASES:
|
|
149
|
+
raise ValueError(f"Unknown phase: {phase!r}")
|
|
150
|
+
return [a for a, info in _ANCHORS.items() if info.phase == phase]
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def prune_events_for_persist(anchors: Iterable[str], *, persist: bool) -> List[str]:
|
|
154
|
+
"""
|
|
155
|
+
Given a sequence of anchors, return a new list with persistence-tied events
|
|
156
|
+
removed when persist=False. Unknown anchors raise ValueError.
|
|
157
|
+
"""
|
|
158
|
+
out: List[str] = []
|
|
159
|
+
for a in anchors:
|
|
160
|
+
if not is_valid_event(a):
|
|
161
|
+
raise ValueError(f"Unknown event anchor: {a!r}")
|
|
162
|
+
if not persist and _ANCHORS[a].persist_tied:
|
|
163
|
+
continue
|
|
164
|
+
out.append(a)
|
|
165
|
+
# keep canonical order irrespective of input order
|
|
166
|
+
out.sort(key=lambda x: _ANCHORS[x].ordinal)
|
|
167
|
+
return out
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def order_events(anchors: Iterable[str]) -> List[str]:
|
|
171
|
+
"""
|
|
172
|
+
Sort a set/list of anchors into canonical lifecycle order.
|
|
173
|
+
Raises on unknown anchors.
|
|
174
|
+
"""
|
|
175
|
+
anchors = list(anchors)
|
|
176
|
+
for a in anchors:
|
|
177
|
+
if a not in _ANCHORS:
|
|
178
|
+
raise ValueError(f"Unknown event anchor: {a!r}")
|
|
179
|
+
anchors.sort(key=lambda x: _ANCHORS[x].ordinal)
|
|
180
|
+
return anchors
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
__all__ = [
|
|
184
|
+
# Phases
|
|
185
|
+
"Phase",
|
|
186
|
+
"PHASES",
|
|
187
|
+
# Anchors (constants)
|
|
188
|
+
"SCHEMA_COLLECT_IN",
|
|
189
|
+
"IN_VALIDATE",
|
|
190
|
+
"RESOLVE_VALUES",
|
|
191
|
+
"PRE_FLUSH",
|
|
192
|
+
"EMIT_ALIASES_PRE",
|
|
193
|
+
"POST_FLUSH",
|
|
194
|
+
"EMIT_ALIASES_POST",
|
|
195
|
+
"SCHEMA_COLLECT_OUT",
|
|
196
|
+
"OUT_BUILD",
|
|
197
|
+
"EMIT_ALIASES_READ",
|
|
198
|
+
"OUT_DUMP",
|
|
199
|
+
# Types / helpers
|
|
200
|
+
"AnchorInfo",
|
|
201
|
+
"is_valid_event",
|
|
202
|
+
"phase_for_event",
|
|
203
|
+
"is_persist_tied",
|
|
204
|
+
"get_anchor_info",
|
|
205
|
+
"all_events_ordered",
|
|
206
|
+
"events_for_phase",
|
|
207
|
+
"prune_events_for_persist",
|
|
208
|
+
"order_events",
|
|
209
|
+
]
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Database session guard utilities for the runtime executor.
|
|
2
|
+
|
|
3
|
+
This module temporarily replaces ``commit`` and ``flush`` on SQLAlchemy
|
|
4
|
+
sessions to enforce phase-specific policies. Each guard returns a handle that
|
|
5
|
+
restores the original methods once the phase completes and provides helpers to
|
|
6
|
+
rollback when the runtime owns the transaction.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
from typing import Any, Optional, Union
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
from sqlalchemy.orm import Session # type: ignore
|
|
16
|
+
from sqlalchemy.ext.asyncio import AsyncSession # type: ignore
|
|
17
|
+
except Exception: # pragma: no cover
|
|
18
|
+
Session = Any # type: ignore
|
|
19
|
+
AsyncSession = Any # type: ignore
|
|
20
|
+
|
|
21
|
+
from .types import _Ctx, PhaseChains
|
|
22
|
+
from .helpers import _is_async_db, _run_chain, _g
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class _GuardHandle:
|
|
28
|
+
"""Stores original ``commit``/``flush`` methods for later restoration."""
|
|
29
|
+
|
|
30
|
+
__slots__ = ("db", "orig_commit", "orig_flush")
|
|
31
|
+
|
|
32
|
+
def __init__(self, db: Any, orig_commit: Any, orig_flush: Any) -> None:
|
|
33
|
+
self.db = db
|
|
34
|
+
self.orig_commit = orig_commit
|
|
35
|
+
self.orig_flush = orig_flush
|
|
36
|
+
|
|
37
|
+
def restore(self) -> None:
|
|
38
|
+
if self.orig_commit is not None:
|
|
39
|
+
try:
|
|
40
|
+
setattr(self.db, "commit", self.orig_commit)
|
|
41
|
+
except Exception:
|
|
42
|
+
pass # pragma: no cover
|
|
43
|
+
if self.orig_flush is not None:
|
|
44
|
+
try:
|
|
45
|
+
setattr(self.db, "flush", self.orig_flush)
|
|
46
|
+
except Exception:
|
|
47
|
+
pass # pragma: no cover
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _install_db_guards(
|
|
51
|
+
db: Union[Session, AsyncSession, None],
|
|
52
|
+
*,
|
|
53
|
+
phase: str,
|
|
54
|
+
allow_flush: bool,
|
|
55
|
+
allow_commit: bool,
|
|
56
|
+
require_owned_tx_for_commit: bool,
|
|
57
|
+
owns_tx: bool,
|
|
58
|
+
) -> _GuardHandle:
|
|
59
|
+
"""Install guards that restrict ``commit``/``flush`` during a phase.
|
|
60
|
+
|
|
61
|
+
Parameters:
|
|
62
|
+
db: SQLAlchemy ``Session``/``AsyncSession`` to guard.
|
|
63
|
+
phase: Name of the executing phase for error messages.
|
|
64
|
+
allow_flush: Whether ``flush`` should be permitted.
|
|
65
|
+
allow_commit: Whether ``commit`` should be permitted.
|
|
66
|
+
require_owned_tx_for_commit: Block commits if the executor did not
|
|
67
|
+
open the transaction.
|
|
68
|
+
owns_tx: Whether the runtime opened the transaction.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
A ``_GuardHandle`` for restoring original methods.
|
|
72
|
+
"""
|
|
73
|
+
if db is None:
|
|
74
|
+
return _GuardHandle(None, None, None)
|
|
75
|
+
orig_commit = getattr(db, "commit", None)
|
|
76
|
+
orig_flush = getattr(db, "flush", None)
|
|
77
|
+
|
|
78
|
+
def _raise(op: str) -> None:
|
|
79
|
+
raise RuntimeError(f"db.{op}() is not allowed during {phase} phase")
|
|
80
|
+
|
|
81
|
+
if not allow_commit or (require_owned_tx_for_commit and not owns_tx):
|
|
82
|
+
if _is_async_db(db):
|
|
83
|
+
|
|
84
|
+
async def _blocked_commit() -> None: # type: ignore[func-returns-value]
|
|
85
|
+
_raise("commit")
|
|
86
|
+
else:
|
|
87
|
+
|
|
88
|
+
def _blocked_commit() -> None: # type: ignore[func-returns-value]
|
|
89
|
+
_raise("commit")
|
|
90
|
+
|
|
91
|
+
setattr(db, "commit", _blocked_commit) # type: ignore[assignment]
|
|
92
|
+
|
|
93
|
+
if not allow_flush:
|
|
94
|
+
if _is_async_db(db):
|
|
95
|
+
|
|
96
|
+
async def _blocked_flush() -> None: # type: ignore[func-returns-value]
|
|
97
|
+
_raise("flush")
|
|
98
|
+
else:
|
|
99
|
+
|
|
100
|
+
def _blocked_flush() -> None: # type: ignore[func-returns-value]
|
|
101
|
+
_raise("flush")
|
|
102
|
+
|
|
103
|
+
setattr(db, "flush", _blocked_flush) # type: ignore[assignment]
|
|
104
|
+
|
|
105
|
+
return _GuardHandle(db, orig_commit, orig_flush)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
async def _rollback_if_owned(
|
|
109
|
+
db: Union[Session, AsyncSession, None],
|
|
110
|
+
owns_tx: bool,
|
|
111
|
+
*,
|
|
112
|
+
phases: Optional[PhaseChains],
|
|
113
|
+
ctx: _Ctx,
|
|
114
|
+
) -> None:
|
|
115
|
+
"""Rollback the session if this runtime owns the transaction."""
|
|
116
|
+
|
|
117
|
+
if not owns_tx or db is None:
|
|
118
|
+
return
|
|
119
|
+
try:
|
|
120
|
+
if _is_async_db(db):
|
|
121
|
+
await db.rollback() # type: ignore[func-returns-value]
|
|
122
|
+
else:
|
|
123
|
+
db.rollback()
|
|
124
|
+
except Exception as rb_exc: # pragma: no cover
|
|
125
|
+
logger.exception("Rollback failed: %s", rb_exc)
|
|
126
|
+
try:
|
|
127
|
+
await _run_chain(ctx, _g(phases, "ON_ROLLBACK"), phase="ON_ROLLBACK")
|
|
128
|
+
except Exception: # pragma: no cover
|
|
129
|
+
pass
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
__all__ = ["_GuardHandle", "_install_db_guards", "_rollback_if_owned"]
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# tigrbl/v3/runtime/executor/helpers.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import inspect
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Any, Iterable, Optional, Sequence
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
from sqlalchemy.ext.asyncio import AsyncSession # type: ignore
|
|
10
|
+
except Exception: # pragma: no cover
|
|
11
|
+
AsyncSession = Any # type: ignore
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
from .. import trace as _trace # type: ignore
|
|
15
|
+
except Exception: # pragma: no cover
|
|
16
|
+
_trace = None # type: ignore
|
|
17
|
+
|
|
18
|
+
from .types import _Ctx, HandlerStep, PhaseChains
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _is_async_db(db: Any) -> bool:
|
|
24
|
+
"""Detect DB interfaces that require `await` for transactional methods."""
|
|
25
|
+
if isinstance(db, AsyncSession) or hasattr(db, "run_sync"):
|
|
26
|
+
return True
|
|
27
|
+
for attr in ("commit", "begin", "rollback", "flush"):
|
|
28
|
+
if inspect.iscoroutinefunction(getattr(db, attr, None)):
|
|
29
|
+
return True
|
|
30
|
+
return False
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _bool_call(meth: Any) -> bool:
|
|
34
|
+
try:
|
|
35
|
+
return bool(meth())
|
|
36
|
+
except Exception: # pragma: no cover
|
|
37
|
+
return False
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _in_tx(db: Any) -> bool:
|
|
41
|
+
for name in ("in_transaction", "in_nested_transaction"):
|
|
42
|
+
attr = getattr(db, name, None)
|
|
43
|
+
if callable(attr):
|
|
44
|
+
if _bool_call(attr):
|
|
45
|
+
return True
|
|
46
|
+
elif attr:
|
|
47
|
+
return True
|
|
48
|
+
return False
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
async def _maybe_await(v: Any) -> Any:
|
|
52
|
+
if inspect.isawaitable(v):
|
|
53
|
+
return await v # type: ignore[func-returns-value]
|
|
54
|
+
return v
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
async def _run_chain(
|
|
58
|
+
ctx: _Ctx, chain: Optional[Iterable[HandlerStep]], *, phase: str
|
|
59
|
+
) -> None:
|
|
60
|
+
if not chain:
|
|
61
|
+
return
|
|
62
|
+
if _trace is not None:
|
|
63
|
+
with _trace.span(ctx, f"phase:{phase}"):
|
|
64
|
+
for step in chain:
|
|
65
|
+
rv = step(ctx)
|
|
66
|
+
rv = await _maybe_await(rv)
|
|
67
|
+
if rv is not None:
|
|
68
|
+
ctx.result = rv
|
|
69
|
+
return
|
|
70
|
+
for step in chain:
|
|
71
|
+
rv = step(ctx)
|
|
72
|
+
rv = await _maybe_await(rv)
|
|
73
|
+
if rv is not None:
|
|
74
|
+
ctx.result = rv
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _g(phases: Optional[PhaseChains], key: str) -> Sequence[HandlerStep]:
|
|
78
|
+
return () if not phases else phases.get(key, ())
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
__all__ = [
|
|
82
|
+
"_is_async_db",
|
|
83
|
+
"_bool_call",
|
|
84
|
+
"_in_tx",
|
|
85
|
+
"_maybe_await",
|
|
86
|
+
"_run_chain",
|
|
87
|
+
"_g",
|
|
88
|
+
]
|