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,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
+ ]
@@ -0,0 +1,150 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict, Iterable, List, Mapping, Optional
4
+ import logging
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+ # Prefer FastAPI HTTPException/status; fall back to Starlette; finally a tiny shim.
9
+ try: # FastAPI present
10
+ from fastapi import HTTPException, status
11
+ except Exception: # pragma: no cover
12
+ try: # Starlette present
13
+ from starlette.exceptions import HTTPException # type: ignore
14
+ from starlette import status # type: ignore
15
+ except Exception: # pragma: no cover
16
+
17
+ class HTTPException(Exception): # minimal shim
18
+ def __init__(
19
+ self,
20
+ status_code: int,
21
+ detail: Any = None,
22
+ headers: Optional[dict] = None,
23
+ ) -> None:
24
+ super().__init__(detail)
25
+ self.status_code = status_code
26
+ self.detail = detail
27
+ self.headers = headers
28
+
29
+ class _Status:
30
+ HTTP_400_BAD_REQUEST = 400
31
+ HTTP_401_UNAUTHORIZED = 401
32
+ HTTP_403_FORBIDDEN = 403
33
+ HTTP_404_NOT_FOUND = 404
34
+ HTTP_409_CONFLICT = 409
35
+ HTTP_422_UNPROCESSABLE_ENTITY = 422
36
+ HTTP_500_INTERNAL_SERVER_ERROR = 500
37
+ HTTP_501_NOT_IMPLEMENTED = 501
38
+ HTTP_503_SERVICE_UNAVAILABLE = 503
39
+ HTTP_504_GATEWAY_TIMEOUT = 504
40
+
41
+ status = _Status() # type: ignore
42
+
43
+ # Optional imports – code must run even if these packages aren’t installed.
44
+ try:
45
+ from pydantic import ValidationError as PydanticValidationError # v2
46
+ except Exception: # pragma: no cover
47
+ PydanticValidationError = None # type: ignore
48
+
49
+ try:
50
+ from fastapi.exceptions import (
51
+ RequestValidationError,
52
+ ) # emitted by FastAPI input validation
53
+ except Exception: # pragma: no cover
54
+ RequestValidationError = None # type: ignore
55
+
56
+ try:
57
+ # SQLAlchemy v1/v2 exception sets
58
+ from sqlalchemy.exc import IntegrityError, DBAPIError, OperationalError
59
+ from sqlalchemy.orm.exc import NoResultFound # type: ignore
60
+ except Exception: # pragma: no cover
61
+ IntegrityError = DBAPIError = OperationalError = NoResultFound = None # type: ignore
62
+
63
+
64
+ # Detect asyncpg constraint errors without importing asyncpg (optional dep).
65
+ _ASYNCPG_CONSTRAINT_NAMES = {
66
+ "UniqueViolationError",
67
+ "ForeignKeyViolationError",
68
+ "NotNullViolationError",
69
+ "CheckViolationError",
70
+ "ExclusionViolationError",
71
+ }
72
+
73
+
74
+ def _is_asyncpg_constraint_error(exc: BaseException) -> bool:
75
+ cls = type(exc)
76
+ return (cls.__module__ or "").startswith("asyncpg") and (
77
+ cls.__name__ in _ASYNCPG_CONSTRAINT_NAMES
78
+ )
79
+
80
+
81
+ def _limit(s: str, n: int = 4000) -> str:
82
+ return s if len(s) <= n else s[: n - 3] + "..."
83
+
84
+
85
+ def _stringify_exc(exc: BaseException) -> str:
86
+ detail = getattr(exc, "detail", None)
87
+ if detail:
88
+ return _limit(str(detail))
89
+ return _limit(f"{exc.__class__!r}: {str(exc) or repr(exc)}")
90
+
91
+
92
+ def _format_validation(err: Any) -> Any:
93
+ try:
94
+ items = err.errors() # pydantic / fastapi RequestValidationError
95
+ if isinstance(items, Iterable):
96
+ return list(items)
97
+ except Exception: # pragma: no cover
98
+ pass
99
+ return _limit(str(err))
100
+
101
+
102
+ def _get_temp(ctx: Any) -> Mapping[str, Any]:
103
+ tmp = getattr(ctx, "temp", None)
104
+ return tmp if isinstance(tmp, Mapping) else {}
105
+
106
+
107
+ def _has_in_errors(ctx: Any) -> bool:
108
+ tmp = _get_temp(ctx)
109
+ if tmp.get("in_invalid") is True:
110
+ return True
111
+ errs = tmp.get("in_errors")
112
+ return isinstance(errs, (list, tuple)) and len(errs) > 0
113
+
114
+
115
+ def _read_in_errors(ctx: Any) -> List[Dict[str, Any]]:
116
+ tmp = _get_temp(ctx)
117
+ errs = tmp.get("in_errors")
118
+ if isinstance(errs, list):
119
+ norm: List[Dict[str, Any]] = []
120
+ for e in errs:
121
+ if isinstance(e, Mapping):
122
+ field = e.get("field")
123
+ code = e.get("code") or "invalid"
124
+ msg = e.get("message") or "Invalid value."
125
+ entry = {"field": field, "code": code, "message": msg}
126
+ for k, v in e.items():
127
+ if k not in entry:
128
+ entry[k] = v
129
+ norm.append(entry)
130
+ return norm
131
+ return []
132
+
133
+
134
+ __all__ = [
135
+ "HTTPException",
136
+ "status",
137
+ "PydanticValidationError",
138
+ "RequestValidationError",
139
+ "IntegrityError",
140
+ "DBAPIError",
141
+ "OperationalError",
142
+ "NoResultFound",
143
+ "_is_asyncpg_constraint_error",
144
+ "_limit",
145
+ "_stringify_exc",
146
+ "_format_validation",
147
+ "_get_temp",
148
+ "_has_in_errors",
149
+ "_read_in_errors",
150
+ ]