tigrbl 0.0.1.dev1__py3-none-any.whl → 0.3.0.dev3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tigrbl/README.md +94 -0
- tigrbl/__init__.py +139 -14
- tigrbl/api/__init__.py +6 -0
- tigrbl/api/_api.py +72 -0
- tigrbl/api/api_spec.py +30 -0
- tigrbl/api/mro_collect.py +43 -0
- tigrbl/api/shortcuts.py +56 -0
- tigrbl/api/tigrbl_api.py +286 -0
- tigrbl/app/__init__.py +0 -0
- tigrbl/app/_app.py +61 -0
- tigrbl/app/app_spec.py +42 -0
- tigrbl/app/mro_collect.py +67 -0
- tigrbl/app/shortcuts.py +65 -0
- tigrbl/app/tigrbl_app.py +314 -0
- tigrbl/bindings/__init__.py +73 -0
- tigrbl/bindings/api/__init__.py +12 -0
- tigrbl/bindings/api/common.py +109 -0
- tigrbl/bindings/api/include.py +256 -0
- tigrbl/bindings/api/resource_proxy.py +149 -0
- tigrbl/bindings/api/rpc.py +111 -0
- tigrbl/bindings/columns.py +49 -0
- tigrbl/bindings/handlers/__init__.py +11 -0
- tigrbl/bindings/handlers/builder.py +119 -0
- tigrbl/bindings/handlers/ctx.py +74 -0
- tigrbl/bindings/handlers/identifiers.py +228 -0
- tigrbl/bindings/handlers/namespaces.py +51 -0
- tigrbl/bindings/handlers/steps.py +276 -0
- tigrbl/bindings/hooks.py +311 -0
- tigrbl/bindings/model.py +194 -0
- tigrbl/bindings/model_helpers.py +139 -0
- tigrbl/bindings/model_registry.py +77 -0
- tigrbl/bindings/rest/__init__.py +7 -0
- tigrbl/bindings/rest/attach.py +34 -0
- tigrbl/bindings/rest/collection.py +265 -0
- tigrbl/bindings/rest/common.py +116 -0
- tigrbl/bindings/rest/fastapi.py +76 -0
- tigrbl/bindings/rest/helpers.py +119 -0
- tigrbl/bindings/rest/io.py +317 -0
- tigrbl/bindings/rest/member.py +367 -0
- tigrbl/bindings/rest/router.py +292 -0
- tigrbl/bindings/rest/routing.py +133 -0
- tigrbl/bindings/rpc.py +364 -0
- tigrbl/bindings/schemas/__init__.py +11 -0
- tigrbl/bindings/schemas/builder.py +348 -0
- tigrbl/bindings/schemas/defaults.py +260 -0
- tigrbl/bindings/schemas/utils.py +193 -0
- tigrbl/column/README.md +62 -0
- tigrbl/column/__init__.py +72 -0
- tigrbl/column/_column.py +96 -0
- tigrbl/column/column_spec.py +40 -0
- tigrbl/column/field_spec.py +31 -0
- tigrbl/column/infer/__init__.py +25 -0
- tigrbl/column/infer/core.py +92 -0
- tigrbl/column/infer/jsonhints.py +44 -0
- tigrbl/column/infer/planning.py +133 -0
- tigrbl/column/infer/types.py +102 -0
- tigrbl/column/infer/utils.py +59 -0
- tigrbl/column/io_spec.py +133 -0
- tigrbl/column/mro_collect.py +59 -0
- tigrbl/column/shortcuts.py +89 -0
- tigrbl/column/storage_spec.py +65 -0
- tigrbl/config/__init__.py +19 -0
- tigrbl/config/constants.py +224 -0
- tigrbl/config/defaults.py +29 -0
- tigrbl/config/resolver.py +295 -0
- tigrbl/core/__init__.py +47 -0
- tigrbl/core/crud/__init__.py +36 -0
- tigrbl/core/crud/bulk.py +168 -0
- tigrbl/core/crud/helpers/__init__.py +76 -0
- tigrbl/core/crud/helpers/db.py +92 -0
- tigrbl/core/crud/helpers/enum.py +86 -0
- tigrbl/core/crud/helpers/filters.py +162 -0
- tigrbl/core/crud/helpers/model.py +123 -0
- tigrbl/core/crud/helpers/normalize.py +99 -0
- tigrbl/core/crud/ops.py +235 -0
- tigrbl/ddl/__init__.py +344 -0
- tigrbl/decorators.py +17 -0
- tigrbl/deps/__init__.py +20 -0
- tigrbl/deps/fastapi.py +45 -0
- tigrbl/deps/favicon.svg +4 -0
- tigrbl/deps/jinja.py +27 -0
- tigrbl/deps/pydantic.py +10 -0
- tigrbl/deps/sqlalchemy.py +94 -0
- tigrbl/deps/starlette.py +36 -0
- tigrbl/engine/__init__.py +26 -0
- tigrbl/engine/_engine.py +130 -0
- tigrbl/engine/bind.py +33 -0
- tigrbl/engine/builders.py +236 -0
- tigrbl/engine/collect.py +111 -0
- tigrbl/engine/decorators.py +108 -0
- tigrbl/engine/engine_spec.py +261 -0
- tigrbl/engine/resolver.py +224 -0
- tigrbl/engine/shortcuts.py +216 -0
- tigrbl/hook/__init__.py +21 -0
- tigrbl/hook/_hook.py +22 -0
- tigrbl/hook/decorators.py +28 -0
- tigrbl/hook/hook_spec.py +24 -0
- tigrbl/hook/mro_collect.py +98 -0
- tigrbl/hook/shortcuts.py +44 -0
- tigrbl/hook/types.py +76 -0
- tigrbl/op/__init__.py +50 -0
- tigrbl/op/_op.py +31 -0
- tigrbl/op/canonical.py +31 -0
- tigrbl/op/collect.py +11 -0
- tigrbl/op/decorators.py +238 -0
- tigrbl/op/model_registry.py +301 -0
- tigrbl/op/mro_collect.py +99 -0
- tigrbl/op/resolver.py +216 -0
- tigrbl/op/types.py +136 -0
- tigrbl/orm/__init__.py +1 -0
- tigrbl/orm/mixins/_RowBound.py +83 -0
- tigrbl/orm/mixins/__init__.py +95 -0
- tigrbl/orm/mixins/bootstrappable.py +113 -0
- tigrbl/orm/mixins/bound.py +47 -0
- tigrbl/orm/mixins/edges.py +40 -0
- tigrbl/orm/mixins/fields.py +165 -0
- tigrbl/orm/mixins/hierarchy.py +54 -0
- tigrbl/orm/mixins/key_digest.py +44 -0
- tigrbl/orm/mixins/lifecycle.py +115 -0
- tigrbl/orm/mixins/locks.py +51 -0
- tigrbl/orm/mixins/markers.py +16 -0
- tigrbl/orm/mixins/operations.py +57 -0
- tigrbl/orm/mixins/ownable.py +337 -0
- tigrbl/orm/mixins/principals.py +98 -0
- tigrbl/orm/mixins/tenant_bound.py +301 -0
- tigrbl/orm/mixins/upsertable.py +111 -0
- tigrbl/orm/mixins/utils.py +49 -0
- tigrbl/orm/tables/__init__.py +72 -0
- tigrbl/orm/tables/_base.py +8 -0
- tigrbl/orm/tables/audit.py +56 -0
- tigrbl/orm/tables/client.py +25 -0
- tigrbl/orm/tables/group.py +29 -0
- tigrbl/orm/tables/org.py +30 -0
- tigrbl/orm/tables/rbac.py +76 -0
- tigrbl/orm/tables/status.py +106 -0
- tigrbl/orm/tables/tenant.py +22 -0
- tigrbl/orm/tables/user.py +39 -0
- tigrbl/response/README.md +34 -0
- tigrbl/response/__init__.py +33 -0
- tigrbl/response/bind.py +12 -0
- tigrbl/response/decorators.py +37 -0
- tigrbl/response/resolver.py +83 -0
- tigrbl/response/shortcuts.py +144 -0
- tigrbl/response/types.py +49 -0
- tigrbl/rest/__init__.py +27 -0
- tigrbl/runtime/README.md +129 -0
- tigrbl/runtime/__init__.py +20 -0
- tigrbl/runtime/atoms/__init__.py +102 -0
- tigrbl/runtime/atoms/emit/__init__.py +42 -0
- tigrbl/runtime/atoms/emit/paired_post.py +158 -0
- tigrbl/runtime/atoms/emit/paired_pre.py +106 -0
- tigrbl/runtime/atoms/emit/readtime_alias.py +120 -0
- tigrbl/runtime/atoms/out/__init__.py +38 -0
- tigrbl/runtime/atoms/out/masking.py +135 -0
- tigrbl/runtime/atoms/refresh/__init__.py +38 -0
- tigrbl/runtime/atoms/refresh/demand.py +130 -0
- tigrbl/runtime/atoms/resolve/__init__.py +40 -0
- tigrbl/runtime/atoms/resolve/assemble.py +167 -0
- tigrbl/runtime/atoms/resolve/paired_gen.py +147 -0
- tigrbl/runtime/atoms/response/__init__.py +17 -0
- tigrbl/runtime/atoms/response/negotiate.py +30 -0
- tigrbl/runtime/atoms/response/negotiation.py +43 -0
- tigrbl/runtime/atoms/response/render.py +36 -0
- tigrbl/runtime/atoms/response/renderer.py +116 -0
- tigrbl/runtime/atoms/response/template.py +44 -0
- tigrbl/runtime/atoms/response/templates.py +88 -0
- tigrbl/runtime/atoms/schema/__init__.py +40 -0
- tigrbl/runtime/atoms/schema/collect_in.py +21 -0
- tigrbl/runtime/atoms/schema/collect_out.py +21 -0
- tigrbl/runtime/atoms/storage/__init__.py +38 -0
- tigrbl/runtime/atoms/storage/to_stored.py +167 -0
- tigrbl/runtime/atoms/wire/__init__.py +45 -0
- tigrbl/runtime/atoms/wire/build_in.py +166 -0
- tigrbl/runtime/atoms/wire/build_out.py +87 -0
- tigrbl/runtime/atoms/wire/dump.py +206 -0
- tigrbl/runtime/atoms/wire/validate_in.py +227 -0
- tigrbl/runtime/context.py +206 -0
- tigrbl/runtime/errors/__init__.py +61 -0
- tigrbl/runtime/errors/converters.py +214 -0
- tigrbl/runtime/errors/exceptions.py +124 -0
- tigrbl/runtime/errors/mappings.py +71 -0
- tigrbl/runtime/errors/utils.py +150 -0
- tigrbl/runtime/events.py +209 -0
- tigrbl/runtime/executor/__init__.py +6 -0
- tigrbl/runtime/executor/guards.py +132 -0
- tigrbl/runtime/executor/helpers.py +88 -0
- tigrbl/runtime/executor/invoke.py +150 -0
- tigrbl/runtime/executor/types.py +84 -0
- tigrbl/runtime/kernel.py +628 -0
- tigrbl/runtime/labels.py +353 -0
- tigrbl/runtime/opview.py +87 -0
- tigrbl/runtime/ordering.py +256 -0
- tigrbl/runtime/system.py +279 -0
- tigrbl/runtime/trace.py +330 -0
- tigrbl/schema/__init__.py +38 -0
- tigrbl/schema/_schema.py +27 -0
- tigrbl/schema/builder/__init__.py +17 -0
- tigrbl/schema/builder/build_schema.py +209 -0
- tigrbl/schema/builder/cache.py +24 -0
- tigrbl/schema/builder/compat.py +16 -0
- tigrbl/schema/builder/extras.py +85 -0
- tigrbl/schema/builder/helpers.py +51 -0
- tigrbl/schema/builder/list_params.py +117 -0
- tigrbl/schema/builder/strip_parent_fields.py +70 -0
- tigrbl/schema/collect.py +55 -0
- tigrbl/schema/decorators.py +68 -0
- tigrbl/schema/get_schema.py +86 -0
- tigrbl/schema/schema_spec.py +20 -0
- tigrbl/schema/shortcuts.py +42 -0
- tigrbl/schema/types.py +34 -0
- tigrbl/schema/utils.py +143 -0
- tigrbl/shortcuts.py +22 -0
- tigrbl/specs.py +44 -0
- tigrbl/system/__init__.py +12 -0
- tigrbl/system/diagnostics/__init__.py +24 -0
- tigrbl/system/diagnostics/compat.py +31 -0
- tigrbl/system/diagnostics/healthz.py +41 -0
- tigrbl/system/diagnostics/hookz.py +51 -0
- tigrbl/system/diagnostics/kernelz.py +20 -0
- tigrbl/system/diagnostics/methodz.py +43 -0
- tigrbl/system/diagnostics/router.py +73 -0
- tigrbl/system/diagnostics/utils.py +43 -0
- tigrbl/table/__init__.py +9 -0
- tigrbl/table/_base.py +237 -0
- tigrbl/table/_table.py +54 -0
- tigrbl/table/mro_collect.py +69 -0
- tigrbl/table/shortcuts.py +57 -0
- tigrbl/table/table_spec.py +28 -0
- tigrbl/transport/__init__.py +74 -0
- tigrbl/transport/jsonrpc/__init__.py +19 -0
- tigrbl/transport/jsonrpc/dispatcher.py +352 -0
- tigrbl/transport/jsonrpc/helpers.py +115 -0
- tigrbl/transport/jsonrpc/models.py +41 -0
- tigrbl/transport/rest/__init__.py +25 -0
- tigrbl/transport/rest/aggregator.py +132 -0
- tigrbl/types/__init__.py +174 -0
- tigrbl/types/allow_anon_provider.py +19 -0
- tigrbl/types/authn_abc.py +30 -0
- tigrbl/types/nested_path_provider.py +22 -0
- tigrbl/types/op.py +35 -0
- tigrbl/types/op_config_provider.py +17 -0
- tigrbl/types/op_verb_alias_provider.py +33 -0
- tigrbl/types/request_extras_provider.py +22 -0
- tigrbl/types/response_extras_provider.py +22 -0
- tigrbl/types/table_config_provider.py +13 -0
- tigrbl-0.3.0.dev3.dist-info/LICENSE +201 -0
- tigrbl-0.3.0.dev3.dist-info/METADATA +501 -0
- tigrbl-0.3.0.dev3.dist-info/RECORD +249 -0
- tigrbl/ExampleAgent.py +0 -1
- tigrbl-0.0.1.dev1.dist-info/METADATA +0 -18
- tigrbl-0.0.1.dev1.dist-info/RECORD +0 -5
- {tigrbl-0.0.1.dev1.dist-info → tigrbl-0.3.0.dev3.dist-info}/WHEEL +0 -0
tigrbl/op/types.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# tigrbl/v3/ops/types.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
|
|
6
|
+
from typing import Any, Literal, Mapping, Optional, Tuple, cast
|
|
7
|
+
|
|
8
|
+
from ..config.constants import CANON as CANONICAL_VERB_TUPLE
|
|
9
|
+
from ..hook.types import PHASE, HookPhase, PHASES, Ctx, StepFn, HookPredicate
|
|
10
|
+
from ..hook import HookSpec as OpHook
|
|
11
|
+
from ..response.types import ResponseSpec
|
|
12
|
+
from ..engine.engine_spec import EngineCfg
|
|
13
|
+
from typing import TYPE_CHECKING
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
16
|
+
from ..schema.types import SchemaArg
|
|
17
|
+
# ───────────────────────────────────────────────────────────────────────────────
|
|
18
|
+
# Core aliases & enums
|
|
19
|
+
# ───────────────────────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
PersistPolicy = Literal[
|
|
22
|
+
"default",
|
|
23
|
+
"prepend",
|
|
24
|
+
"append",
|
|
25
|
+
"override",
|
|
26
|
+
"skip",
|
|
27
|
+
] # TX policy
|
|
28
|
+
Arity = Literal["collection", "member"] # HTTP path shape
|
|
29
|
+
|
|
30
|
+
TargetOp = Literal[
|
|
31
|
+
"create",
|
|
32
|
+
"read",
|
|
33
|
+
"update",
|
|
34
|
+
"replace",
|
|
35
|
+
"merge",
|
|
36
|
+
"delete",
|
|
37
|
+
"list",
|
|
38
|
+
"clear",
|
|
39
|
+
"bulk_create",
|
|
40
|
+
"bulk_update",
|
|
41
|
+
"bulk_replace",
|
|
42
|
+
"bulk_merge",
|
|
43
|
+
"bulk_delete",
|
|
44
|
+
"custom",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
VerbAliasPolicy = Literal["both", "alias_only", "canonical_only"] # legacy export
|
|
48
|
+
|
|
49
|
+
# ───────────────────────────────────────────────────────────────────────────────
|
|
50
|
+
# Lazy-capable schema argument types
|
|
51
|
+
# ───────────────────────────────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# ───────────────────────────────────────────────────────────────────────────────
|
|
55
|
+
# Engine binding (optional, used by resolver precedence: op > table > api > app)
|
|
56
|
+
# ───────────────────────────────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass(frozen=True, slots=True)
|
|
60
|
+
class OpSpec:
|
|
61
|
+
"""
|
|
62
|
+
Single source of truth for an operation.
|
|
63
|
+
|
|
64
|
+
• `target` = canonical verb ("create"…,"custom")
|
|
65
|
+
• `arity` = REST shape ("member"|"collection")
|
|
66
|
+
|
|
67
|
+
Serialization mode is inferred **only** from schema presence:
|
|
68
|
+
- if model.schemas.<alias>.out exists → serialize
|
|
69
|
+
- otherwise → raw pass-through
|
|
70
|
+
|
|
71
|
+
Optional engine binding:
|
|
72
|
+
- `engine` allows per-op routing (DSN string or structured mapping).
|
|
73
|
+
When present, it participates in resolver precedence (op > table > api > app).
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
# Identity & exposure
|
|
77
|
+
alias: str
|
|
78
|
+
target: TargetOp
|
|
79
|
+
table: Optional[type] = None
|
|
80
|
+
expose_routes: bool = True
|
|
81
|
+
expose_rpc: bool = True
|
|
82
|
+
expose_method: bool = True
|
|
83
|
+
|
|
84
|
+
# Optional per-op engine binding (DSN string or mapping spec)
|
|
85
|
+
engine: Optional[EngineCfg] = None
|
|
86
|
+
|
|
87
|
+
# HTTP behavior
|
|
88
|
+
arity: Arity = "collection"
|
|
89
|
+
http_methods: Optional[Tuple[str, ...]] = None
|
|
90
|
+
path_suffix: Optional[str] = None
|
|
91
|
+
tags: Tuple[str, ...] = field(default_factory=tuple)
|
|
92
|
+
status_code: Optional[int] = None
|
|
93
|
+
response: Optional[ResponseSpec] = None
|
|
94
|
+
|
|
95
|
+
# Persistence
|
|
96
|
+
persist: PersistPolicy = "default"
|
|
97
|
+
|
|
98
|
+
# Schema overrides (resolved later by binder)
|
|
99
|
+
request_model: Optional[SchemaArg] = None
|
|
100
|
+
response_model: Optional[SchemaArg] = None
|
|
101
|
+
|
|
102
|
+
# Return shaping: "raw" passthrough vs "model" serialization
|
|
103
|
+
returns: Literal["raw", "model"] = "raw"
|
|
104
|
+
|
|
105
|
+
# Handler & hooks
|
|
106
|
+
handler: Optional[StepFn] = None
|
|
107
|
+
hooks: Tuple[OpHook, ...] = field(default_factory=tuple)
|
|
108
|
+
|
|
109
|
+
# RBAC / diagnostics
|
|
110
|
+
rbac_guard_op: Optional[str] = None
|
|
111
|
+
core: Optional[StepFn] = None
|
|
112
|
+
core_raw: Optional[StepFn] = None
|
|
113
|
+
extra: Mapping[str, Any] = field(default_factory=dict)
|
|
114
|
+
deps: Tuple[StepFn | str, ...] = field(default_factory=tuple)
|
|
115
|
+
secdeps: Tuple[StepFn | str, ...] = field(default_factory=tuple)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# Canonical verb set
|
|
119
|
+
CANON: Tuple[TargetOp, ...] = cast(Tuple[TargetOp, ...], CANONICAL_VERB_TUPLE)
|
|
120
|
+
|
|
121
|
+
__all__ = [
|
|
122
|
+
"PersistPolicy",
|
|
123
|
+
"Arity",
|
|
124
|
+
"TargetOp",
|
|
125
|
+
"VerbAliasPolicy",
|
|
126
|
+
"PHASE",
|
|
127
|
+
"HookPhase",
|
|
128
|
+
"PHASES",
|
|
129
|
+
"Ctx",
|
|
130
|
+
"StepFn",
|
|
131
|
+
"HookPredicate",
|
|
132
|
+
"EngineCfg",
|
|
133
|
+
"OpHook",
|
|
134
|
+
"OpSpec",
|
|
135
|
+
"CANON",
|
|
136
|
+
]
|
tigrbl/orm/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""ORM utilities containing SQLAlchemy tables and mixins."""
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# tigrbl/v3/mixins/_RowBound.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Any, Mapping, Sequence
|
|
5
|
+
|
|
6
|
+
from ...runtime.errors import HTTP_ERROR_MESSAGES, create_standardized_error
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class _RowBound:
|
|
10
|
+
"""
|
|
11
|
+
Base mix-in for row-level visibility.
|
|
12
|
+
|
|
13
|
+
Concrete subclasses **must** override:
|
|
14
|
+
|
|
15
|
+
@staticmethod
|
|
16
|
+
def is_visible(obj, ctx) -> bool
|
|
17
|
+
|
|
18
|
+
Hooks are wired only if the subclass actually provides an implementation.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
# ────────────────────────────────────────────────────────────────────
|
|
22
|
+
# Tigrbl bootstrap
|
|
23
|
+
# -------------------------------------------------------------------
|
|
24
|
+
def __init_subclass__(cls, **kw):
|
|
25
|
+
super().__init_subclass__(**kw)
|
|
26
|
+
cls._install_rowbound_hooks()
|
|
27
|
+
|
|
28
|
+
@classmethod
|
|
29
|
+
def _install_rowbound_hooks(cls) -> None:
|
|
30
|
+
# Skip abstract helpers or unmapped mix-ins
|
|
31
|
+
if cls.is_visible is _RowBound.is_visible:
|
|
32
|
+
return
|
|
33
|
+
if not hasattr(cls, "__table__"):
|
|
34
|
+
return
|
|
35
|
+
|
|
36
|
+
hook = cls._make_row_visibility_hook()
|
|
37
|
+
hooks_attr = getattr(cls, "__tigrbl_hooks__", {})
|
|
38
|
+
hooks = {**hooks_attr} if isinstance(hooks_attr, dict) else {}
|
|
39
|
+
|
|
40
|
+
def _append(alias: str, phase: str, fn) -> None:
|
|
41
|
+
phase_map = hooks.get(alias) or {}
|
|
42
|
+
lst = list(phase_map.get(phase) or [])
|
|
43
|
+
if fn not in lst:
|
|
44
|
+
lst.append(fn)
|
|
45
|
+
phase_map[phase] = tuple(lst)
|
|
46
|
+
hooks[alias] = phase_map
|
|
47
|
+
|
|
48
|
+
for op in ("read", "list"):
|
|
49
|
+
_append(op, "POST_HANDLER", hook)
|
|
50
|
+
|
|
51
|
+
setattr(cls, "__tigrbl_hooks__", hooks)
|
|
52
|
+
|
|
53
|
+
# ────────────────────────────────────────────────────────────────────
|
|
54
|
+
# Per-request hook
|
|
55
|
+
# -------------------------------------------------------------------
|
|
56
|
+
@classmethod
|
|
57
|
+
def _make_row_visibility_hook(cls):
|
|
58
|
+
def _row_visibility_hook(ctx: Mapping[str, Any]) -> None:
|
|
59
|
+
if "result" not in ctx: # nothing to filter
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
res = ctx["result"]
|
|
63
|
+
|
|
64
|
+
# LIST → keep only visible rows
|
|
65
|
+
if isinstance(res, Sequence):
|
|
66
|
+
ctx["result"] = [row for row in res if cls.is_visible(row, ctx)]
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
# READ → invisible row → pretend 404
|
|
70
|
+
if not cls.is_visible(res, ctx):
|
|
71
|
+
http_exc, _, _ = create_standardized_error(
|
|
72
|
+
404, message=HTTP_ERROR_MESSAGES[404]
|
|
73
|
+
)
|
|
74
|
+
raise http_exc
|
|
75
|
+
|
|
76
|
+
return _row_visibility_hook
|
|
77
|
+
|
|
78
|
+
# -------------------------------------------------------------------
|
|
79
|
+
# Must be overridden
|
|
80
|
+
# -------------------------------------------------------------------
|
|
81
|
+
@staticmethod
|
|
82
|
+
def is_visible(obj, ctx) -> bool: # pragma: no cover
|
|
83
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from .bootstrappable import Bootstrappable
|
|
4
|
+
from .upsertable import Upsertable
|
|
5
|
+
from .ownable import Ownable, OwnerPolicy
|
|
6
|
+
from .tenant_bound import TenantBound, TenantPolicy
|
|
7
|
+
from .key_digest import KeyDigest
|
|
8
|
+
|
|
9
|
+
from .utils import (
|
|
10
|
+
tzutcnow,
|
|
11
|
+
tzutcnow_plus_day,
|
|
12
|
+
_infer_schema,
|
|
13
|
+
uuid_example,
|
|
14
|
+
CRUD_IN,
|
|
15
|
+
CRUD_OUT,
|
|
16
|
+
CRUD_IO,
|
|
17
|
+
RO_IO,
|
|
18
|
+
)
|
|
19
|
+
from .principals import GUIDPk, TenantColumn, UserColumn, OrgColumn, Principal
|
|
20
|
+
from .bound import OwnerBound, UserBound
|
|
21
|
+
from .lifecycle import (
|
|
22
|
+
Created,
|
|
23
|
+
LastUsed,
|
|
24
|
+
Timestamped,
|
|
25
|
+
ActiveToggle,
|
|
26
|
+
SoftDelete,
|
|
27
|
+
Versioned,
|
|
28
|
+
)
|
|
29
|
+
from .hierarchy import Contained, TreeNode
|
|
30
|
+
from .edges import RelationEdge, MaskableEdge, TaggableEdge
|
|
31
|
+
from .markers import AsyncCapable, Audited
|
|
32
|
+
from .locks import RowLock, SoftLock
|
|
33
|
+
from .operations import BulkCapable, Replaceable, Mergeable, Streamable
|
|
34
|
+
from .fields import (
|
|
35
|
+
Slugged,
|
|
36
|
+
StatusColumn,
|
|
37
|
+
ValidityWindow,
|
|
38
|
+
Monetary,
|
|
39
|
+
ExtRef,
|
|
40
|
+
MetaJSON,
|
|
41
|
+
BlobRef,
|
|
42
|
+
SearchVector,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
__all__ = [
|
|
46
|
+
"Bootstrappable",
|
|
47
|
+
"Upsertable",
|
|
48
|
+
"Ownable",
|
|
49
|
+
"OwnerPolicy",
|
|
50
|
+
"TenantBound",
|
|
51
|
+
"TenantPolicy",
|
|
52
|
+
"KeyDigest",
|
|
53
|
+
"tzutcnow",
|
|
54
|
+
"tzutcnow_plus_day",
|
|
55
|
+
"_infer_schema",
|
|
56
|
+
"uuid_example",
|
|
57
|
+
"CRUD_IN",
|
|
58
|
+
"CRUD_OUT",
|
|
59
|
+
"CRUD_IO",
|
|
60
|
+
"RO_IO",
|
|
61
|
+
"GUIDPk",
|
|
62
|
+
"TenantColumn",
|
|
63
|
+
"UserColumn",
|
|
64
|
+
"OrgColumn",
|
|
65
|
+
"Principal",
|
|
66
|
+
"OwnerBound",
|
|
67
|
+
"UserBound",
|
|
68
|
+
"Created",
|
|
69
|
+
"LastUsed",
|
|
70
|
+
"Timestamped",
|
|
71
|
+
"ActiveToggle",
|
|
72
|
+
"SoftDelete",
|
|
73
|
+
"Versioned",
|
|
74
|
+
"Contained",
|
|
75
|
+
"TreeNode",
|
|
76
|
+
"RelationEdge",
|
|
77
|
+
"MaskableEdge",
|
|
78
|
+
"TaggableEdge",
|
|
79
|
+
"AsyncCapable",
|
|
80
|
+
"Audited",
|
|
81
|
+
"RowLock",
|
|
82
|
+
"SoftLock",
|
|
83
|
+
"BulkCapable",
|
|
84
|
+
"Replaceable",
|
|
85
|
+
"Mergeable",
|
|
86
|
+
"Streamable",
|
|
87
|
+
"Slugged",
|
|
88
|
+
"StatusColumn",
|
|
89
|
+
"ValidityWindow",
|
|
90
|
+
"Monetary",
|
|
91
|
+
"ExtRef",
|
|
92
|
+
"MetaJSON",
|
|
93
|
+
"BlobRef",
|
|
94
|
+
"SearchVector",
|
|
95
|
+
]
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, ClassVar, Iterable
|
|
4
|
+
import logging
|
|
5
|
+
import sqlalchemy as sa
|
|
6
|
+
from sqlalchemy import inspect as sa_inspect
|
|
7
|
+
from sqlalchemy.exc import IntegrityError
|
|
8
|
+
|
|
9
|
+
from ...types import Session, event
|
|
10
|
+
|
|
11
|
+
log = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Bootstrappable:
|
|
15
|
+
"""
|
|
16
|
+
Seed DEFAULT_ROWS for *this mapped class only* with zero magic.
|
|
17
|
+
|
|
18
|
+
Rules:
|
|
19
|
+
- Insert ONLY keys present on this class's mapped columns.
|
|
20
|
+
- No auto defaults, no timestamp injection, no unique probing.
|
|
21
|
+
- Idempotency only if ALL primary key columns are present in the row.
|
|
22
|
+
- Listener is attached to cls.__table__ (no cross-class/global effects).
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
DEFAULT_ROWS: ClassVar[list[dict[str, Any]]] = []
|
|
26
|
+
|
|
27
|
+
def __init_subclass__(cls, **kw):
|
|
28
|
+
super().__init_subclass__(**kw)
|
|
29
|
+
event.listen(
|
|
30
|
+
cls.__table__, "after_create", cls._after_create_insert_default_rows
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def _after_create_insert_default_rows(cls, target, connection, **_):
|
|
35
|
+
if not getattr(cls, "DEFAULT_ROWS", None):
|
|
36
|
+
return
|
|
37
|
+
from sqlalchemy.orm import sessionmaker
|
|
38
|
+
|
|
39
|
+
SessionLocal = sessionmaker(bind=connection, future=True)
|
|
40
|
+
db: Session = SessionLocal()
|
|
41
|
+
try:
|
|
42
|
+
cls._insert_rows(db, cls.DEFAULT_ROWS)
|
|
43
|
+
db.commit()
|
|
44
|
+
except Exception as e:
|
|
45
|
+
db.rollback()
|
|
46
|
+
log.warning(
|
|
47
|
+
"Bootstrappable seed failed for %s: %s",
|
|
48
|
+
cls.__name__,
|
|
49
|
+
repr(e),
|
|
50
|
+
exc_info=True,
|
|
51
|
+
)
|
|
52
|
+
finally:
|
|
53
|
+
db.close()
|
|
54
|
+
|
|
55
|
+
@classmethod
|
|
56
|
+
def ensure_bootstrapped(
|
|
57
|
+
cls, db: Session, rows: Iterable[dict[str, Any]] | None = None
|
|
58
|
+
) -> None:
|
|
59
|
+
rows = cls.DEFAULT_ROWS if rows is None else list(rows)
|
|
60
|
+
if rows:
|
|
61
|
+
cls._insert_rows(db, rows)
|
|
62
|
+
|
|
63
|
+
@classmethod
|
|
64
|
+
def _insert_rows(cls, db: Session, rows: Iterable[dict[str, Any]]) -> None:
|
|
65
|
+
mapper = sa_inspect(cls)
|
|
66
|
+
|
|
67
|
+
# --- pick an insertable target (avoid boolean eval + avoid JOINs) ---
|
|
68
|
+
local = mapper.local_table
|
|
69
|
+
table = local if local is not None else mapper.persist_selectable
|
|
70
|
+
if not hasattr(table, "insert"): # e.g., persist_selectable is a JOIN
|
|
71
|
+
table = mapper.local_table
|
|
72
|
+
|
|
73
|
+
col_keys = {c.key for c in mapper.columns}
|
|
74
|
+
pk_cols = list(table.primary_key.columns) if table.primary_key else []
|
|
75
|
+
pk_keys = {c.key for c in pk_cols}
|
|
76
|
+
|
|
77
|
+
def clean(r: dict[str, Any]) -> dict[str, Any]:
|
|
78
|
+
# keep only columns mapped on THIS class
|
|
79
|
+
return {k: r[k] for k in r.keys() & col_keys}
|
|
80
|
+
|
|
81
|
+
payloads = [clean(r) for r in rows if r]
|
|
82
|
+
if not payloads:
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
# Idempotent path if all PK columns are provided (per row)
|
|
86
|
+
can_upsert = bool(pk_cols) and all(pk_keys <= set(p.keys()) for p in payloads)
|
|
87
|
+
|
|
88
|
+
dialect = db.get_bind().dialect.name
|
|
89
|
+
|
|
90
|
+
if can_upsert and dialect == "postgresql":
|
|
91
|
+
from sqlalchemy.dialects.postgresql import insert as pg_insert
|
|
92
|
+
|
|
93
|
+
stmt = (
|
|
94
|
+
pg_insert(table)
|
|
95
|
+
.values(payloads)
|
|
96
|
+
.on_conflict_do_nothing(index_elements=[c.name for c in pk_cols])
|
|
97
|
+
)
|
|
98
|
+
db.execute(stmt)
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
if can_upsert and dialect == "sqlite":
|
|
102
|
+
# Best-effort idempotency for SQLite
|
|
103
|
+
from sqlalchemy.dialects.sqlite import insert as sqlite_insert
|
|
104
|
+
|
|
105
|
+
db.execute(sqlite_insert(table).values(payloads).prefix_with("OR IGNORE"))
|
|
106
|
+
return
|
|
107
|
+
|
|
108
|
+
# Fallback: plain inserts; swallow duplicate races
|
|
109
|
+
for p in payloads:
|
|
110
|
+
try:
|
|
111
|
+
db.execute(sa.insert(table).values(**p))
|
|
112
|
+
except IntegrityError:
|
|
113
|
+
db.rollback() # treat as already present
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from ...config.constants import CTX_AUTH_KEY, CTX_USER_ID_KEY
|
|
4
|
+
from ...specs import ColumnSpec, F, S, acol
|
|
5
|
+
from ...specs.storage_spec import ForeignKeySpec
|
|
6
|
+
from ...types import PgUUID, UUID, Mapped
|
|
7
|
+
|
|
8
|
+
from .utils import uuid_example, CRUD_IO
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class OwnerBound:
|
|
12
|
+
owner_id: Mapped[UUID] = acol(
|
|
13
|
+
spec=ColumnSpec(
|
|
14
|
+
storage=S(
|
|
15
|
+
type_=PgUUID(as_uuid=True),
|
|
16
|
+
fk=ForeignKeySpec(target="users.id"),
|
|
17
|
+
),
|
|
18
|
+
field=F(py_type=UUID, constraints={"examples": [uuid_example]}),
|
|
19
|
+
io=CRUD_IO,
|
|
20
|
+
)
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
@classmethod
|
|
24
|
+
def filter_for_ctx(cls, q, ctx):
|
|
25
|
+
auto_fields = ctx.get(CTX_AUTH_KEY, {})
|
|
26
|
+
return q.filter(cls.owner_id == auto_fields.get(CTX_USER_ID_KEY))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class UserBound:
|
|
30
|
+
user_id: Mapped[UUID] = acol(
|
|
31
|
+
spec=ColumnSpec(
|
|
32
|
+
storage=S(
|
|
33
|
+
type_=PgUUID(as_uuid=True),
|
|
34
|
+
fk=ForeignKeySpec(target="users.id"),
|
|
35
|
+
),
|
|
36
|
+
field=F(py_type=UUID, constraints={"examples": [uuid_example]}),
|
|
37
|
+
io=CRUD_IO,
|
|
38
|
+
)
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def filter_for_ctx(cls, q, ctx):
|
|
43
|
+
auto_fields = ctx.get(CTX_AUTH_KEY, {})
|
|
44
|
+
return q.filter(cls.user_id == auto_fields.get(CTX_USER_ID_KEY))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
__all__ = ["OwnerBound", "UserBound"]
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from ...specs import ColumnSpec, F, S, acol
|
|
4
|
+
from ...types import Integer, String, declarative_mixin, Mapped
|
|
5
|
+
|
|
6
|
+
from .utils import CRUD_IO
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@declarative_mixin
|
|
10
|
+
class RelationEdge:
|
|
11
|
+
"""Marker: row itself is an association—no extra columns required."""
|
|
12
|
+
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@declarative_mixin
|
|
17
|
+
class MaskableEdge:
|
|
18
|
+
"""Edge row with bitmap of verbs/roles."""
|
|
19
|
+
|
|
20
|
+
mask: Mapped[int] = acol(
|
|
21
|
+
spec=ColumnSpec(
|
|
22
|
+
storage=S(type_=Integer, nullable=False),
|
|
23
|
+
field=F(py_type=int),
|
|
24
|
+
io=CRUD_IO,
|
|
25
|
+
)
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@declarative_mixin
|
|
30
|
+
class TaggableEdge:
|
|
31
|
+
tag: Mapped[str] = acol(
|
|
32
|
+
spec=ColumnSpec(
|
|
33
|
+
storage=S(type_=String, nullable=False),
|
|
34
|
+
field=F(py_type=str),
|
|
35
|
+
io=CRUD_IO,
|
|
36
|
+
)
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
__all__ = ["RelationEdge", "MaskableEdge", "TaggableEdge"]
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import datetime as dt
|
|
4
|
+
from decimal import Decimal
|
|
5
|
+
|
|
6
|
+
from ...specs import ColumnSpec, F, IO, S, acol
|
|
7
|
+
from ...types import (
|
|
8
|
+
TZDateTime,
|
|
9
|
+
PgUUID,
|
|
10
|
+
String,
|
|
11
|
+
SAEnum,
|
|
12
|
+
Numeric,
|
|
13
|
+
JSONB,
|
|
14
|
+
TSVECTOR,
|
|
15
|
+
UUID,
|
|
16
|
+
Index,
|
|
17
|
+
declarative_mixin,
|
|
18
|
+
declared_attr,
|
|
19
|
+
Mapped,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
from .utils import tzutcnow, tzutcnow_plus_day, CRUD_IO
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@declarative_mixin
|
|
26
|
+
class Slugged:
|
|
27
|
+
slug: Mapped[str] = acol(
|
|
28
|
+
spec=ColumnSpec(
|
|
29
|
+
storage=S(type_=String, unique=True, nullable=False),
|
|
30
|
+
field=F(py_type=str, constraints={"max_length": 120}),
|
|
31
|
+
io=CRUD_IO,
|
|
32
|
+
)
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@declarative_mixin
|
|
37
|
+
class StatusColumn:
|
|
38
|
+
status: Mapped[str] = acol(
|
|
39
|
+
spec=ColumnSpec(
|
|
40
|
+
storage=S(
|
|
41
|
+
type_=SAEnum(
|
|
42
|
+
"queued",
|
|
43
|
+
"waiting",
|
|
44
|
+
"input_required",
|
|
45
|
+
"auth_required",
|
|
46
|
+
"approved",
|
|
47
|
+
"rejected",
|
|
48
|
+
"dispatched",
|
|
49
|
+
"running",
|
|
50
|
+
"paused",
|
|
51
|
+
"success",
|
|
52
|
+
"failed",
|
|
53
|
+
"cancelled",
|
|
54
|
+
name="status_enum",
|
|
55
|
+
),
|
|
56
|
+
default="waiting",
|
|
57
|
+
nullable=False,
|
|
58
|
+
),
|
|
59
|
+
field=F(py_type=str),
|
|
60
|
+
io=CRUD_IO,
|
|
61
|
+
)
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@declarative_mixin
|
|
66
|
+
class ValidityWindow:
|
|
67
|
+
valid_from: Mapped[dt.datetime] = acol(
|
|
68
|
+
spec=ColumnSpec(
|
|
69
|
+
storage=S(type_=TZDateTime, default=tzutcnow, nullable=False),
|
|
70
|
+
field=F(py_type=dt.datetime),
|
|
71
|
+
io=CRUD_IO,
|
|
72
|
+
)
|
|
73
|
+
)
|
|
74
|
+
valid_to: Mapped[dt.datetime | None] = acol(
|
|
75
|
+
spec=ColumnSpec(
|
|
76
|
+
storage=S(type_=TZDateTime, default=tzutcnow_plus_day),
|
|
77
|
+
field=F(py_type=dt.datetime),
|
|
78
|
+
io=CRUD_IO,
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@declarative_mixin
|
|
84
|
+
class Monetary:
|
|
85
|
+
amount: Mapped[Decimal] = acol(
|
|
86
|
+
spec=ColumnSpec(
|
|
87
|
+
storage=S(type_=Numeric(18, 2), nullable=False),
|
|
88
|
+
field=F(py_type=Decimal),
|
|
89
|
+
io=CRUD_IO,
|
|
90
|
+
)
|
|
91
|
+
)
|
|
92
|
+
currency: Mapped[str] = acol(
|
|
93
|
+
spec=ColumnSpec(
|
|
94
|
+
storage=S(type_=String, default="USD", nullable=False),
|
|
95
|
+
field=F(py_type=str, constraints={"max_length": 3}),
|
|
96
|
+
io=CRUD_IO,
|
|
97
|
+
)
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@declarative_mixin
|
|
102
|
+
class ExtRef:
|
|
103
|
+
external_id: Mapped[str] = acol(
|
|
104
|
+
spec=ColumnSpec(
|
|
105
|
+
storage=S(type_=String),
|
|
106
|
+
field=F(py_type=str),
|
|
107
|
+
io=CRUD_IO,
|
|
108
|
+
)
|
|
109
|
+
)
|
|
110
|
+
provider: Mapped[str] = acol(
|
|
111
|
+
spec=ColumnSpec(
|
|
112
|
+
storage=S(type_=String),
|
|
113
|
+
field=F(py_type=str),
|
|
114
|
+
io=CRUD_IO,
|
|
115
|
+
)
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@declarative_mixin
|
|
120
|
+
class MetaJSON:
|
|
121
|
+
meta: Mapped[dict] = acol(
|
|
122
|
+
spec=ColumnSpec(
|
|
123
|
+
storage=S(type_=JSONB, default=dict),
|
|
124
|
+
field=F(py_type=dict),
|
|
125
|
+
io=CRUD_IO,
|
|
126
|
+
)
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@declarative_mixin
|
|
131
|
+
class BlobRef:
|
|
132
|
+
blob_id: Mapped[UUID | None] = acol(
|
|
133
|
+
spec=ColumnSpec(
|
|
134
|
+
storage=S(type_=PgUUID(as_uuid=True)),
|
|
135
|
+
field=F(py_type=UUID),
|
|
136
|
+
io=CRUD_IO,
|
|
137
|
+
)
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@declarative_mixin
|
|
142
|
+
class SearchVector:
|
|
143
|
+
tsv: Mapped[str] = acol(
|
|
144
|
+
spec=ColumnSpec(
|
|
145
|
+
storage=S(type_=TSVECTOR),
|
|
146
|
+
field=F(py_type=str),
|
|
147
|
+
io=IO(),
|
|
148
|
+
)
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
@declared_attr
|
|
152
|
+
def __table_args__(cls):
|
|
153
|
+
return (Index(f"ix_{cls.__tablename__}_tsv", "tsv"),)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
__all__ = [
|
|
157
|
+
"Slugged",
|
|
158
|
+
"StatusColumn",
|
|
159
|
+
"ValidityWindow",
|
|
160
|
+
"Monetary",
|
|
161
|
+
"ExtRef",
|
|
162
|
+
"MetaJSON",
|
|
163
|
+
"BlobRef",
|
|
164
|
+
"SearchVector",
|
|
165
|
+
]
|