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.
Files changed (252) hide show
  1. tigrbl/README.md +94 -0
  2. tigrbl/__init__.py +139 -14
  3. tigrbl/api/__init__.py +6 -0
  4. tigrbl/api/_api.py +72 -0
  5. tigrbl/api/api_spec.py +30 -0
  6. tigrbl/api/mro_collect.py +43 -0
  7. tigrbl/api/shortcuts.py +56 -0
  8. tigrbl/api/tigrbl_api.py +286 -0
  9. tigrbl/app/__init__.py +0 -0
  10. tigrbl/app/_app.py +61 -0
  11. tigrbl/app/app_spec.py +42 -0
  12. tigrbl/app/mro_collect.py +67 -0
  13. tigrbl/app/shortcuts.py +65 -0
  14. tigrbl/app/tigrbl_app.py +314 -0
  15. tigrbl/bindings/__init__.py +73 -0
  16. tigrbl/bindings/api/__init__.py +12 -0
  17. tigrbl/bindings/api/common.py +109 -0
  18. tigrbl/bindings/api/include.py +256 -0
  19. tigrbl/bindings/api/resource_proxy.py +149 -0
  20. tigrbl/bindings/api/rpc.py +111 -0
  21. tigrbl/bindings/columns.py +49 -0
  22. tigrbl/bindings/handlers/__init__.py +11 -0
  23. tigrbl/bindings/handlers/builder.py +119 -0
  24. tigrbl/bindings/handlers/ctx.py +74 -0
  25. tigrbl/bindings/handlers/identifiers.py +228 -0
  26. tigrbl/bindings/handlers/namespaces.py +51 -0
  27. tigrbl/bindings/handlers/steps.py +276 -0
  28. tigrbl/bindings/hooks.py +311 -0
  29. tigrbl/bindings/model.py +194 -0
  30. tigrbl/bindings/model_helpers.py +139 -0
  31. tigrbl/bindings/model_registry.py +77 -0
  32. tigrbl/bindings/rest/__init__.py +7 -0
  33. tigrbl/bindings/rest/attach.py +34 -0
  34. tigrbl/bindings/rest/collection.py +265 -0
  35. tigrbl/bindings/rest/common.py +116 -0
  36. tigrbl/bindings/rest/fastapi.py +76 -0
  37. tigrbl/bindings/rest/helpers.py +119 -0
  38. tigrbl/bindings/rest/io.py +317 -0
  39. tigrbl/bindings/rest/member.py +367 -0
  40. tigrbl/bindings/rest/router.py +292 -0
  41. tigrbl/bindings/rest/routing.py +133 -0
  42. tigrbl/bindings/rpc.py +364 -0
  43. tigrbl/bindings/schemas/__init__.py +11 -0
  44. tigrbl/bindings/schemas/builder.py +348 -0
  45. tigrbl/bindings/schemas/defaults.py +260 -0
  46. tigrbl/bindings/schemas/utils.py +193 -0
  47. tigrbl/column/README.md +62 -0
  48. tigrbl/column/__init__.py +72 -0
  49. tigrbl/column/_column.py +96 -0
  50. tigrbl/column/column_spec.py +40 -0
  51. tigrbl/column/field_spec.py +31 -0
  52. tigrbl/column/infer/__init__.py +25 -0
  53. tigrbl/column/infer/core.py +92 -0
  54. tigrbl/column/infer/jsonhints.py +44 -0
  55. tigrbl/column/infer/planning.py +133 -0
  56. tigrbl/column/infer/types.py +102 -0
  57. tigrbl/column/infer/utils.py +59 -0
  58. tigrbl/column/io_spec.py +133 -0
  59. tigrbl/column/mro_collect.py +59 -0
  60. tigrbl/column/shortcuts.py +89 -0
  61. tigrbl/column/storage_spec.py +65 -0
  62. tigrbl/config/__init__.py +19 -0
  63. tigrbl/config/constants.py +224 -0
  64. tigrbl/config/defaults.py +29 -0
  65. tigrbl/config/resolver.py +295 -0
  66. tigrbl/core/__init__.py +47 -0
  67. tigrbl/core/crud/__init__.py +36 -0
  68. tigrbl/core/crud/bulk.py +168 -0
  69. tigrbl/core/crud/helpers/__init__.py +76 -0
  70. tigrbl/core/crud/helpers/db.py +92 -0
  71. tigrbl/core/crud/helpers/enum.py +86 -0
  72. tigrbl/core/crud/helpers/filters.py +162 -0
  73. tigrbl/core/crud/helpers/model.py +123 -0
  74. tigrbl/core/crud/helpers/normalize.py +99 -0
  75. tigrbl/core/crud/ops.py +235 -0
  76. tigrbl/ddl/__init__.py +344 -0
  77. tigrbl/decorators.py +17 -0
  78. tigrbl/deps/__init__.py +20 -0
  79. tigrbl/deps/fastapi.py +45 -0
  80. tigrbl/deps/favicon.svg +4 -0
  81. tigrbl/deps/jinja.py +27 -0
  82. tigrbl/deps/pydantic.py +10 -0
  83. tigrbl/deps/sqlalchemy.py +94 -0
  84. tigrbl/deps/starlette.py +36 -0
  85. tigrbl/engine/__init__.py +26 -0
  86. tigrbl/engine/_engine.py +130 -0
  87. tigrbl/engine/bind.py +33 -0
  88. tigrbl/engine/builders.py +236 -0
  89. tigrbl/engine/collect.py +111 -0
  90. tigrbl/engine/decorators.py +108 -0
  91. tigrbl/engine/engine_spec.py +261 -0
  92. tigrbl/engine/resolver.py +224 -0
  93. tigrbl/engine/shortcuts.py +216 -0
  94. tigrbl/hook/__init__.py +21 -0
  95. tigrbl/hook/_hook.py +22 -0
  96. tigrbl/hook/decorators.py +28 -0
  97. tigrbl/hook/hook_spec.py +24 -0
  98. tigrbl/hook/mro_collect.py +98 -0
  99. tigrbl/hook/shortcuts.py +44 -0
  100. tigrbl/hook/types.py +76 -0
  101. tigrbl/op/__init__.py +50 -0
  102. tigrbl/op/_op.py +31 -0
  103. tigrbl/op/canonical.py +31 -0
  104. tigrbl/op/collect.py +11 -0
  105. tigrbl/op/decorators.py +238 -0
  106. tigrbl/op/model_registry.py +301 -0
  107. tigrbl/op/mro_collect.py +99 -0
  108. tigrbl/op/resolver.py +216 -0
  109. tigrbl/op/types.py +136 -0
  110. tigrbl/orm/__init__.py +1 -0
  111. tigrbl/orm/mixins/_RowBound.py +83 -0
  112. tigrbl/orm/mixins/__init__.py +95 -0
  113. tigrbl/orm/mixins/bootstrappable.py +113 -0
  114. tigrbl/orm/mixins/bound.py +47 -0
  115. tigrbl/orm/mixins/edges.py +40 -0
  116. tigrbl/orm/mixins/fields.py +165 -0
  117. tigrbl/orm/mixins/hierarchy.py +54 -0
  118. tigrbl/orm/mixins/key_digest.py +44 -0
  119. tigrbl/orm/mixins/lifecycle.py +115 -0
  120. tigrbl/orm/mixins/locks.py +51 -0
  121. tigrbl/orm/mixins/markers.py +16 -0
  122. tigrbl/orm/mixins/operations.py +57 -0
  123. tigrbl/orm/mixins/ownable.py +337 -0
  124. tigrbl/orm/mixins/principals.py +98 -0
  125. tigrbl/orm/mixins/tenant_bound.py +301 -0
  126. tigrbl/orm/mixins/upsertable.py +111 -0
  127. tigrbl/orm/mixins/utils.py +49 -0
  128. tigrbl/orm/tables/__init__.py +72 -0
  129. tigrbl/orm/tables/_base.py +8 -0
  130. tigrbl/orm/tables/audit.py +56 -0
  131. tigrbl/orm/tables/client.py +25 -0
  132. tigrbl/orm/tables/group.py +29 -0
  133. tigrbl/orm/tables/org.py +30 -0
  134. tigrbl/orm/tables/rbac.py +76 -0
  135. tigrbl/orm/tables/status.py +106 -0
  136. tigrbl/orm/tables/tenant.py +22 -0
  137. tigrbl/orm/tables/user.py +39 -0
  138. tigrbl/response/README.md +34 -0
  139. tigrbl/response/__init__.py +33 -0
  140. tigrbl/response/bind.py +12 -0
  141. tigrbl/response/decorators.py +37 -0
  142. tigrbl/response/resolver.py +83 -0
  143. tigrbl/response/shortcuts.py +144 -0
  144. tigrbl/response/types.py +49 -0
  145. tigrbl/rest/__init__.py +27 -0
  146. tigrbl/runtime/README.md +129 -0
  147. tigrbl/runtime/__init__.py +20 -0
  148. tigrbl/runtime/atoms/__init__.py +102 -0
  149. tigrbl/runtime/atoms/emit/__init__.py +42 -0
  150. tigrbl/runtime/atoms/emit/paired_post.py +158 -0
  151. tigrbl/runtime/atoms/emit/paired_pre.py +106 -0
  152. tigrbl/runtime/atoms/emit/readtime_alias.py +120 -0
  153. tigrbl/runtime/atoms/out/__init__.py +38 -0
  154. tigrbl/runtime/atoms/out/masking.py +135 -0
  155. tigrbl/runtime/atoms/refresh/__init__.py +38 -0
  156. tigrbl/runtime/atoms/refresh/demand.py +130 -0
  157. tigrbl/runtime/atoms/resolve/__init__.py +40 -0
  158. tigrbl/runtime/atoms/resolve/assemble.py +167 -0
  159. tigrbl/runtime/atoms/resolve/paired_gen.py +147 -0
  160. tigrbl/runtime/atoms/response/__init__.py +17 -0
  161. tigrbl/runtime/atoms/response/negotiate.py +30 -0
  162. tigrbl/runtime/atoms/response/negotiation.py +43 -0
  163. tigrbl/runtime/atoms/response/render.py +36 -0
  164. tigrbl/runtime/atoms/response/renderer.py +116 -0
  165. tigrbl/runtime/atoms/response/template.py +44 -0
  166. tigrbl/runtime/atoms/response/templates.py +88 -0
  167. tigrbl/runtime/atoms/schema/__init__.py +40 -0
  168. tigrbl/runtime/atoms/schema/collect_in.py +21 -0
  169. tigrbl/runtime/atoms/schema/collect_out.py +21 -0
  170. tigrbl/runtime/atoms/storage/__init__.py +38 -0
  171. tigrbl/runtime/atoms/storage/to_stored.py +167 -0
  172. tigrbl/runtime/atoms/wire/__init__.py +45 -0
  173. tigrbl/runtime/atoms/wire/build_in.py +166 -0
  174. tigrbl/runtime/atoms/wire/build_out.py +87 -0
  175. tigrbl/runtime/atoms/wire/dump.py +206 -0
  176. tigrbl/runtime/atoms/wire/validate_in.py +227 -0
  177. tigrbl/runtime/context.py +206 -0
  178. tigrbl/runtime/errors/__init__.py +61 -0
  179. tigrbl/runtime/errors/converters.py +214 -0
  180. tigrbl/runtime/errors/exceptions.py +124 -0
  181. tigrbl/runtime/errors/mappings.py +71 -0
  182. tigrbl/runtime/errors/utils.py +150 -0
  183. tigrbl/runtime/events.py +209 -0
  184. tigrbl/runtime/executor/__init__.py +6 -0
  185. tigrbl/runtime/executor/guards.py +132 -0
  186. tigrbl/runtime/executor/helpers.py +88 -0
  187. tigrbl/runtime/executor/invoke.py +150 -0
  188. tigrbl/runtime/executor/types.py +84 -0
  189. tigrbl/runtime/kernel.py +628 -0
  190. tigrbl/runtime/labels.py +353 -0
  191. tigrbl/runtime/opview.py +87 -0
  192. tigrbl/runtime/ordering.py +256 -0
  193. tigrbl/runtime/system.py +279 -0
  194. tigrbl/runtime/trace.py +330 -0
  195. tigrbl/schema/__init__.py +38 -0
  196. tigrbl/schema/_schema.py +27 -0
  197. tigrbl/schema/builder/__init__.py +17 -0
  198. tigrbl/schema/builder/build_schema.py +209 -0
  199. tigrbl/schema/builder/cache.py +24 -0
  200. tigrbl/schema/builder/compat.py +16 -0
  201. tigrbl/schema/builder/extras.py +85 -0
  202. tigrbl/schema/builder/helpers.py +51 -0
  203. tigrbl/schema/builder/list_params.py +117 -0
  204. tigrbl/schema/builder/strip_parent_fields.py +70 -0
  205. tigrbl/schema/collect.py +55 -0
  206. tigrbl/schema/decorators.py +68 -0
  207. tigrbl/schema/get_schema.py +86 -0
  208. tigrbl/schema/schema_spec.py +20 -0
  209. tigrbl/schema/shortcuts.py +42 -0
  210. tigrbl/schema/types.py +34 -0
  211. tigrbl/schema/utils.py +143 -0
  212. tigrbl/shortcuts.py +22 -0
  213. tigrbl/specs.py +44 -0
  214. tigrbl/system/__init__.py +12 -0
  215. tigrbl/system/diagnostics/__init__.py +24 -0
  216. tigrbl/system/diagnostics/compat.py +31 -0
  217. tigrbl/system/diagnostics/healthz.py +41 -0
  218. tigrbl/system/diagnostics/hookz.py +51 -0
  219. tigrbl/system/diagnostics/kernelz.py +20 -0
  220. tigrbl/system/diagnostics/methodz.py +43 -0
  221. tigrbl/system/diagnostics/router.py +73 -0
  222. tigrbl/system/diagnostics/utils.py +43 -0
  223. tigrbl/table/__init__.py +9 -0
  224. tigrbl/table/_base.py +237 -0
  225. tigrbl/table/_table.py +54 -0
  226. tigrbl/table/mro_collect.py +69 -0
  227. tigrbl/table/shortcuts.py +57 -0
  228. tigrbl/table/table_spec.py +28 -0
  229. tigrbl/transport/__init__.py +74 -0
  230. tigrbl/transport/jsonrpc/__init__.py +19 -0
  231. tigrbl/transport/jsonrpc/dispatcher.py +352 -0
  232. tigrbl/transport/jsonrpc/helpers.py +115 -0
  233. tigrbl/transport/jsonrpc/models.py +41 -0
  234. tigrbl/transport/rest/__init__.py +25 -0
  235. tigrbl/transport/rest/aggregator.py +132 -0
  236. tigrbl/types/__init__.py +174 -0
  237. tigrbl/types/allow_anon_provider.py +19 -0
  238. tigrbl/types/authn_abc.py +30 -0
  239. tigrbl/types/nested_path_provider.py +22 -0
  240. tigrbl/types/op.py +35 -0
  241. tigrbl/types/op_config_provider.py +17 -0
  242. tigrbl/types/op_verb_alias_provider.py +33 -0
  243. tigrbl/types/request_extras_provider.py +22 -0
  244. tigrbl/types/response_extras_provider.py +22 -0
  245. tigrbl/types/table_config_provider.py +13 -0
  246. tigrbl-0.3.0.dev3.dist-info/LICENSE +201 -0
  247. tigrbl-0.3.0.dev3.dist-info/METADATA +501 -0
  248. tigrbl-0.3.0.dev3.dist-info/RECORD +249 -0
  249. tigrbl/ExampleAgent.py +0 -1
  250. tigrbl-0.0.1.dev1.dist-info/METADATA +0 -18
  251. tigrbl-0.0.1.dev1.dist-info/RECORD +0 -5
  252. {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__)
@@ -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
+ ]
@@ -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
+ ]