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,25 @@
|
|
|
1
|
+
# peagen/orm/api_key.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
from ...specs import acol, IO, S, F
|
|
6
|
+
from ...types import LargeBinary, Mapped, String
|
|
7
|
+
|
|
8
|
+
from ._base import Base
|
|
9
|
+
from ..mixins import ActiveToggle, GUIDPk, Timestamped, TenantBound
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Client(Base, GUIDPk, Timestamped, TenantBound, ActiveToggle):
|
|
13
|
+
__tablename__ = "clients"
|
|
14
|
+
__abstract__ = True
|
|
15
|
+
# ---------------------------------------------------------------- columns --
|
|
16
|
+
client_secret_hash: Mapped[bytes] = acol(
|
|
17
|
+
storage=S(LargeBinary(60), nullable=False),
|
|
18
|
+
field=F(),
|
|
19
|
+
io=IO(in_verbs=("create",)),
|
|
20
|
+
)
|
|
21
|
+
redirect_uris: Mapped[str] = acol(
|
|
22
|
+
storage=S(String, nullable=False),
|
|
23
|
+
field=F(constraints={"max_length": 1000}),
|
|
24
|
+
io=IO(),
|
|
25
|
+
)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Group model."""
|
|
2
|
+
|
|
3
|
+
from ._base import Base
|
|
4
|
+
from ..mixins import GUIDPk, Timestamped, TenantBound, Principal
|
|
5
|
+
from ...specs import IO, F, acol, S
|
|
6
|
+
from ...types import Mapped, String
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Group(Base, GUIDPk, Timestamped, TenantBound, Principal):
|
|
10
|
+
__tablename__ = "groups"
|
|
11
|
+
name: Mapped[str] = acol(
|
|
12
|
+
storage=S(String),
|
|
13
|
+
field=F(),
|
|
14
|
+
io=IO(),
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
__all__ = ["Group"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
for _name in list(globals()):
|
|
22
|
+
if _name not in __all__ and not _name.startswith("__"):
|
|
23
|
+
del globals()[_name]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def __dir__():
|
|
27
|
+
"""Tighten ``dir()`` output for interactive sessions."""
|
|
28
|
+
|
|
29
|
+
return sorted(__all__)
|
tigrbl/orm/tables/org.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Org model."""
|
|
2
|
+
|
|
3
|
+
from ._base import Base
|
|
4
|
+
from ..mixins import GUIDPk, Timestamped, TenantBound, Principal
|
|
5
|
+
from ...specs import IO, F, acol, S
|
|
6
|
+
from ...types import Mapped, String
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Org(Base, GUIDPk, Timestamped, TenantBound, Principal):
|
|
10
|
+
__tablename__ = "orgs"
|
|
11
|
+
__abstract__ = True
|
|
12
|
+
name: Mapped[str] = acol(
|
|
13
|
+
storage=S(String),
|
|
14
|
+
field=F(),
|
|
15
|
+
io=IO(),
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
__all__ = ["Org"]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
for _name in list(globals()):
|
|
23
|
+
if _name not in __all__ and not _name.startswith("__"):
|
|
24
|
+
del globals()[_name]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def __dir__():
|
|
28
|
+
"""Tighten ``dir()`` output for interactive sessions."""
|
|
29
|
+
|
|
30
|
+
return sorted(__all__)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from uuid import UUID
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
from ...specs import IO, F, acol, S
|
|
5
|
+
from ...specs.storage_spec import ForeignKeySpec
|
|
6
|
+
from ...types import Integer, String, PgUUID, Mapped
|
|
7
|
+
|
|
8
|
+
from . import Base
|
|
9
|
+
from ..mixins import (
|
|
10
|
+
GUIDPk,
|
|
11
|
+
TenantBound,
|
|
12
|
+
RelationEdge,
|
|
13
|
+
Timestamped,
|
|
14
|
+
MaskableEdge,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# ───────── RBAC core ──────────────────────────────────────────────────
|
|
19
|
+
class Role(Base, GUIDPk, Timestamped, TenantBound):
|
|
20
|
+
__tablename__ = "roles"
|
|
21
|
+
slug: Mapped[str] = acol(
|
|
22
|
+
storage=S(String, unique=True),
|
|
23
|
+
field=F(),
|
|
24
|
+
io=IO(),
|
|
25
|
+
)
|
|
26
|
+
global_mask: Mapped[int] = acol(
|
|
27
|
+
storage=S(Integer, default=0),
|
|
28
|
+
field=F(),
|
|
29
|
+
io=IO(),
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class RolePerm(Base, GUIDPk, Timestamped, TenantBound, RelationEdge, MaskableEdge):
|
|
34
|
+
__tablename__ = "role_perms"
|
|
35
|
+
role_id: Mapped[UUID] = acol(
|
|
36
|
+
storage=S(PgUUID, fk=ForeignKeySpec("roles.id")),
|
|
37
|
+
field=F(),
|
|
38
|
+
io=IO(),
|
|
39
|
+
)
|
|
40
|
+
target_table: Mapped[str] = acol(
|
|
41
|
+
storage=S(String),
|
|
42
|
+
field=F(),
|
|
43
|
+
io=IO(),
|
|
44
|
+
)
|
|
45
|
+
target_id: Mapped[str] = acol(
|
|
46
|
+
storage=S(String),
|
|
47
|
+
field=F(),
|
|
48
|
+
io=IO(),
|
|
49
|
+
) # row or sentinel
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class RoleGrant(Base, GUIDPk, Timestamped, TenantBound, RelationEdge):
|
|
53
|
+
__tablename__ = "role_grants"
|
|
54
|
+
principal_id: Mapped[UUID] = acol(
|
|
55
|
+
storage=S(PgUUID),
|
|
56
|
+
field=F(),
|
|
57
|
+
io=IO(),
|
|
58
|
+
) # FK to principal row
|
|
59
|
+
role_id: Mapped[UUID] = acol(
|
|
60
|
+
storage=S(PgUUID, fk=ForeignKeySpec("roles.id")),
|
|
61
|
+
field=F(),
|
|
62
|
+
io=IO(),
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
__all__ = ["Role", "RolePerm", "RoleGrant"]
|
|
67
|
+
|
|
68
|
+
for _name in list(globals()):
|
|
69
|
+
if _name not in __all__ and not _name.startswith("__"):
|
|
70
|
+
del globals()[_name]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def __dir__():
|
|
74
|
+
"""Tighten ``dir()`` output for interactive sessions."""
|
|
75
|
+
|
|
76
|
+
return sorted(__all__)
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""
|
|
2
|
+
StatusEnum table
|
|
3
|
+
----------------
|
|
4
|
+
Canonical store of workflow / lifecycle states.
|
|
5
|
+
|
|
6
|
+
• The `Status` enum below should always be the **only** source of truth for
|
|
7
|
+
allowed values. Every place in the codebase ‒ mix-ins, business logic,
|
|
8
|
+
RPC schemas, etc. ‒ should import it instead of hard-coding strings.
|
|
9
|
+
• Domain tables that need a status field should `ForeignKey` to
|
|
10
|
+
`status_enums.code` **or** just declare a `Column(SAEnum(Status, …))`
|
|
11
|
+
if the FK is unnecessary.
|
|
12
|
+
|
|
13
|
+
If you add/remove a state later, just edit the `Status` enum – the rest
|
|
14
|
+
keeps working.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from enum import StrEnum
|
|
20
|
+
|
|
21
|
+
from ...specs import acol, F, IO, S
|
|
22
|
+
from ...types import Integer, Mapped, SAEnum, String
|
|
23
|
+
|
|
24
|
+
from ._base import Base
|
|
25
|
+
from ..mixins import Timestamped # created_at / updated_at
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# ────────────────────────────────────────────────────────────────────────
|
|
29
|
+
# 1. In-memory application enum (single source of truth)
|
|
30
|
+
# ────────────────────────────────────────────────────────────────────────
|
|
31
|
+
class Status(StrEnum):
|
|
32
|
+
# queued / dispatching
|
|
33
|
+
QUEUED = "queued"
|
|
34
|
+
WAITING = "waiting"
|
|
35
|
+
INPUT_REQUIRED = "input_required"
|
|
36
|
+
AUTH_REQUIRED = "auth_required"
|
|
37
|
+
|
|
38
|
+
# approvals
|
|
39
|
+
APPROVED = "approved"
|
|
40
|
+
REJECTED = "rejected"
|
|
41
|
+
|
|
42
|
+
# execution lifecycle
|
|
43
|
+
DISPATCHED = "dispatched"
|
|
44
|
+
RUNNING = "running"
|
|
45
|
+
PAUSED = "paused"
|
|
46
|
+
|
|
47
|
+
# final states
|
|
48
|
+
SUCCESS = "success"
|
|
49
|
+
FAILED = "failed"
|
|
50
|
+
CANCELLED = "cancelled"
|
|
51
|
+
|
|
52
|
+
# (legacy / generic)
|
|
53
|
+
PENDING = "pending" # optional catch-all
|
|
54
|
+
ACTIVE = "active"
|
|
55
|
+
SUSPENDED = "suspended"
|
|
56
|
+
DISABLED = "disabled"
|
|
57
|
+
DELETED = "deleted"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# ────────────────────────────────────────────────────────────────────────
|
|
61
|
+
# 2. Persistent lookup table
|
|
62
|
+
# ────────────────────────────────────────────────────────────────────────
|
|
63
|
+
class StatusEnum(Base, Timestamped):
|
|
64
|
+
"""
|
|
65
|
+
id – surrogate PK (easy FK if needed elsewhere)
|
|
66
|
+
code – canonical string value from the Status enum
|
|
67
|
+
label – human-readable label (“Paused”, “Failed”, …)
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
__tablename__ = "status_enums"
|
|
71
|
+
|
|
72
|
+
id: Mapped[int] = acol(
|
|
73
|
+
storage=S(Integer, primary_key=True, autoincrement=True),
|
|
74
|
+
field=F(),
|
|
75
|
+
io=IO(out_verbs=("read", "list")),
|
|
76
|
+
)
|
|
77
|
+
code: Mapped[Status] = acol(
|
|
78
|
+
storage=S(
|
|
79
|
+
SAEnum(Status, name="status_code_enum"),
|
|
80
|
+
nullable=False,
|
|
81
|
+
unique=True,
|
|
82
|
+
),
|
|
83
|
+
field=F(py_type=Status),
|
|
84
|
+
io=IO(out_verbs=("read", "list")),
|
|
85
|
+
)
|
|
86
|
+
label: Mapped[str] = acol(
|
|
87
|
+
storage=S(String, nullable=False),
|
|
88
|
+
field=F(),
|
|
89
|
+
io=IO(out_verbs=("read", "list")),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
def __repr__(self) -> str: # noqa: D401
|
|
93
|
+
return f"<StatusEnum {self.code}>"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
__all__ = ["Status", "StatusEnum"]
|
|
97
|
+
|
|
98
|
+
for _name in list(globals()):
|
|
99
|
+
if _name not in __all__ and not _name.startswith("__"):
|
|
100
|
+
del globals()[_name]
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def __dir__():
|
|
104
|
+
"""Tighten ``dir()`` output for interactive sessions."""
|
|
105
|
+
|
|
106
|
+
return sorted(__all__)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Tenant model."""
|
|
2
|
+
|
|
3
|
+
from ._base import Base
|
|
4
|
+
from ..mixins import GUIDPk, Slugged, Timestamped
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Tenant(Base, GUIDPk, Slugged, Timestamped):
|
|
8
|
+
__tablename__ = "tenants"
|
|
9
|
+
__abstract__ = True
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
__all__ = ["Tenant"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
for _name in list(globals()):
|
|
16
|
+
if _name not in __all__ and not _name.startswith("__"):
|
|
17
|
+
del globals()[_name]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def __dir__():
|
|
21
|
+
"""Tighten ``dir()`` output for interactive sessions."""
|
|
22
|
+
return sorted(__all__)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""User model."""
|
|
2
|
+
|
|
3
|
+
from ._base import Base
|
|
4
|
+
from ..mixins import (
|
|
5
|
+
GUIDPk,
|
|
6
|
+
Timestamped,
|
|
7
|
+
TenantBound,
|
|
8
|
+
Principal,
|
|
9
|
+
AsyncCapable,
|
|
10
|
+
ActiveToggle,
|
|
11
|
+
)
|
|
12
|
+
from ...specs import IO, acol, F, S
|
|
13
|
+
from ...types import Mapped, String
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class User(
|
|
17
|
+
Base, GUIDPk, Timestamped, TenantBound, Principal, AsyncCapable, ActiveToggle
|
|
18
|
+
):
|
|
19
|
+
__tablename__ = "users"
|
|
20
|
+
__abstract__ = True
|
|
21
|
+
username: Mapped[str] = acol(
|
|
22
|
+
storage=S(String(32), nullable=False),
|
|
23
|
+
field=F(constraints={"max_length": 32}),
|
|
24
|
+
io=IO(),
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
__all__ = ["User"]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
for _name in list(globals()):
|
|
32
|
+
if _name not in __all__ and not _name.startswith("__"):
|
|
33
|
+
del globals()[_name]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def __dir__():
|
|
37
|
+
"""Tighten ``dir()`` output for interactive sessions."""
|
|
38
|
+
|
|
39
|
+
return sorted(__all__)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Response and Template Specs
|
|
2
|
+
|
|
3
|
+
Tigrbl exposes flexible response configuration through the `ResponseSpec` and `TemplateSpec` dataclasses in `tigrbl.response.types`.
|
|
4
|
+
|
|
5
|
+
## `ResponseSpec`
|
|
6
|
+
|
|
7
|
+
`ResponseSpec` controls how an operation returns data:
|
|
8
|
+
|
|
9
|
+
- **`kind`** – response mode: `"auto"`, `"json"`, `"html"`, `"text"`, `"file"`, `"stream"`, or `"redirect"`.
|
|
10
|
+
- **`media_type`** – explicit `Content-Type` header.
|
|
11
|
+
- **`status_code`** – HTTP status override.
|
|
12
|
+
- **`headers`** – mapping of additional headers.
|
|
13
|
+
- **`envelope`** – wrap payloads in a standard envelope when `True`.
|
|
14
|
+
- **`template`** – optional [`TemplateSpec`](#templatespec) to render an HTML response.
|
|
15
|
+
- **`filename`** – file name for `file` responses.
|
|
16
|
+
- **`download`** – force browser downloads when `True`.
|
|
17
|
+
- **`etag`** – ETag header value.
|
|
18
|
+
- **`cache_control`** – `Cache-Control` header value.
|
|
19
|
+
- **`redirect_to`** – target location for `redirect` responses.
|
|
20
|
+
|
|
21
|
+
## `TemplateSpec`
|
|
22
|
+
|
|
23
|
+
`TemplateSpec` defines how templates are resolved and rendered:
|
|
24
|
+
|
|
25
|
+
- **`name`** – template identifier passed to the renderer.
|
|
26
|
+
- **`search_paths`** – directories to search for template files.
|
|
27
|
+
- **`package`** – Python package providing templates via `importlib.resources`.
|
|
28
|
+
- **`auto_reload`** – when `True`, reload templates on each request (useful in development).
|
|
29
|
+
- **`filters`** – custom Jinja2 filters.
|
|
30
|
+
- **`globals`** – additional template globals.
|
|
31
|
+
|
|
32
|
+
A `TemplateSpec` may be nested inside a `ResponseSpec`'s `template` field to render HTML output. When provided, the runtime invokes `render_template` and populates the response body with the rendered HTML.
|
|
33
|
+
|
|
34
|
+
These specs offer a declarative way to tailor outbound responses and integrate server-side templates without manual response handling.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from .decorators import response_ctx, get_attached_response_spec
|
|
2
|
+
from .types import (
|
|
3
|
+
Response,
|
|
4
|
+
ResponseKind,
|
|
5
|
+
ResponseSpec,
|
|
6
|
+
Template,
|
|
7
|
+
TemplateSpec,
|
|
8
|
+
)
|
|
9
|
+
from .resolver import resolve_response_spec, infer_hints
|
|
10
|
+
from .shortcuts import as_json, as_html, as_text, as_redirect, as_stream, as_file
|
|
11
|
+
from ..runtime.atoms.response.renderer import ResponseHints, render
|
|
12
|
+
from ..runtime.atoms.response.templates import render_template
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"response_ctx",
|
|
16
|
+
"get_attached_response_spec",
|
|
17
|
+
"ResponseSpec",
|
|
18
|
+
"ResponseKind",
|
|
19
|
+
"TemplateSpec",
|
|
20
|
+
"Response",
|
|
21
|
+
"Template",
|
|
22
|
+
"resolve_response_spec",
|
|
23
|
+
"infer_hints",
|
|
24
|
+
"as_json",
|
|
25
|
+
"as_html",
|
|
26
|
+
"as_text",
|
|
27
|
+
"as_redirect",
|
|
28
|
+
"as_stream",
|
|
29
|
+
"as_file",
|
|
30
|
+
"ResponseHints",
|
|
31
|
+
"render",
|
|
32
|
+
"render_template",
|
|
33
|
+
]
|
tigrbl/response/bind.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Response binding helpers (placeholder)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from typing import Any, Dict
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def bind(collected: Dict[str, Any]) -> None: # pragma: no cover - trivial
|
|
8
|
+
"""Bind collected response configuration. Currently a no-op."""
|
|
9
|
+
return None
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
__all__ = ["bind"]
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Any, Callable, Optional, TypeVar, overload
|
|
3
|
+
|
|
4
|
+
from .types import ResponseSpec
|
|
5
|
+
|
|
6
|
+
T = TypeVar("T")
|
|
7
|
+
_ATTR = "__tigrbl_response_spec__"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _to_spec(spec: Optional[ResponseSpec] = None, **kwargs: Any) -> ResponseSpec:
|
|
11
|
+
if spec is not None and kwargs:
|
|
12
|
+
raise TypeError("response_ctx: provide either a ResponseSpec or keyword args")
|
|
13
|
+
if spec is not None:
|
|
14
|
+
return spec
|
|
15
|
+
return ResponseSpec(**kwargs)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@overload
|
|
19
|
+
def response_ctx(spec: ResponseSpec) -> Callable[[T], T]: ...
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@overload
|
|
23
|
+
def response_ctx(**kwargs: Any) -> Callable[[T], T]: ...
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def response_ctx(*args: Any, **kwargs: Any) -> Callable[[T], T]:
|
|
27
|
+
spec = _to_spec(*args, **kwargs)
|
|
28
|
+
|
|
29
|
+
def decorator(target: T) -> T:
|
|
30
|
+
setattr(target, _ATTR, spec)
|
|
31
|
+
return target
|
|
32
|
+
|
|
33
|
+
return decorator
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def get_attached_response_spec(obj: Any) -> Optional[ResponseSpec]:
|
|
37
|
+
return getattr(obj, _ATTR, None)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Dict, Optional
|
|
3
|
+
|
|
4
|
+
from dataclasses import fields
|
|
5
|
+
from .types import ResponseSpec, TemplateSpec
|
|
6
|
+
from ..runtime.atoms.response.renderer import ResponseHints
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _merge_template(
|
|
10
|
+
base: Optional[TemplateSpec], over: Optional[TemplateSpec]
|
|
11
|
+
) -> Optional[TemplateSpec]:
|
|
12
|
+
if over is None:
|
|
13
|
+
return base
|
|
14
|
+
if base is None:
|
|
15
|
+
return over
|
|
16
|
+
paths = list(base.search_paths)
|
|
17
|
+
for p in over.search_paths:
|
|
18
|
+
if p not in paths:
|
|
19
|
+
paths.append(p)
|
|
20
|
+
return TemplateSpec(
|
|
21
|
+
name=over.name or base.name,
|
|
22
|
+
search_paths=paths,
|
|
23
|
+
package=over.package or base.package,
|
|
24
|
+
auto_reload=over.auto_reload
|
|
25
|
+
if over.auto_reload is not None
|
|
26
|
+
else base.auto_reload,
|
|
27
|
+
filters={**base.filters, **over.filters},
|
|
28
|
+
globals={**base.globals, **over.globals},
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _merge(
|
|
33
|
+
base: Optional[ResponseSpec], over: Optional[ResponseSpec]
|
|
34
|
+
) -> Optional[ResponseSpec]:
|
|
35
|
+
if over is None:
|
|
36
|
+
return base
|
|
37
|
+
if base is None:
|
|
38
|
+
return over
|
|
39
|
+
data = {f.name: getattr(base, f.name) for f in fields(base)}
|
|
40
|
+
for f in fields(over):
|
|
41
|
+
v = getattr(over, f.name)
|
|
42
|
+
if v is not None:
|
|
43
|
+
if f.name == "template":
|
|
44
|
+
data[f.name] = _merge_template(getattr(base, f.name), v)
|
|
45
|
+
elif isinstance(v, dict) and isinstance(data.get(f.name), dict):
|
|
46
|
+
d = dict(data.get(f.name))
|
|
47
|
+
d.update(v)
|
|
48
|
+
data[f.name] = d
|
|
49
|
+
else:
|
|
50
|
+
data[f.name] = v
|
|
51
|
+
return ResponseSpec(**data)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def resolve_response_spec(
|
|
55
|
+
*candidates: Optional[ResponseSpec],
|
|
56
|
+
) -> Optional[ResponseSpec]:
|
|
57
|
+
spec: Optional[ResponseSpec] = None
|
|
58
|
+
for c in candidates:
|
|
59
|
+
spec = _merge(spec, c)
|
|
60
|
+
return spec
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def infer_hints(
|
|
64
|
+
spec: Optional[ResponseSpec],
|
|
65
|
+
) -> tuple[ResponseHints, Optional[bool], Optional[str]]:
|
|
66
|
+
if spec is None:
|
|
67
|
+
return ResponseHints(), None, None
|
|
68
|
+
headers: Dict[str, str] = dict(spec.headers or {})
|
|
69
|
+
if spec.cache_control:
|
|
70
|
+
headers.setdefault("Cache-Control", spec.cache_control)
|
|
71
|
+
hints = ResponseHints(
|
|
72
|
+
media_type=spec.media_type,
|
|
73
|
+
status_code=spec.status_code or 200,
|
|
74
|
+
headers=headers,
|
|
75
|
+
filename=spec.filename,
|
|
76
|
+
download=bool(spec.download) if spec.download is not None else False,
|
|
77
|
+
etag=spec.etag,
|
|
78
|
+
)
|
|
79
|
+
default_media = spec.media_type
|
|
80
|
+
return hints, spec.envelope, default_media
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
__all__ = ["resolve_response_spec", "infer_hints"]
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Any, AsyncIterable, Iterable, Mapping, Optional, Union
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import mimetypes
|
|
8
|
+
|
|
9
|
+
from ..deps.starlette import (
|
|
10
|
+
JSONResponse,
|
|
11
|
+
HTMLResponse,
|
|
12
|
+
PlainTextResponse,
|
|
13
|
+
StreamingResponse,
|
|
14
|
+
FileResponse as StarletteFileResponse,
|
|
15
|
+
RedirectResponse,
|
|
16
|
+
Response,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
import orjson as _orjson
|
|
21
|
+
|
|
22
|
+
def _dumps(obj: Any) -> bytes:
|
|
23
|
+
return _orjson.dumps(
|
|
24
|
+
obj, option=_orjson.OPT_NON_STR_KEYS | _orjson.OPT_SERIALIZE_NUMPY
|
|
25
|
+
)
|
|
26
|
+
except Exception: # pragma: no cover - fallback
|
|
27
|
+
|
|
28
|
+
def _dumps(obj: Any) -> bytes:
|
|
29
|
+
return json.dumps(
|
|
30
|
+
obj,
|
|
31
|
+
separators=(",", ":"),
|
|
32
|
+
ensure_ascii=False,
|
|
33
|
+
default=str,
|
|
34
|
+
).encode("utf-8")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _maybe_envelope(data: Any) -> Any:
|
|
38
|
+
if isinstance(data, Mapping) and ("data" in data or "error" in data):
|
|
39
|
+
return data
|
|
40
|
+
return {"data": data, "ok": True}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
JSON = Mapping[str, Any]
|
|
44
|
+
Headers = Mapping[str, str]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def as_json(
|
|
48
|
+
data: Any,
|
|
49
|
+
*,
|
|
50
|
+
status: int = 200,
|
|
51
|
+
headers: Optional[Headers] = None,
|
|
52
|
+
envelope: bool = True,
|
|
53
|
+
dumps=_dumps,
|
|
54
|
+
) -> Response:
|
|
55
|
+
payload = _maybe_envelope(data) if envelope else data
|
|
56
|
+
try:
|
|
57
|
+
return JSONResponse(
|
|
58
|
+
payload,
|
|
59
|
+
status_code=status,
|
|
60
|
+
headers=dict(headers or {}),
|
|
61
|
+
dumps=lambda o: dumps(o).decode(),
|
|
62
|
+
)
|
|
63
|
+
except TypeError: # pragma: no cover - starlette >= 0.44
|
|
64
|
+
return Response(
|
|
65
|
+
dumps(payload),
|
|
66
|
+
status_code=status,
|
|
67
|
+
headers=dict(headers or {}),
|
|
68
|
+
media_type="application/json",
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def as_html(
|
|
73
|
+
html: str, *, status: int = 200, headers: Optional[Headers] = None
|
|
74
|
+
) -> Response:
|
|
75
|
+
return HTMLResponse(html, status_code=status, headers=dict(headers or {}))
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def as_text(
|
|
79
|
+
text: str, *, status: int = 200, headers: Optional[Headers] = None
|
|
80
|
+
) -> Response:
|
|
81
|
+
return PlainTextResponse(text, status_code=status, headers=dict(headers or {}))
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def as_redirect(
|
|
85
|
+
url: str, *, status: int = 307, headers: Optional[Headers] = None
|
|
86
|
+
) -> Response:
|
|
87
|
+
return RedirectResponse(url, status_code=status, headers=dict(headers or {}))
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def as_stream(
|
|
91
|
+
chunks: Union[Iterable[bytes], AsyncIterable[bytes]],
|
|
92
|
+
*,
|
|
93
|
+
media_type: str = "application/octet-stream",
|
|
94
|
+
status: int = 200,
|
|
95
|
+
headers: Optional[Headers] = None,
|
|
96
|
+
) -> Response:
|
|
97
|
+
return StreamingResponse(
|
|
98
|
+
chunks, media_type=media_type, status_code=status, headers=dict(headers or {})
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def as_file(
|
|
103
|
+
path: Union[str, Path],
|
|
104
|
+
*,
|
|
105
|
+
filename: Optional[str] = None,
|
|
106
|
+
download: bool = False,
|
|
107
|
+
status: int = 200,
|
|
108
|
+
headers: Optional[Headers] = None,
|
|
109
|
+
stat_result: Optional[os.stat_result] = None,
|
|
110
|
+
etag: Optional[str] = None,
|
|
111
|
+
last_modified: Optional[datetime] = None,
|
|
112
|
+
) -> Response:
|
|
113
|
+
p = Path(path)
|
|
114
|
+
if not p.exists() or not p.is_file():
|
|
115
|
+
return PlainTextResponse("Not Found", status_code=404)
|
|
116
|
+
media_type, _ = mimetypes.guess_type(str(p))
|
|
117
|
+
media_type = media_type or "application/octet-stream"
|
|
118
|
+
hdrs = dict(headers or {})
|
|
119
|
+
st = stat_result or os.stat(p)
|
|
120
|
+
if etag is None:
|
|
121
|
+
etag = f'W/"{st.st_mtime_ns}-{st.st_size}"'
|
|
122
|
+
lm = last_modified or datetime.fromtimestamp(st.st_mtime, tz=timezone.utc)
|
|
123
|
+
hdrs.setdefault("ETag", etag)
|
|
124
|
+
hdrs.setdefault("Last-Modified", lm.strftime("%a, %d %b %Y %H:%M:%S GMT"))
|
|
125
|
+
if download or filename:
|
|
126
|
+
fname = filename or p.name
|
|
127
|
+
hdrs.setdefault("Content-Disposition", f'attachment; filename="{fname}"')
|
|
128
|
+
return StarletteFileResponse(
|
|
129
|
+
str(p),
|
|
130
|
+
status_code=status,
|
|
131
|
+
media_type=media_type,
|
|
132
|
+
filename=filename,
|
|
133
|
+
headers=hdrs,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
__all__ = [
|
|
138
|
+
"as_json",
|
|
139
|
+
"as_html",
|
|
140
|
+
"as_text",
|
|
141
|
+
"as_redirect",
|
|
142
|
+
"as_stream",
|
|
143
|
+
"as_file",
|
|
144
|
+
]
|