tigrbl 0.0.1.dev1__py3-none-any.whl → 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (269) 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 +97 -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 +291 -0
  9. tigrbl/app/__init__.py +0 -0
  10. tigrbl/app/_app.py +86 -0
  11. tigrbl/app/_model_registry.py +41 -0
  12. tigrbl/app/app_spec.py +42 -0
  13. tigrbl/app/mro_collect.py +67 -0
  14. tigrbl/app/shortcuts.py +65 -0
  15. tigrbl/app/tigrbl_app.py +319 -0
  16. tigrbl/bindings/__init__.py +73 -0
  17. tigrbl/bindings/api/__init__.py +12 -0
  18. tigrbl/bindings/api/common.py +109 -0
  19. tigrbl/bindings/api/include.py +256 -0
  20. tigrbl/bindings/api/resource_proxy.py +149 -0
  21. tigrbl/bindings/api/rpc.py +111 -0
  22. tigrbl/bindings/columns.py +49 -0
  23. tigrbl/bindings/handlers/__init__.py +11 -0
  24. tigrbl/bindings/handlers/builder.py +119 -0
  25. tigrbl/bindings/handlers/ctx.py +74 -0
  26. tigrbl/bindings/handlers/identifiers.py +228 -0
  27. tigrbl/bindings/handlers/namespaces.py +51 -0
  28. tigrbl/bindings/handlers/steps.py +276 -0
  29. tigrbl/bindings/hooks.py +311 -0
  30. tigrbl/bindings/model.py +194 -0
  31. tigrbl/bindings/model_helpers.py +139 -0
  32. tigrbl/bindings/model_registry.py +77 -0
  33. tigrbl/bindings/rest/__init__.py +7 -0
  34. tigrbl/bindings/rest/attach.py +34 -0
  35. tigrbl/bindings/rest/collection.py +286 -0
  36. tigrbl/bindings/rest/common.py +120 -0
  37. tigrbl/bindings/rest/fastapi.py +76 -0
  38. tigrbl/bindings/rest/helpers.py +119 -0
  39. tigrbl/bindings/rest/io.py +317 -0
  40. tigrbl/bindings/rest/io_headers.py +49 -0
  41. tigrbl/bindings/rest/member.py +386 -0
  42. tigrbl/bindings/rest/router.py +296 -0
  43. tigrbl/bindings/rest/routing.py +153 -0
  44. tigrbl/bindings/rpc.py +364 -0
  45. tigrbl/bindings/schemas/__init__.py +11 -0
  46. tigrbl/bindings/schemas/builder.py +348 -0
  47. tigrbl/bindings/schemas/defaults.py +260 -0
  48. tigrbl/bindings/schemas/utils.py +193 -0
  49. tigrbl/column/README.md +62 -0
  50. tigrbl/column/__init__.py +72 -0
  51. tigrbl/column/_column.py +96 -0
  52. tigrbl/column/column_spec.py +40 -0
  53. tigrbl/column/field_spec.py +31 -0
  54. tigrbl/column/infer/__init__.py +25 -0
  55. tigrbl/column/infer/core.py +92 -0
  56. tigrbl/column/infer/jsonhints.py +44 -0
  57. tigrbl/column/infer/planning.py +133 -0
  58. tigrbl/column/infer/types.py +102 -0
  59. tigrbl/column/infer/utils.py +59 -0
  60. tigrbl/column/io_spec.py +136 -0
  61. tigrbl/column/mro_collect.py +59 -0
  62. tigrbl/column/shortcuts.py +89 -0
  63. tigrbl/column/storage_spec.py +65 -0
  64. tigrbl/config/__init__.py +19 -0
  65. tigrbl/config/constants.py +224 -0
  66. tigrbl/config/defaults.py +29 -0
  67. tigrbl/config/resolver.py +295 -0
  68. tigrbl/core/__init__.py +47 -0
  69. tigrbl/core/crud/__init__.py +36 -0
  70. tigrbl/core/crud/bulk.py +168 -0
  71. tigrbl/core/crud/helpers/__init__.py +76 -0
  72. tigrbl/core/crud/helpers/db.py +92 -0
  73. tigrbl/core/crud/helpers/enum.py +86 -0
  74. tigrbl/core/crud/helpers/filters.py +162 -0
  75. tigrbl/core/crud/helpers/model.py +123 -0
  76. tigrbl/core/crud/helpers/normalize.py +99 -0
  77. tigrbl/core/crud/ops.py +235 -0
  78. tigrbl/ddl/__init__.py +344 -0
  79. tigrbl/decorators.py +17 -0
  80. tigrbl/deps/__init__.py +20 -0
  81. tigrbl/deps/fastapi.py +45 -0
  82. tigrbl/deps/favicon.svg +4 -0
  83. tigrbl/deps/jinja.py +27 -0
  84. tigrbl/deps/pydantic.py +10 -0
  85. tigrbl/deps/sqlalchemy.py +94 -0
  86. tigrbl/deps/starlette.py +36 -0
  87. tigrbl/engine/__init__.py +45 -0
  88. tigrbl/engine/_engine.py +144 -0
  89. tigrbl/engine/bind.py +33 -0
  90. tigrbl/engine/builders.py +236 -0
  91. tigrbl/engine/capabilities.py +29 -0
  92. tigrbl/engine/collect.py +111 -0
  93. tigrbl/engine/decorators.py +110 -0
  94. tigrbl/engine/docs/PLUGINS.md +49 -0
  95. tigrbl/engine/engine_spec.py +355 -0
  96. tigrbl/engine/plugins.py +52 -0
  97. tigrbl/engine/registry.py +36 -0
  98. tigrbl/engine/resolver.py +224 -0
  99. tigrbl/engine/shortcuts.py +216 -0
  100. tigrbl/hook/__init__.py +21 -0
  101. tigrbl/hook/_hook.py +22 -0
  102. tigrbl/hook/decorators.py +28 -0
  103. tigrbl/hook/hook_spec.py +24 -0
  104. tigrbl/hook/mro_collect.py +98 -0
  105. tigrbl/hook/shortcuts.py +44 -0
  106. tigrbl/hook/types.py +76 -0
  107. tigrbl/op/__init__.py +50 -0
  108. tigrbl/op/_op.py +31 -0
  109. tigrbl/op/canonical.py +31 -0
  110. tigrbl/op/collect.py +11 -0
  111. tigrbl/op/decorators.py +238 -0
  112. tigrbl/op/model_registry.py +301 -0
  113. tigrbl/op/mro_collect.py +99 -0
  114. tigrbl/op/resolver.py +216 -0
  115. tigrbl/op/types.py +136 -0
  116. tigrbl/orm/__init__.py +1 -0
  117. tigrbl/orm/mixins/_RowBound.py +83 -0
  118. tigrbl/orm/mixins/__init__.py +95 -0
  119. tigrbl/orm/mixins/bootstrappable.py +113 -0
  120. tigrbl/orm/mixins/bound.py +47 -0
  121. tigrbl/orm/mixins/edges.py +40 -0
  122. tigrbl/orm/mixins/fields.py +165 -0
  123. tigrbl/orm/mixins/hierarchy.py +54 -0
  124. tigrbl/orm/mixins/key_digest.py +44 -0
  125. tigrbl/orm/mixins/lifecycle.py +115 -0
  126. tigrbl/orm/mixins/locks.py +51 -0
  127. tigrbl/orm/mixins/markers.py +16 -0
  128. tigrbl/orm/mixins/operations.py +57 -0
  129. tigrbl/orm/mixins/ownable.py +337 -0
  130. tigrbl/orm/mixins/principals.py +98 -0
  131. tigrbl/orm/mixins/tenant_bound.py +301 -0
  132. tigrbl/orm/mixins/upsertable.py +118 -0
  133. tigrbl/orm/mixins/utils.py +49 -0
  134. tigrbl/orm/tables/__init__.py +72 -0
  135. tigrbl/orm/tables/_base.py +8 -0
  136. tigrbl/orm/tables/audit.py +56 -0
  137. tigrbl/orm/tables/client.py +25 -0
  138. tigrbl/orm/tables/group.py +29 -0
  139. tigrbl/orm/tables/org.py +30 -0
  140. tigrbl/orm/tables/rbac.py +76 -0
  141. tigrbl/orm/tables/status.py +106 -0
  142. tigrbl/orm/tables/tenant.py +22 -0
  143. tigrbl/orm/tables/user.py +39 -0
  144. tigrbl/response/README.md +34 -0
  145. tigrbl/response/__init__.py +33 -0
  146. tigrbl/response/bind.py +12 -0
  147. tigrbl/response/decorators.py +37 -0
  148. tigrbl/response/resolver.py +83 -0
  149. tigrbl/response/shortcuts.py +171 -0
  150. tigrbl/response/types.py +49 -0
  151. tigrbl/rest/__init__.py +27 -0
  152. tigrbl/runtime/README.md +129 -0
  153. tigrbl/runtime/__init__.py +20 -0
  154. tigrbl/runtime/atoms/__init__.py +102 -0
  155. tigrbl/runtime/atoms/emit/__init__.py +42 -0
  156. tigrbl/runtime/atoms/emit/paired_post.py +158 -0
  157. tigrbl/runtime/atoms/emit/paired_pre.py +106 -0
  158. tigrbl/runtime/atoms/emit/readtime_alias.py +120 -0
  159. tigrbl/runtime/atoms/out/__init__.py +38 -0
  160. tigrbl/runtime/atoms/out/masking.py +135 -0
  161. tigrbl/runtime/atoms/refresh/__init__.py +38 -0
  162. tigrbl/runtime/atoms/refresh/demand.py +130 -0
  163. tigrbl/runtime/atoms/resolve/__init__.py +40 -0
  164. tigrbl/runtime/atoms/resolve/assemble.py +167 -0
  165. tigrbl/runtime/atoms/resolve/paired_gen.py +147 -0
  166. tigrbl/runtime/atoms/response/__init__.py +19 -0
  167. tigrbl/runtime/atoms/response/headers_from_payload.py +57 -0
  168. tigrbl/runtime/atoms/response/negotiate.py +30 -0
  169. tigrbl/runtime/atoms/response/negotiation.py +43 -0
  170. tigrbl/runtime/atoms/response/render.py +36 -0
  171. tigrbl/runtime/atoms/response/renderer.py +116 -0
  172. tigrbl/runtime/atoms/response/template.py +44 -0
  173. tigrbl/runtime/atoms/response/templates.py +88 -0
  174. tigrbl/runtime/atoms/schema/__init__.py +40 -0
  175. tigrbl/runtime/atoms/schema/collect_in.py +21 -0
  176. tigrbl/runtime/atoms/schema/collect_out.py +21 -0
  177. tigrbl/runtime/atoms/storage/__init__.py +38 -0
  178. tigrbl/runtime/atoms/storage/to_stored.py +167 -0
  179. tigrbl/runtime/atoms/wire/__init__.py +45 -0
  180. tigrbl/runtime/atoms/wire/build_in.py +166 -0
  181. tigrbl/runtime/atoms/wire/build_out.py +87 -0
  182. tigrbl/runtime/atoms/wire/dump.py +206 -0
  183. tigrbl/runtime/atoms/wire/validate_in.py +227 -0
  184. tigrbl/runtime/context.py +206 -0
  185. tigrbl/runtime/errors/__init__.py +61 -0
  186. tigrbl/runtime/errors/converters.py +214 -0
  187. tigrbl/runtime/errors/exceptions.py +124 -0
  188. tigrbl/runtime/errors/mappings.py +71 -0
  189. tigrbl/runtime/errors/utils.py +150 -0
  190. tigrbl/runtime/events.py +209 -0
  191. tigrbl/runtime/executor/__init__.py +6 -0
  192. tigrbl/runtime/executor/guards.py +132 -0
  193. tigrbl/runtime/executor/helpers.py +88 -0
  194. tigrbl/runtime/executor/invoke.py +150 -0
  195. tigrbl/runtime/executor/types.py +84 -0
  196. tigrbl/runtime/kernel.py +644 -0
  197. tigrbl/runtime/labels.py +353 -0
  198. tigrbl/runtime/opview.py +89 -0
  199. tigrbl/runtime/ordering.py +256 -0
  200. tigrbl/runtime/system.py +279 -0
  201. tigrbl/runtime/trace.py +330 -0
  202. tigrbl/schema/__init__.py +38 -0
  203. tigrbl/schema/_schema.py +27 -0
  204. tigrbl/schema/builder/__init__.py +17 -0
  205. tigrbl/schema/builder/build_schema.py +209 -0
  206. tigrbl/schema/builder/cache.py +24 -0
  207. tigrbl/schema/builder/compat.py +16 -0
  208. tigrbl/schema/builder/extras.py +85 -0
  209. tigrbl/schema/builder/helpers.py +51 -0
  210. tigrbl/schema/builder/list_params.py +117 -0
  211. tigrbl/schema/builder/strip_parent_fields.py +70 -0
  212. tigrbl/schema/collect.py +79 -0
  213. tigrbl/schema/decorators.py +68 -0
  214. tigrbl/schema/get_schema.py +86 -0
  215. tigrbl/schema/schema_spec.py +20 -0
  216. tigrbl/schema/shortcuts.py +42 -0
  217. tigrbl/schema/types.py +34 -0
  218. tigrbl/schema/utils.py +143 -0
  219. tigrbl/session/README.md +14 -0
  220. tigrbl/session/__init__.py +28 -0
  221. tigrbl/session/abc.py +76 -0
  222. tigrbl/session/base.py +151 -0
  223. tigrbl/session/decorators.py +43 -0
  224. tigrbl/session/default.py +118 -0
  225. tigrbl/session/shortcuts.py +50 -0
  226. tigrbl/session/spec.py +112 -0
  227. tigrbl/shortcuts.py +22 -0
  228. tigrbl/specs.py +44 -0
  229. tigrbl/system/__init__.py +13 -0
  230. tigrbl/system/diagnostics/__init__.py +24 -0
  231. tigrbl/system/diagnostics/compat.py +31 -0
  232. tigrbl/system/diagnostics/healthz.py +41 -0
  233. tigrbl/system/diagnostics/hookz.py +51 -0
  234. tigrbl/system/diagnostics/kernelz.py +20 -0
  235. tigrbl/system/diagnostics/methodz.py +43 -0
  236. tigrbl/system/diagnostics/router.py +73 -0
  237. tigrbl/system/diagnostics/utils.py +43 -0
  238. tigrbl/system/uvicorn.py +60 -0
  239. tigrbl/table/__init__.py +9 -0
  240. tigrbl/table/_base.py +260 -0
  241. tigrbl/table/_table.py +54 -0
  242. tigrbl/table/mro_collect.py +69 -0
  243. tigrbl/table/shortcuts.py +57 -0
  244. tigrbl/table/table_spec.py +28 -0
  245. tigrbl/transport/__init__.py +74 -0
  246. tigrbl/transport/jsonrpc/__init__.py +19 -0
  247. tigrbl/transport/jsonrpc/dispatcher.py +352 -0
  248. tigrbl/transport/jsonrpc/helpers.py +115 -0
  249. tigrbl/transport/jsonrpc/models.py +41 -0
  250. tigrbl/transport/rest/__init__.py +25 -0
  251. tigrbl/transport/rest/aggregator.py +132 -0
  252. tigrbl/types/__init__.py +170 -0
  253. tigrbl/types/allow_anon_provider.py +19 -0
  254. tigrbl/types/authn_abc.py +30 -0
  255. tigrbl/types/nested_path_provider.py +22 -0
  256. tigrbl/types/op.py +35 -0
  257. tigrbl/types/op_config_provider.py +17 -0
  258. tigrbl/types/op_verb_alias_provider.py +33 -0
  259. tigrbl/types/request_extras_provider.py +22 -0
  260. tigrbl/types/response_extras_provider.py +22 -0
  261. tigrbl/types/table_config_provider.py +13 -0
  262. tigrbl/types/uuid.py +55 -0
  263. tigrbl-0.3.0.dist-info/METADATA +516 -0
  264. tigrbl-0.3.0.dist-info/RECORD +266 -0
  265. {tigrbl-0.0.1.dev1.dist-info → tigrbl-0.3.0.dist-info}/WHEEL +1 -1
  266. tigrbl-0.3.0.dist-info/licenses/LICENSE +201 -0
  267. tigrbl/ExampleAgent.py +0 -1
  268. tigrbl-0.0.1.dev1.dist-info/METADATA +0 -18
  269. tigrbl-0.0.1.dev1.dist-info/RECORD +0 -5
@@ -0,0 +1,206 @@
1
+ # tigrbl/v3/runtime/context.py
2
+ from __future__ import annotations
3
+
4
+ import datetime as _dt
5
+ from dataclasses import dataclass, field
6
+ from typing import Any, Dict, Mapping, Optional, Sequence
7
+
8
+
9
+ def _canon_op(op: Optional[str]) -> str:
10
+ return (op or "").strip().lower() or "unknown"
11
+
12
+
13
+ @dataclass
14
+ class Context:
15
+ """
16
+ Canonical runtime context shared by the kernel and atoms.
17
+
18
+ Minimal contract (consumed by atoms we’ve written so far):
19
+ - op: operation name (e.g., 'create' | 'update' | 'read' | 'list' | custom)
20
+ - persist: write vs. read (affects pruning of persist-tied anchors)
21
+ - specs: mapping of field -> ColumnSpec (frozen at bind time)
22
+ - cfg: read-only config view (see config.resolver.CfgView)
23
+ - temp: dict scratchpad used by atoms to exchange data
24
+
25
+ Optional adapter slots:
26
+ - model: owning model type / class
27
+ - obj: hydrated ORM instance (if any)
28
+ - session: DB session / unit-of-work handle
29
+ - user, tenant, now: identity/time hints
30
+ - row/values/current_values: mapping fallbacks (for read paths)
31
+ - in_data / payload / data / body: inbound payload staging (for build_in)
32
+ """
33
+
34
+ # core
35
+ op: str
36
+ persist: bool
37
+ specs: Mapping[str, Any]
38
+ cfg: Any
39
+
40
+ # shared scratchpad
41
+ temp: Dict[str, Any] = field(default_factory=dict)
42
+
43
+ # optional context
44
+ model: Any | None = None
45
+ obj: Any | None = None
46
+ session: Any | None = None
47
+
48
+ # identity/time
49
+ user: Any | None = None
50
+ tenant: Any | None = None
51
+ now: _dt.datetime | None = None
52
+
53
+ # read-path fallbacks
54
+ row: Mapping[str, Any] | None = None
55
+ values: Mapping[str, Any] | None = None
56
+ current_values: Mapping[str, Any] | None = None
57
+
58
+ # inbound staging (router/adapters may set any one of these)
59
+ in_data: Any | None = None
60
+ payload: Any | None = None
61
+ data: Any | None = None
62
+ body: Any | None = None
63
+
64
+ def __post_init__(self) -> None:
65
+ self.op = _canon_op(self.op)
66
+ # Normalize now to a timezone-aware UTC timestamp when not provided
67
+ if self.now is None:
68
+ try:
69
+ self.now = _dt.datetime.now(_dt.timezone.utc)
70
+ except Exception: # pragma: no cover
71
+ self.now = _dt.datetime.utcnow().replace(tzinfo=None)
72
+
73
+ # Ensure temp is a dict (atoms rely on it)
74
+ if not isinstance(self.temp, dict):
75
+ self.temp = dict(self.temp)
76
+
77
+ # ── convenience flags ─────────────────────────────────────────────────────
78
+
79
+ @property
80
+ def is_write(self) -> bool:
81
+ """Alias for persist; reads better in some call sites."""
82
+ return bool(self.persist)
83
+
84
+ # ── safe read-only view for user callables (generators, default_factory) ──
85
+
86
+ def safe_view(
87
+ self,
88
+ *,
89
+ include_temp: bool = False,
90
+ temp_keys: Optional[Sequence[str]] = None,
91
+ ) -> Mapping[str, Any]:
92
+ """
93
+ Return a small, read-only mapping exposing only safe, frequently useful keys.
94
+
95
+ By default, temp is NOT included (to avoid leaking internals like paired raw values).
96
+ If include_temp=True, only exposes the keys listed in 'temp_keys' (if provided),
97
+ otherwise exposes a conservative subset.
98
+
99
+ This method is intended to be passed into author callables such as
100
+ default_factory(ctx_view) or paired token generators.
101
+ """
102
+ base = {
103
+ "op": self.op,
104
+ "persist": self.persist,
105
+ "model": self.model,
106
+ "specs": self.specs,
107
+ "user": self.user,
108
+ "tenant": self.tenant,
109
+ "now": self.now,
110
+ }
111
+ if include_temp:
112
+ allowed = set(temp_keys or ("assembled_values", "virtual_in"))
113
+ exposed: Dict[str, Any] = {}
114
+ for k in allowed:
115
+ if k in self.temp:
116
+ exposed[k] = self.temp[k]
117
+ base = {**base, "temp": MappingProxy(exposed)}
118
+ return MappingProxy(base)
119
+
120
+ # ── tiny helpers used by atoms / kernel ───────────────────────────────────
121
+
122
+ def mark_used_returning(self, value: bool = True) -> None:
123
+ """Flag that DB RETURNING already hydrated values."""
124
+ self.temp["used_returning"] = bool(value)
125
+
126
+ def merge_hydrated_values(
127
+ self, mapping: Mapping[str, Any], *, replace: bool = False
128
+ ) -> None:
129
+ """
130
+ Save values hydrated from DB (RETURNING/refresh). If replace=False (default),
131
+ performs a shallow merge into any existing 'hydrated_values'.
132
+ """
133
+ if not isinstance(mapping, Mapping):
134
+ return
135
+ hv = self.temp.get("hydrated_values")
136
+ if replace or not isinstance(hv, dict):
137
+ self.temp["hydrated_values"] = dict(mapping)
138
+ else:
139
+ hv.update(mapping)
140
+
141
+ def add_response_extras(
142
+ self, extras: Mapping[str, Any], *, overwrite: Optional[bool] = None
143
+ ) -> Sequence[str]:
144
+ """
145
+ Merge alias extras into temp['response_extras'].
146
+ Returns a tuple of conflicting keys that were skipped when overwrite=False.
147
+ """
148
+ if not isinstance(extras, Mapping) or not extras:
149
+ return ()
150
+ buf = self.temp.get("response_extras")
151
+ if not isinstance(buf, dict):
152
+ buf = {}
153
+ self.temp["response_extras"] = buf
154
+ if overwrite is None:
155
+ # fall back to cfg; atoms call wire:dump to honor final overwrite policy
156
+ overwrite = bool(getattr(self.cfg, "response_extras_overwrite", False))
157
+ conflicts: list[str] = []
158
+ for k, v in extras.items():
159
+ if (k in buf) and not overwrite:
160
+ conflicts.append(k)
161
+ continue
162
+ buf[k] = v
163
+ return tuple(conflicts)
164
+
165
+ def get_response_payload(self) -> Any:
166
+ """Return the payload assembled by wire:dump (or None if not yet available)."""
167
+ return self.temp.get("response_payload")
168
+
169
+ # ── representation (avoid leaking large/sensitive temp contents) ──────────
170
+
171
+ def __repr__(self) -> str: # pragma: no cover
172
+ model_name = getattr(self.model, "__name__", None) or str(self.model)
173
+ return (
174
+ f"Context(op={self.op!r}, persist={self.persist}, model={model_name!r}, "
175
+ f"user={(getattr(self.user, 'id', None) or None)!r}, temp_keys={sorted(self.temp.keys())})"
176
+ )
177
+
178
+
179
+ # ── tiny immutable mapping proxy (local; no external deps) ────────────────────
180
+
181
+
182
+ class MappingProxy(Mapping[str, Any]):
183
+ """A lightweight, read-only mapping wrapper."""
184
+
185
+ __slots__ = ("_d",)
186
+
187
+ def __init__(self, data: Mapping[str, Any]):
188
+ self._d = dict(data)
189
+
190
+ def __getitem__(self, k: str) -> Any:
191
+ return self._d[k]
192
+
193
+ def __iter__(self):
194
+ return iter(self._d)
195
+
196
+ def __len__(self) -> int:
197
+ return len(self._d)
198
+
199
+ def get(self, key: str, default: Any = None) -> Any:
200
+ return self._d.get(key, default)
201
+
202
+ def __repr__(self) -> str: # pragma: no cover
203
+ return f"MappingProxy({self._d!r})"
204
+
205
+
206
+ __all__ = ["Context", "MappingProxy"]
@@ -0,0 +1,61 @@
1
+ from __future__ import annotations
2
+
3
+ from .utils import HTTPException, status
4
+ from .mappings import (
5
+ HTTP_ERROR_MESSAGES,
6
+ ERROR_MESSAGES,
7
+ _HTTP_TO_RPC,
8
+ _RPC_TO_HTTP,
9
+ )
10
+ from .converters import (
11
+ http_exc_to_rpc,
12
+ rpc_error_to_http,
13
+ _http_exc_to_rpc,
14
+ _rpc_error_to_http,
15
+ create_standardized_error,
16
+ create_standardized_error_from_status,
17
+ to_rpc_error_payload,
18
+ )
19
+ from .exceptions import (
20
+ TigrblError,
21
+ PlanningError,
22
+ LabelError,
23
+ ConfigError,
24
+ SystemStepError,
25
+ ValidationError,
26
+ TransformError,
27
+ DeriveError,
28
+ KernelAbort,
29
+ coerce_runtime_error,
30
+ raise_for_in_errors,
31
+ )
32
+
33
+ __all__ = [
34
+ "HTTPException",
35
+ "status",
36
+ # maps & messages
37
+ "HTTP_ERROR_MESSAGES",
38
+ "ERROR_MESSAGES",
39
+ "_HTTP_TO_RPC",
40
+ "_RPC_TO_HTTP",
41
+ # conversions
42
+ "http_exc_to_rpc",
43
+ "rpc_error_to_http",
44
+ "_http_exc_to_rpc",
45
+ "_rpc_error_to_http",
46
+ "create_standardized_error",
47
+ "create_standardized_error_from_status",
48
+ "to_rpc_error_payload",
49
+ # typed errors + helpers
50
+ "TigrblError",
51
+ "PlanningError",
52
+ "LabelError",
53
+ "ConfigError",
54
+ "SystemStepError",
55
+ "ValidationError",
56
+ "TransformError",
57
+ "DeriveError",
58
+ "KernelAbort",
59
+ "coerce_runtime_error",
60
+ "raise_for_in_errors",
61
+ ]
@@ -0,0 +1,214 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Tuple
4
+
5
+ from .utils import (
6
+ HTTPException,
7
+ status,
8
+ PydanticValidationError,
9
+ RequestValidationError,
10
+ IntegrityError,
11
+ DBAPIError,
12
+ OperationalError,
13
+ NoResultFound,
14
+ _is_asyncpg_constraint_error,
15
+ _stringify_exc,
16
+ _format_validation,
17
+ )
18
+ from .exceptions import TigrblError
19
+ from .mappings import (
20
+ _HTTP_TO_RPC,
21
+ _RPC_TO_HTTP,
22
+ ERROR_MESSAGES,
23
+ HTTP_ERROR_MESSAGES,
24
+ )
25
+
26
+
27
+ def http_exc_to_rpc(exc: HTTPException) -> tuple[int, str, Any | None]:
28
+ """Convert HTTPException → (rpc_code, message, data)."""
29
+ code = _HTTP_TO_RPC.get(exc.status_code, -32603)
30
+ detail = exc.detail
31
+ if isinstance(detail, (dict, list)):
32
+ return code, ERROR_MESSAGES.get(code, "Unknown error"), detail
33
+ msg = getattr(exc, "rpc_message", None) or (
34
+ detail if isinstance(detail, str) else None
35
+ )
36
+ if not msg:
37
+ msg = ERROR_MESSAGES.get(
38
+ code, HTTP_ERROR_MESSAGES.get(exc.status_code, "Unknown error")
39
+ )
40
+ data = getattr(exc, "rpc_data", None)
41
+ return code, msg, data
42
+
43
+
44
+ def rpc_error_to_http(
45
+ rpc_code: int, message: str | None = None, data: Any | None = None
46
+ ) -> HTTPException:
47
+ """Convert JSON-RPC error code (and optional message/data) → HTTPException."""
48
+ http_status = _RPC_TO_HTTP.get(rpc_code, 500)
49
+ msg = (
50
+ message
51
+ or HTTP_ERROR_MESSAGES.get(http_status)
52
+ or ERROR_MESSAGES.get(rpc_code, "Unknown error")
53
+ )
54
+ http_exc = HTTPException(status_code=http_status, detail=msg)
55
+ setattr(http_exc, "rpc_code", rpc_code)
56
+ setattr(http_exc, "rpc_message", msg)
57
+ setattr(http_exc, "rpc_data", data)
58
+ return http_exc
59
+
60
+
61
+ def _http_exc_to_rpc(exc: HTTPException) -> tuple[int, str, Any | None]:
62
+ """Alias for :func:`http_exc_to_rpc` to preserve older import paths."""
63
+ return http_exc_to_rpc(exc)
64
+
65
+
66
+ def _rpc_error_to_http(
67
+ rpc_code: int, message: str | None = None, data: Any | None = None
68
+ ) -> HTTPException:
69
+ """Alias for :func:`rpc_error_to_http` to preserve older import paths."""
70
+ return rpc_error_to_http(rpc_code, message, data)
71
+
72
+
73
+ def _classify_exception(
74
+ exc: BaseException,
75
+ ) -> Tuple[int, str | dict | list, Any | None]:
76
+ """
77
+ Return (http_status, detail_or_message, data) suitable for HTTPException and JSON-RPC mapping.
78
+ `detail_or_message` may be a string OR a structured dict/list (validation).
79
+ """
80
+ # 0) Typed Tigrbl errors
81
+ if isinstance(exc, TigrblError):
82
+ status_code = getattr(exc, "status", 400) or 400
83
+ details = getattr(exc, "details", None)
84
+ if isinstance(details, (dict, list)):
85
+ return status_code, details, details
86
+ return status_code, str(exc) or exc.code, None
87
+
88
+ # 1) Pass-through HTTPException preserving detail
89
+ if isinstance(exc, HTTPException):
90
+ return exc.status_code, exc.detail, getattr(exc, "rpc_data", None)
91
+
92
+ # 2) Validation errors → 422 with structured data
93
+ if (PydanticValidationError is not None) and isinstance(
94
+ exc, PydanticValidationError
95
+ ):
96
+ return (
97
+ status.HTTP_422_UNPROCESSABLE_ENTITY,
98
+ HTTP_ERROR_MESSAGES.get(422, "Validation failed"),
99
+ _format_validation(exc),
100
+ )
101
+ if (RequestValidationError is not None) and isinstance(exc, RequestValidationError):
102
+ return (
103
+ status.HTTP_422_UNPROCESSABLE_ENTITY,
104
+ HTTP_ERROR_MESSAGES.get(422, "Validation failed"),
105
+ _format_validation(exc),
106
+ )
107
+
108
+ # 3) Common client errors
109
+ if isinstance(exc, (ValueError, TypeError, KeyError)):
110
+ return status.HTTP_400_BAD_REQUEST, _stringify_exc(exc), None
111
+ if isinstance(exc, PermissionError):
112
+ return status.HTTP_403_FORBIDDEN, _stringify_exc(exc), None
113
+ if isinstance(exc, NotImplementedError):
114
+ return status.HTTP_501_NOT_IMPLEMENTED, _stringify_exc(exc), None
115
+ if isinstance(exc, TimeoutError):
116
+ return status.HTTP_504_GATEWAY_TIMEOUT, _stringify_exc(exc), None
117
+
118
+ # 4) ORM/DB mapping
119
+ if (NoResultFound is not None) and isinstance(exc, NoResultFound):
120
+ return status.HTTP_404_NOT_FOUND, "Resource not found", None
121
+
122
+ if _is_asyncpg_constraint_error(exc):
123
+ return status.HTTP_409_CONFLICT, _stringify_exc(exc), None
124
+
125
+ if (IntegrityError is not None) and isinstance(exc, IntegrityError):
126
+ msg = _stringify_exc(exc)
127
+ lower_msg = msg.lower()
128
+ if "not null constraint" in lower_msg or "check constraint" in lower_msg:
129
+ return status.HTTP_422_UNPROCESSABLE_ENTITY, msg, None
130
+ return status.HTTP_409_CONFLICT, msg, None
131
+
132
+ if (OperationalError is not None) and isinstance(exc, OperationalError):
133
+ return status.HTTP_503_SERVICE_UNAVAILABLE, _stringify_exc(exc), None
134
+
135
+ if (DBAPIError is not None) and isinstance(exc, DBAPIError):
136
+ return status.HTTP_500_INTERNAL_SERVER_ERROR, _stringify_exc(exc), None
137
+
138
+ # 5) Fallback
139
+ return status.HTTP_500_INTERNAL_SERVER_ERROR, _stringify_exc(exc), None
140
+
141
+
142
+ def create_standardized_error(exc: BaseException) -> HTTPException:
143
+ """
144
+ Normalize any exception → HTTPException with attached RPC context:
145
+ • .rpc_code
146
+ • .rpc_message
147
+ • .rpc_data
148
+ """
149
+ http_status, detail_or_message, data = _classify_exception(exc)
150
+ rpc_code = _HTTP_TO_RPC.get(http_status, -32603)
151
+ if isinstance(detail_or_message, (dict, list)):
152
+ http_detail = detail_or_message
153
+ rpc_message = ERROR_MESSAGES.get(
154
+ rpc_code, HTTP_ERROR_MESSAGES.get(http_status, "Unknown error")
155
+ )
156
+ else:
157
+ http_detail = detail_or_message
158
+ rpc_message = detail_or_message or ERROR_MESSAGES.get(
159
+ rpc_code, HTTP_ERROR_MESSAGES.get(http_status, "Unknown error")
160
+ )
161
+ http_exc = HTTPException(status_code=http_status, detail=http_detail)
162
+ setattr(http_exc, "rpc_code", rpc_code)
163
+ setattr(http_exc, "rpc_message", rpc_message)
164
+ setattr(http_exc, "rpc_data", data)
165
+ return http_exc
166
+
167
+
168
+ def create_standardized_error_from_status(
169
+ http_status: int,
170
+ message: str | None = None,
171
+ *,
172
+ rpc_code: int | None = None,
173
+ data: Any | None = None,
174
+ ) -> tuple[HTTPException, int, str]:
175
+ """Explicit constructor used by code paths that already decided on an HTTP status."""
176
+ if rpc_code is None:
177
+ rpc_code = _HTTP_TO_RPC.get(http_status, -32603)
178
+ if message is None:
179
+ http_message = HTTP_ERROR_MESSAGES.get(http_status) or ERROR_MESSAGES.get(
180
+ rpc_code, "Unknown error"
181
+ )
182
+ rpc_message = ERROR_MESSAGES.get(rpc_code) or HTTP_ERROR_MESSAGES.get(
183
+ http_status, "Unknown error"
184
+ )
185
+ else:
186
+ http_message = rpc_message = message
187
+ http_exc = HTTPException(status_code=http_status, detail=http_message)
188
+ setattr(http_exc, "rpc_code", rpc_code)
189
+ setattr(http_exc, "rpc_message", rpc_message)
190
+ setattr(http_exc, "rpc_data", data)
191
+ return http_exc, rpc_code, rpc_message
192
+
193
+
194
+ def to_rpc_error_payload(exc: HTTPException) -> dict:
195
+ """Produce a JSON-RPC error object from an HTTPException (with or without rpc_* attrs)."""
196
+ code, msg, data = http_exc_to_rpc(exc)
197
+ payload = {"code": code, "message": msg}
198
+ if data is not None:
199
+ payload["data"] = data
200
+ else:
201
+ if isinstance(exc.detail, (dict, list)):
202
+ payload["data"] = exc.detail
203
+ return payload
204
+
205
+
206
+ __all__ = [
207
+ "http_exc_to_rpc",
208
+ "rpc_error_to_http",
209
+ "_http_exc_to_rpc",
210
+ "_rpc_error_to_http",
211
+ "create_standardized_error",
212
+ "create_standardized_error_from_status",
213
+ "to_rpc_error_payload",
214
+ ]
@@ -0,0 +1,124 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict, Optional
4
+
5
+ from .utils import _read_in_errors, _has_in_errors
6
+
7
+
8
+ class TigrblError(Exception):
9
+ """Base class for runtime errors in Tigrbl v3."""
10
+
11
+ code: str = "tigrbl_error"
12
+ status: int = 400
13
+
14
+ def __init__(
15
+ self,
16
+ message: str = "",
17
+ *,
18
+ code: Optional[str] = None,
19
+ status: Optional[int] = None,
20
+ details: Any = None,
21
+ cause: Optional[BaseException] = None,
22
+ ):
23
+ super().__init__(message)
24
+ if cause is not None:
25
+ self.__cause__ = cause
26
+ if code is not None:
27
+ self.code = code
28
+ if status is not None:
29
+ self.status = status
30
+ self.details = details
31
+
32
+ def to_dict(self) -> Dict[str, Any]:
33
+ d = {
34
+ "type": self.__class__.__name__,
35
+ "code": self.code,
36
+ "status": self.status,
37
+ "message": str(self),
38
+ }
39
+ if self.details is not None:
40
+ d["details"] = self.details
41
+ return d
42
+
43
+
44
+ class PlanningError(TigrblError):
45
+ code = "planning_error"
46
+ status = 500
47
+
48
+
49
+ class LabelError(TigrblError):
50
+ code = "label_error"
51
+ status = 400
52
+
53
+
54
+ class ConfigError(TigrblError):
55
+ code = "config_error"
56
+ status = 400
57
+
58
+
59
+ class SystemStepError(TigrblError):
60
+ code = "system_step_error"
61
+ status = 500
62
+
63
+
64
+ class ValidationError(TigrblError):
65
+ code = "validation_error"
66
+ status = 422
67
+
68
+ @staticmethod
69
+ def from_ctx(
70
+ ctx: Any, message: str = "Input validation failed."
71
+ ) -> "ValidationError":
72
+ return ValidationError(message, status=422, details=_read_in_errors(ctx))
73
+
74
+
75
+ class TransformError(TigrblError):
76
+ code = "transform_error"
77
+ status = 400
78
+
79
+
80
+ class DeriveError(TigrblError):
81
+ code = "derive_error"
82
+ status = 400
83
+
84
+
85
+ class KernelAbort(TigrblError):
86
+ code = "kernel_abort"
87
+ status = 403
88
+
89
+
90
+ def coerce_runtime_error(exc: BaseException, ctx: Any | None = None) -> TigrblError:
91
+ """
92
+ Map arbitrary exceptions to a typed TigrblError for consistent kernel handling.
93
+ - Already TigrblError → return as-is
94
+ - ValueError + ctx.temp['in_errors'] → ValidationError
95
+ - Otherwise → generic TigrblError
96
+ """
97
+ if isinstance(exc, TigrblError):
98
+ return exc
99
+ if isinstance(exc, ValueError) and ctx is not None and _has_in_errors(ctx):
100
+ return ValidationError.from_ctx(
101
+ ctx, message=str(exc) or "Input validation failed."
102
+ )
103
+ return TigrblError(str(exc) or exc.__class__.__name__)
104
+
105
+
106
+ def raise_for_in_errors(ctx: Any) -> None:
107
+ """Raise a typed ValidationError if ctx.temp['in_errors'] indicates invalid input."""
108
+ if _has_in_errors(ctx):
109
+ raise ValidationError.from_ctx(ctx)
110
+
111
+
112
+ __all__ = [
113
+ "TigrblError",
114
+ "PlanningError",
115
+ "LabelError",
116
+ "ConfigError",
117
+ "SystemStepError",
118
+ "ValidationError",
119
+ "TransformError",
120
+ "DeriveError",
121
+ "KernelAbort",
122
+ "coerce_runtime_error",
123
+ "raise_for_in_errors",
124
+ ]
@@ -0,0 +1,71 @@
1
+ from __future__ import annotations
2
+
3
+ # HTTP → JSON-RPC code map
4
+ _HTTP_TO_RPC: dict[int, int] = {
5
+ 400: -32602,
6
+ 401: -32001,
7
+ 403: -32002,
8
+ 404: -32003,
9
+ 409: -32004,
10
+ 422: -32602,
11
+ 500: -32603,
12
+ 501: -32603,
13
+ 503: -32603,
14
+ 504: -32603,
15
+ }
16
+
17
+ # JSON-RPC → HTTP status map
18
+ _RPC_TO_HTTP: dict[int, int] = {
19
+ -32700: 400,
20
+ -32600: 400,
21
+ -32601: 404,
22
+ -32602: 400,
23
+ -32603: 500,
24
+ -32001: 401,
25
+ -32002: 403,
26
+ -32003: 404,
27
+ -32004: 409,
28
+ }
29
+
30
+ # Standardized error messages
31
+ ERROR_MESSAGES: dict[int, str] = {
32
+ -32700: "Parse error",
33
+ -32600: "Invalid Request",
34
+ -32601: "Method not found",
35
+ -32602: "Invalid params",
36
+ -32603: "Internal error",
37
+ -32001: "Authentication required",
38
+ -32002: "Insufficient permissions",
39
+ -32003: "Resource not found",
40
+ -32004: "Resource conflict",
41
+ -32000: "Server error",
42
+ -32099: "Duplicate key constraint violation",
43
+ -32098: "Data constraint violation",
44
+ -32097: "Foreign key constraint violation",
45
+ -32096: "Authentication required",
46
+ -32095: "Authorization failed",
47
+ -32094: "Resource not found",
48
+ -32093: "Validation error",
49
+ -32092: "Transaction failed",
50
+ }
51
+
52
+ # HTTP status code → standardized message
53
+ HTTP_ERROR_MESSAGES: dict[int, str] = {
54
+ 400: "Bad Request: malformed input",
55
+ 401: "Unauthorized: authentication required",
56
+ 403: "Forbidden: insufficient permissions",
57
+ 404: "Not Found: resource does not exist",
58
+ 409: "Conflict: duplicate key or constraint violation",
59
+ 422: "Unprocessable Entity: validation failed",
60
+ 500: "Internal Server Error: unexpected server error",
61
+ 501: "Not Implemented",
62
+ 503: "Service Unavailable",
63
+ 504: "Gateway Timeout",
64
+ }
65
+
66
+ __all__ = [
67
+ "_HTTP_TO_RPC",
68
+ "_RPC_TO_HTTP",
69
+ "ERROR_MESSAGES",
70
+ "HTTP_ERROR_MESSAGES",
71
+ ]