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
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from ...specs import ColumnSpec, F, S, acol
|
|
4
|
+
from ...specs.storage_spec import ForeignKeySpec
|
|
5
|
+
from ...types import PgUUID, UUID, String, declarative_mixin, declared_attr, Mapped
|
|
6
|
+
|
|
7
|
+
from .utils import CRUD_IO
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@declarative_mixin
|
|
11
|
+
class Contained:
|
|
12
|
+
@declared_attr
|
|
13
|
+
def parent_id(cls) -> Mapped[UUID]:
|
|
14
|
+
if not hasattr(cls, "parent_table"):
|
|
15
|
+
raise AttributeError("subclass must set parent_table")
|
|
16
|
+
spec = ColumnSpec(
|
|
17
|
+
storage=S(
|
|
18
|
+
type_=PgUUID(as_uuid=True),
|
|
19
|
+
fk=ForeignKeySpec(target=f"{cls.parent_table}.id"),
|
|
20
|
+
nullable=False,
|
|
21
|
+
),
|
|
22
|
+
field=F(py_type=UUID),
|
|
23
|
+
io=CRUD_IO,
|
|
24
|
+
)
|
|
25
|
+
return acol(spec=spec)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@declarative_mixin
|
|
29
|
+
class TreeNode:
|
|
30
|
+
"""Self-nesting hierarchy."""
|
|
31
|
+
|
|
32
|
+
@declared_attr
|
|
33
|
+
def parent_id(cls) -> Mapped[UUID | None]:
|
|
34
|
+
spec = ColumnSpec(
|
|
35
|
+
storage=S(
|
|
36
|
+
type_=PgUUID(as_uuid=True),
|
|
37
|
+
fk=ForeignKeySpec(target=f"{cls.__tablename__}.id"),
|
|
38
|
+
nullable=True,
|
|
39
|
+
),
|
|
40
|
+
field=F(py_type=UUID),
|
|
41
|
+
io=CRUD_IO,
|
|
42
|
+
)
|
|
43
|
+
return acol(spec=spec)
|
|
44
|
+
|
|
45
|
+
path: Mapped[str] = acol(
|
|
46
|
+
spec=ColumnSpec(
|
|
47
|
+
storage=S(type_=String),
|
|
48
|
+
field=F(py_type=str),
|
|
49
|
+
io=CRUD_IO,
|
|
50
|
+
)
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
__all__ = ["Contained", "TreeNode"]
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from hashlib import sha256
|
|
4
|
+
from secrets import token_urlsafe
|
|
5
|
+
|
|
6
|
+
from ...column.io_spec import Pair
|
|
7
|
+
from ...specs import F, IO, S, acol
|
|
8
|
+
from ...types import Mapped, String, declarative_mixin
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@declarative_mixin
|
|
12
|
+
class KeyDigest:
|
|
13
|
+
"""Provides hashed API key storage with helpers."""
|
|
14
|
+
|
|
15
|
+
@staticmethod
|
|
16
|
+
def _generate_pair(_ctx: dict | None = None) -> Pair:
|
|
17
|
+
"""Generate a raw/stored pair for API keys."""
|
|
18
|
+
raw = token_urlsafe(32)
|
|
19
|
+
return Pair(raw=raw, stored=sha256(raw.encode()).hexdigest())
|
|
20
|
+
|
|
21
|
+
digest: Mapped[str] = acol(
|
|
22
|
+
storage=S(String, nullable=False, unique=True, index=True),
|
|
23
|
+
field=F(constraints={"max_length": 64}),
|
|
24
|
+
io=IO(out_verbs=("read", "list", "create"), mutable_verbs=("create",)).paired(
|
|
25
|
+
make=_generate_pair,
|
|
26
|
+
alias="api_key",
|
|
27
|
+
verbs=("create",),
|
|
28
|
+
emit="post_refresh",
|
|
29
|
+
alias_field=F(py_type=str), # include alias in the response schema
|
|
30
|
+
mask_last=None, # set an int if you want masking
|
|
31
|
+
),
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
def digest_of(value: str) -> str:
|
|
36
|
+
return sha256(value.encode()).hexdigest()
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def raw_key(self) -> str: # pragma: no cover - write-only
|
|
40
|
+
raise AttributeError("raw_key is write-only")
|
|
41
|
+
|
|
42
|
+
@raw_key.setter
|
|
43
|
+
def raw_key(self, value: str) -> None:
|
|
44
|
+
self.digest = self.digest_of(value)
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import datetime as dt
|
|
4
|
+
|
|
5
|
+
from ...specs import ColumnSpec, F, IO, S, acol
|
|
6
|
+
from ...types import (
|
|
7
|
+
TZDateTime,
|
|
8
|
+
Boolean,
|
|
9
|
+
Integer,
|
|
10
|
+
PgUUID,
|
|
11
|
+
UUID,
|
|
12
|
+
declarative_mixin,
|
|
13
|
+
Mapped,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
from .utils import tzutcnow, CRUD_IO, RO_IO
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@declarative_mixin
|
|
20
|
+
class Created:
|
|
21
|
+
created_at: Mapped[dt.datetime] = acol(
|
|
22
|
+
spec=ColumnSpec(
|
|
23
|
+
storage=S(type_=TZDateTime, default=tzutcnow, nullable=False),
|
|
24
|
+
field=F(py_type=dt.datetime),
|
|
25
|
+
io=RO_IO,
|
|
26
|
+
)
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@declarative_mixin
|
|
31
|
+
class LastUsed:
|
|
32
|
+
last_used_at: Mapped[dt.datetime | None] = acol(
|
|
33
|
+
spec=ColumnSpec(
|
|
34
|
+
storage=S(type_=TZDateTime, nullable=True, onupdate=tzutcnow),
|
|
35
|
+
field=F(py_type=dt.datetime),
|
|
36
|
+
io=IO(out_verbs=("read", "list", "create")),
|
|
37
|
+
)
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
def touch(self) -> None:
|
|
41
|
+
"""Mark the object as used now."""
|
|
42
|
+
self.last_used_at = tzutcnow()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@declarative_mixin
|
|
46
|
+
class Timestamped:
|
|
47
|
+
created_at: Mapped[dt.datetime] = acol(
|
|
48
|
+
spec=ColumnSpec(
|
|
49
|
+
storage=S(type_=TZDateTime, default=tzutcnow, nullable=False),
|
|
50
|
+
field=F(py_type=dt.datetime),
|
|
51
|
+
io=RO_IO,
|
|
52
|
+
)
|
|
53
|
+
)
|
|
54
|
+
updated_at: Mapped[dt.datetime] = acol(
|
|
55
|
+
spec=ColumnSpec(
|
|
56
|
+
storage=S(
|
|
57
|
+
type_=TZDateTime,
|
|
58
|
+
default=tzutcnow,
|
|
59
|
+
onupdate=tzutcnow,
|
|
60
|
+
nullable=False,
|
|
61
|
+
),
|
|
62
|
+
field=F(py_type=dt.datetime),
|
|
63
|
+
io=RO_IO,
|
|
64
|
+
)
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@declarative_mixin
|
|
69
|
+
class ActiveToggle:
|
|
70
|
+
is_active: Mapped[bool] = acol(
|
|
71
|
+
spec=ColumnSpec(
|
|
72
|
+
storage=S(type_=Boolean, default=True, nullable=False),
|
|
73
|
+
field=F(py_type=bool),
|
|
74
|
+
io=CRUD_IO,
|
|
75
|
+
)
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@declarative_mixin
|
|
80
|
+
class SoftDelete:
|
|
81
|
+
deleted_at: Mapped[dt.datetime | None] = acol(
|
|
82
|
+
spec=ColumnSpec(
|
|
83
|
+
storage=S(type_=TZDateTime, nullable=True),
|
|
84
|
+
field=F(py_type=dt.datetime),
|
|
85
|
+
io=CRUD_IO,
|
|
86
|
+
)
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@declarative_mixin
|
|
91
|
+
class Versioned:
|
|
92
|
+
revision: Mapped[int] = acol(
|
|
93
|
+
spec=ColumnSpec(
|
|
94
|
+
storage=S(type_=Integer, default=1, nullable=False),
|
|
95
|
+
field=F(py_type=int),
|
|
96
|
+
io=CRUD_IO,
|
|
97
|
+
)
|
|
98
|
+
)
|
|
99
|
+
prev_id: Mapped[UUID | None] = acol(
|
|
100
|
+
spec=ColumnSpec(
|
|
101
|
+
storage=S(type_=PgUUID(as_uuid=True), nullable=True),
|
|
102
|
+
field=F(py_type=UUID),
|
|
103
|
+
io=CRUD_IO,
|
|
104
|
+
)
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
__all__ = [
|
|
109
|
+
"Created",
|
|
110
|
+
"LastUsed",
|
|
111
|
+
"Timestamped",
|
|
112
|
+
"ActiveToggle",
|
|
113
|
+
"SoftDelete",
|
|
114
|
+
"Versioned",
|
|
115
|
+
]
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import datetime as dt
|
|
4
|
+
|
|
5
|
+
from ...specs import ColumnSpec, F, S, acol
|
|
6
|
+
from ...specs.storage_spec import ForeignKeySpec
|
|
7
|
+
from ...types import PgUUID, TZDateTime, UUID, declarative_mixin, Mapped
|
|
8
|
+
|
|
9
|
+
from .utils import CRUD_IO
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@declarative_mixin
|
|
13
|
+
class RowLock:
|
|
14
|
+
lock_token: Mapped[UUID | None] = acol(
|
|
15
|
+
spec=ColumnSpec(
|
|
16
|
+
storage=S(type_=PgUUID(as_uuid=True), nullable=True),
|
|
17
|
+
field=F(py_type=UUID),
|
|
18
|
+
io=CRUD_IO,
|
|
19
|
+
)
|
|
20
|
+
)
|
|
21
|
+
locked_at: Mapped[dt.datetime | None] = acol(
|
|
22
|
+
spec=ColumnSpec(
|
|
23
|
+
storage=S(type_=TZDateTime, nullable=True),
|
|
24
|
+
field=F(py_type=dt.datetime),
|
|
25
|
+
io=CRUD_IO,
|
|
26
|
+
)
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@declarative_mixin
|
|
31
|
+
class SoftLock:
|
|
32
|
+
locked_by: Mapped[UUID | None] = acol(
|
|
33
|
+
spec=ColumnSpec(
|
|
34
|
+
storage=S(
|
|
35
|
+
type_=PgUUID(as_uuid=True),
|
|
36
|
+
fk=ForeignKeySpec(target="users.id"),
|
|
37
|
+
),
|
|
38
|
+
field=F(py_type=UUID),
|
|
39
|
+
io=CRUD_IO,
|
|
40
|
+
)
|
|
41
|
+
)
|
|
42
|
+
locked_at: Mapped[dt.datetime | None] = acol(
|
|
43
|
+
spec=ColumnSpec(
|
|
44
|
+
storage=S(type_=TZDateTime),
|
|
45
|
+
field=F(py_type=dt.datetime),
|
|
46
|
+
io=CRUD_IO,
|
|
47
|
+
)
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
__all__ = ["RowLock", "SoftLock"]
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from ...config.constants import BULK_VERBS
|
|
4
|
+
from ...types import declarative_mixin
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@declarative_mixin
|
|
8
|
+
class BulkCapable:
|
|
9
|
+
__tigrbl_defaults_mode__: str = "all"
|
|
10
|
+
__tigrbl_defaults_include__: set[str] = {
|
|
11
|
+
v for v in BULK_VERBS if v not in {"bulk_replace", "bulk_merge"}
|
|
12
|
+
}
|
|
13
|
+
__tigrbl_defaults_exclude__: set[str] = set()
|
|
14
|
+
|
|
15
|
+
def __init_subclass__(cls, **kw):
|
|
16
|
+
super().__init_subclass__(**kw)
|
|
17
|
+
inc = set(getattr(cls, "__tigrbl_defaults_include__", set()))
|
|
18
|
+
inc.update(BulkCapable.__tigrbl_defaults_include__)
|
|
19
|
+
cls.__tigrbl_defaults_include__ = inc
|
|
20
|
+
|
|
21
|
+
exc = set(getattr(cls, "__tigrbl_defaults_exclude__", set()))
|
|
22
|
+
exc.update(BulkCapable.__tigrbl_defaults_exclude__)
|
|
23
|
+
cls.__tigrbl_defaults_exclude__ = exc
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@declarative_mixin
|
|
27
|
+
class Replaceable:
|
|
28
|
+
__tigrbl_defaults_mode__: str = "all"
|
|
29
|
+
__tigrbl_defaults_include__: set[str] = {"replace", "bulk_replace"}
|
|
30
|
+
__tigrbl_defaults_exclude__: set[str] = set()
|
|
31
|
+
|
|
32
|
+
def __init_subclass__(cls, **kw):
|
|
33
|
+
super().__init_subclass__(**kw)
|
|
34
|
+
inc = set(getattr(cls, "__tigrbl_defaults_include__", set()))
|
|
35
|
+
inc.update(Replaceable.__tigrbl_defaults_include__)
|
|
36
|
+
cls.__tigrbl_defaults_include__ = inc
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@declarative_mixin
|
|
40
|
+
class Mergeable:
|
|
41
|
+
__tigrbl_defaults_mode__: str = "all"
|
|
42
|
+
__tigrbl_defaults_include__: set[str] = {"merge", "bulk_merge"}
|
|
43
|
+
__tigrbl_defaults_exclude__: set[str] = set()
|
|
44
|
+
|
|
45
|
+
def __init_subclass__(cls, **kw):
|
|
46
|
+
super().__init_subclass__(**kw)
|
|
47
|
+
inc = set(getattr(cls, "__tigrbl_defaults_include__", set()))
|
|
48
|
+
inc.update(Mergeable.__tigrbl_defaults_include__)
|
|
49
|
+
cls.__tigrbl_defaults_include__ = inc
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@declarative_mixin
|
|
53
|
+
class Streamable:
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
__all__ = ["BulkCapable", "Replaceable", "Mergeable", "Streamable"]
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
# tigrbl/v3/mixins/ownable.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from typing import Any, Mapping
|
|
7
|
+
from uuid import UUID
|
|
8
|
+
|
|
9
|
+
from ...types import PgUUID, Mapped, declared_attr
|
|
10
|
+
|
|
11
|
+
from ...specs import acol
|
|
12
|
+
from ...config.constants import (
|
|
13
|
+
TIGRBL_HOOKS_ATTR,
|
|
14
|
+
TIGRBL_OWNER_POLICY_ATTR,
|
|
15
|
+
CTX_AUTH_KEY,
|
|
16
|
+
CTX_USER_ID_KEY,
|
|
17
|
+
)
|
|
18
|
+
from ...runtime.errors import create_standardized_error
|
|
19
|
+
from ...specs import ColumnSpec, F, IO, S
|
|
20
|
+
from ...specs.storage_spec import ForeignKeySpec
|
|
21
|
+
|
|
22
|
+
log = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class OwnerPolicy(str, Enum):
|
|
26
|
+
CLIENT_SET = "client" # client may set; validated against user_id if provided
|
|
27
|
+
DEFAULT_TO_USER = "default" # if missing, default to user_id
|
|
28
|
+
STRICT_SERVER = "strict" # server enforces user_id; client cannot override
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _infer_schema(cls, default: str = "public") -> str:
|
|
32
|
+
args = getattr(cls, "__table_args__", None)
|
|
33
|
+
if not args:
|
|
34
|
+
return default
|
|
35
|
+
if isinstance(args, dict):
|
|
36
|
+
return args.get("schema", default)
|
|
37
|
+
if isinstance(args, (tuple, list)):
|
|
38
|
+
for elem in args:
|
|
39
|
+
if isinstance(elem, dict) and "schema" in elem:
|
|
40
|
+
return elem["schema"]
|
|
41
|
+
return default
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _is_missing(v: Any) -> bool:
|
|
45
|
+
return v is None or (isinstance(v, str) and not v.strip())
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _normalize_uuid(v: Any) -> Any:
|
|
49
|
+
if isinstance(v, UUID):
|
|
50
|
+
return v
|
|
51
|
+
if isinstance(v, str):
|
|
52
|
+
try:
|
|
53
|
+
return UUID(v)
|
|
54
|
+
except ValueError:
|
|
55
|
+
return v
|
|
56
|
+
return v
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _ctx_user_id(ctx: Mapping[str, Any]) -> Any | None:
|
|
60
|
+
"""
|
|
61
|
+
Best-effort extraction of the caller user_id from ctx.
|
|
62
|
+
Checks:
|
|
63
|
+
1) ctx["user_id"] (preferred in v3)
|
|
64
|
+
2) ctx["auth"]["user_id"] (v3 conventional)
|
|
65
|
+
3) ctx["injected_fields"]["user_id"] (legacy)
|
|
66
|
+
4) ctx["auth_context"]["user_id"] (legacy)
|
|
67
|
+
"""
|
|
68
|
+
# 1) direct
|
|
69
|
+
u = (
|
|
70
|
+
ctx.get(CTX_USER_ID_KEY)
|
|
71
|
+
if isinstance(ctx, dict)
|
|
72
|
+
else getattr(ctx, CTX_USER_ID_KEY, None)
|
|
73
|
+
)
|
|
74
|
+
if u:
|
|
75
|
+
return _normalize_uuid(u)
|
|
76
|
+
|
|
77
|
+
# 2) auth dict
|
|
78
|
+
auth = (
|
|
79
|
+
ctx.get(CTX_AUTH_KEY)
|
|
80
|
+
if isinstance(ctx, dict)
|
|
81
|
+
else getattr(ctx, CTX_AUTH_KEY, None)
|
|
82
|
+
) or {}
|
|
83
|
+
u = auth.get("user_id")
|
|
84
|
+
if u:
|
|
85
|
+
return _normalize_uuid(u)
|
|
86
|
+
|
|
87
|
+
# 3 & 4) legacy fallbacks
|
|
88
|
+
inj = (
|
|
89
|
+
ctx.get("injected_fields")
|
|
90
|
+
if isinstance(ctx, dict)
|
|
91
|
+
else getattr(ctx, "injected_fields", None)
|
|
92
|
+
) or {}
|
|
93
|
+
u = inj.get("user_id")
|
|
94
|
+
if u:
|
|
95
|
+
return _normalize_uuid(u)
|
|
96
|
+
|
|
97
|
+
ac = (
|
|
98
|
+
ctx.get("auth_context")
|
|
99
|
+
if isinstance(ctx, dict)
|
|
100
|
+
else getattr(ctx, "auth_context", None)
|
|
101
|
+
) or {}
|
|
102
|
+
u = ac.get("user_id")
|
|
103
|
+
if u:
|
|
104
|
+
return _normalize_uuid(u)
|
|
105
|
+
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class Ownable:
|
|
110
|
+
"""
|
|
111
|
+
Mixin that adds an `owner_id` column and installs v3 hooks to enforce ownership policy.
|
|
112
|
+
|
|
113
|
+
Policy (per `__tigrbl_owner_policy__`):
|
|
114
|
+
• CLIENT_SET: client may provide `owner_id`; if missing, we leave it as-is.
|
|
115
|
+
• DEFAULT_TO_USER: if `owner_id` missing, default to ctx user; if provided, keep it.
|
|
116
|
+
• STRICT_SERVER: always enforce `owner_id = ctx user`; reject mismatches.
|
|
117
|
+
|
|
118
|
+
Hooks (installed at class creation via __init_subclass__):
|
|
119
|
+
• PRE_TX_BEGIN on "create": normalize/enforce `owner_id` in ctx.env.params & ctx.payload
|
|
120
|
+
• PRE_TX_BEGIN on "update": forbid changing `owner_id` unless CLIENT_SET and matches ctx user
|
|
121
|
+
(note: if you need to compare with the existing DB value, do that in POST_HANDLER where
|
|
122
|
+
your core sets ctx["result"] or fetch the row here — this version validates intent only)
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
__tigrbl_owner_policy__: OwnerPolicy = OwnerPolicy.CLIENT_SET
|
|
126
|
+
|
|
127
|
+
@declared_attr
|
|
128
|
+
def owner_id(cls) -> Mapped[UUID]:
|
|
129
|
+
pol = getattr(cls, TIGRBL_OWNER_POLICY_ATTR, OwnerPolicy.CLIENT_SET)
|
|
130
|
+
schema = _infer_schema(cls, default="public")
|
|
131
|
+
|
|
132
|
+
in_verbs = (
|
|
133
|
+
("create", "update", "replace")
|
|
134
|
+
if pol == OwnerPolicy.CLIENT_SET
|
|
135
|
+
else ("create",)
|
|
136
|
+
)
|
|
137
|
+
io = IO(
|
|
138
|
+
in_verbs=in_verbs,
|
|
139
|
+
out_verbs=("read", "list"),
|
|
140
|
+
mutable_verbs=in_verbs,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
spec = ColumnSpec(
|
|
144
|
+
storage=S(
|
|
145
|
+
type_=PgUUID(as_uuid=True),
|
|
146
|
+
fk=ForeignKeySpec(target=f"{schema}.users.id"),
|
|
147
|
+
nullable=False,
|
|
148
|
+
index=True,
|
|
149
|
+
),
|
|
150
|
+
field=F(py_type=UUID),
|
|
151
|
+
io=io,
|
|
152
|
+
)
|
|
153
|
+
return acol(spec=spec)
|
|
154
|
+
|
|
155
|
+
# ── hook installers --------------------------------------------------------
|
|
156
|
+
|
|
157
|
+
def __init_subclass__(cls, **kwargs):
|
|
158
|
+
super().__init_subclass__(**kwargs)
|
|
159
|
+
cls._install_ownable_hooks()
|
|
160
|
+
|
|
161
|
+
@classmethod
|
|
162
|
+
def _install_ownable_hooks(cls) -> None:
|
|
163
|
+
"""
|
|
164
|
+
Attach PRE_TX_BEGIN hooks to the class under __tigrbl_hooks__.
|
|
165
|
+
|
|
166
|
+
Structure expected by v3 binder:
|
|
167
|
+
{
|
|
168
|
+
"<alias>": {
|
|
169
|
+
"PRE_TX_BEGIN": [callable, ...],
|
|
170
|
+
...
|
|
171
|
+
},
|
|
172
|
+
...
|
|
173
|
+
}
|
|
174
|
+
"""
|
|
175
|
+
|
|
176
|
+
def _err(status: int, msg: str):
|
|
177
|
+
http_exc, _, _ = create_standardized_error(status, message=msg)
|
|
178
|
+
raise http_exc
|
|
179
|
+
|
|
180
|
+
def _before_create(ctx: dict[str, Any]) -> None:
|
|
181
|
+
env = (
|
|
182
|
+
ctx.get("env") if isinstance(ctx, dict) else getattr(ctx, "env", None)
|
|
183
|
+
) or {}
|
|
184
|
+
params = (
|
|
185
|
+
(
|
|
186
|
+
env.get("params")
|
|
187
|
+
if isinstance(env, dict)
|
|
188
|
+
else getattr(env, "params", None)
|
|
189
|
+
)
|
|
190
|
+
or (
|
|
191
|
+
ctx.get("payload")
|
|
192
|
+
if isinstance(ctx, dict)
|
|
193
|
+
else getattr(ctx, "payload", None)
|
|
194
|
+
)
|
|
195
|
+
or {}
|
|
196
|
+
)
|
|
197
|
+
if hasattr(params, "model_dump"):
|
|
198
|
+
params = params.model_dump()
|
|
199
|
+
|
|
200
|
+
user_id = _ctx_user_id(ctx)
|
|
201
|
+
provided = params.get("owner_id")
|
|
202
|
+
missing = _is_missing(provided)
|
|
203
|
+
pol = getattr(cls, TIGRBL_OWNER_POLICY_ATTR, OwnerPolicy.CLIENT_SET)
|
|
204
|
+
|
|
205
|
+
log.debug(
|
|
206
|
+
"Ownable PRE_TX_BEGIN(create): policy=%s params=%s user_id=%s",
|
|
207
|
+
pol,
|
|
208
|
+
params,
|
|
209
|
+
user_id,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
if pol == OwnerPolicy.STRICT_SERVER:
|
|
213
|
+
if user_id is None:
|
|
214
|
+
_err(400, "owner_id is required.")
|
|
215
|
+
if not missing:
|
|
216
|
+
if _normalize_uuid(provided) != _normalize_uuid(user_id):
|
|
217
|
+
_err(400, "owner_id mismatch.")
|
|
218
|
+
_err(400, "owner_id is server-assigned.")
|
|
219
|
+
params["owner_id"] = user_id # always enforce server value
|
|
220
|
+
elif pol == OwnerPolicy.DEFAULT_TO_USER:
|
|
221
|
+
if missing and user_id is not None:
|
|
222
|
+
params["owner_id"] = user_id
|
|
223
|
+
elif not missing:
|
|
224
|
+
params["owner_id"] = _normalize_uuid(provided)
|
|
225
|
+
else: # CLIENT_SET
|
|
226
|
+
if not missing:
|
|
227
|
+
params["owner_id"] = _normalize_uuid(provided)
|
|
228
|
+
# if missing, leave as-is (schema/DB may enforce NOT NULL)
|
|
229
|
+
|
|
230
|
+
# write back into both env.params and payload so downstream sees the same view
|
|
231
|
+
env_attr = (
|
|
232
|
+
ctx.get("env") if isinstance(ctx, dict) else getattr(ctx, "env", None)
|
|
233
|
+
)
|
|
234
|
+
if env_attr is not None:
|
|
235
|
+
if isinstance(env_attr, dict):
|
|
236
|
+
env_attr["params"] = params
|
|
237
|
+
else:
|
|
238
|
+
env_attr.params = params
|
|
239
|
+
if isinstance(ctx, dict):
|
|
240
|
+
ctx["payload"] = params
|
|
241
|
+
else:
|
|
242
|
+
setattr(ctx, "payload", params)
|
|
243
|
+
|
|
244
|
+
def _before_update(ctx: dict[str, Any]) -> None:
|
|
245
|
+
env = (
|
|
246
|
+
ctx.get("env") if isinstance(ctx, dict) else getattr(ctx, "env", None)
|
|
247
|
+
) or {}
|
|
248
|
+
params = (
|
|
249
|
+
(
|
|
250
|
+
env.get("params")
|
|
251
|
+
if isinstance(env, dict)
|
|
252
|
+
else getattr(env, "params", None)
|
|
253
|
+
)
|
|
254
|
+
or (
|
|
255
|
+
ctx.get("payload")
|
|
256
|
+
if isinstance(ctx, dict)
|
|
257
|
+
else getattr(ctx, "payload", None)
|
|
258
|
+
)
|
|
259
|
+
or {}
|
|
260
|
+
)
|
|
261
|
+
if hasattr(params, "model_dump"):
|
|
262
|
+
params = params.model_dump()
|
|
263
|
+
|
|
264
|
+
if "owner_id" not in params:
|
|
265
|
+
return # nothing to check
|
|
266
|
+
|
|
267
|
+
pol = getattr(cls, TIGRBL_OWNER_POLICY_ATTR, OwnerPolicy.CLIENT_SET)
|
|
268
|
+
if _is_missing(params.get("owner_id")):
|
|
269
|
+
# treat None/"" as not provided → drop it
|
|
270
|
+
params.pop("owner_id", None)
|
|
271
|
+
env_attr = (
|
|
272
|
+
ctx.get("env")
|
|
273
|
+
if isinstance(ctx, dict)
|
|
274
|
+
else getattr(ctx, "env", None)
|
|
275
|
+
)
|
|
276
|
+
if env_attr is not None:
|
|
277
|
+
if isinstance(env_attr, dict):
|
|
278
|
+
env_attr["params"] = params
|
|
279
|
+
else:
|
|
280
|
+
env_attr.params = params
|
|
281
|
+
if isinstance(ctx, dict):
|
|
282
|
+
ctx["payload"] = params
|
|
283
|
+
else:
|
|
284
|
+
setattr(ctx, "payload", params)
|
|
285
|
+
return
|
|
286
|
+
|
|
287
|
+
if pol != OwnerPolicy.CLIENT_SET:
|
|
288
|
+
_err(400, "owner_id is immutable.")
|
|
289
|
+
|
|
290
|
+
# CLIENT_SET: require new value == caller user_id unless an admin flag is present
|
|
291
|
+
new_val = _normalize_uuid(params["owner_id"])
|
|
292
|
+
user_id = _ctx_user_id(ctx)
|
|
293
|
+
is_admin = bool(ctx.get("is_admin"))
|
|
294
|
+
|
|
295
|
+
log.debug(
|
|
296
|
+
"Ownable PRE_TX_BEGIN(update): new=%s user_id=%s is_admin=%s",
|
|
297
|
+
new_val,
|
|
298
|
+
user_id,
|
|
299
|
+
is_admin,
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
if not is_admin and user_id is not None and new_val != user_id:
|
|
303
|
+
_err(403, "Cannot transfer ownership.")
|
|
304
|
+
|
|
305
|
+
# normalize stored value
|
|
306
|
+
params["owner_id"] = new_val
|
|
307
|
+
env_attr = (
|
|
308
|
+
ctx.get("env") if isinstance(ctx, dict) else getattr(ctx, "env", None)
|
|
309
|
+
)
|
|
310
|
+
if env_attr is not None:
|
|
311
|
+
if isinstance(env_attr, dict):
|
|
312
|
+
env_attr["params"] = params
|
|
313
|
+
else:
|
|
314
|
+
env_attr.params = params
|
|
315
|
+
if isinstance(ctx, dict):
|
|
316
|
+
ctx["payload"] = params
|
|
317
|
+
else:
|
|
318
|
+
setattr(ctx, "payload", params)
|
|
319
|
+
|
|
320
|
+
# Attach (merge) into __tigrbl_hooks__ without clobbering existing mappings
|
|
321
|
+
hooks = getattr(cls, TIGRBL_HOOKS_ATTR, None) or {}
|
|
322
|
+
hooks = {**hooks} # shallow copy
|
|
323
|
+
|
|
324
|
+
def _append(alias: str, phase: str, fn):
|
|
325
|
+
phase_map = hooks.get(alias) or {}
|
|
326
|
+
lst = list(phase_map.get(phase) or [])
|
|
327
|
+
if fn not in lst:
|
|
328
|
+
lst.append(fn)
|
|
329
|
+
phase_map[phase] = tuple(
|
|
330
|
+
lst
|
|
331
|
+
) # tuples are safer against accidental mutation
|
|
332
|
+
hooks[alias] = phase_map
|
|
333
|
+
|
|
334
|
+
_append("create", "PRE_TX_BEGIN", _before_create)
|
|
335
|
+
_append("update", "PRE_TX_BEGIN", _before_update)
|
|
336
|
+
|
|
337
|
+
setattr(cls, TIGRBL_HOOKS_ATTR, hooks)
|