tigrbl 0.0.1.dev1__py3-none-any.whl → 0.3.0.dev3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tigrbl/README.md +94 -0
- tigrbl/__init__.py +139 -14
- tigrbl/api/__init__.py +6 -0
- tigrbl/api/_api.py +72 -0
- tigrbl/api/api_spec.py +30 -0
- tigrbl/api/mro_collect.py +43 -0
- tigrbl/api/shortcuts.py +56 -0
- tigrbl/api/tigrbl_api.py +286 -0
- tigrbl/app/__init__.py +0 -0
- tigrbl/app/_app.py +61 -0
- tigrbl/app/app_spec.py +42 -0
- tigrbl/app/mro_collect.py +67 -0
- tigrbl/app/shortcuts.py +65 -0
- tigrbl/app/tigrbl_app.py +314 -0
- tigrbl/bindings/__init__.py +73 -0
- tigrbl/bindings/api/__init__.py +12 -0
- tigrbl/bindings/api/common.py +109 -0
- tigrbl/bindings/api/include.py +256 -0
- tigrbl/bindings/api/resource_proxy.py +149 -0
- tigrbl/bindings/api/rpc.py +111 -0
- tigrbl/bindings/columns.py +49 -0
- tigrbl/bindings/handlers/__init__.py +11 -0
- tigrbl/bindings/handlers/builder.py +119 -0
- tigrbl/bindings/handlers/ctx.py +74 -0
- tigrbl/bindings/handlers/identifiers.py +228 -0
- tigrbl/bindings/handlers/namespaces.py +51 -0
- tigrbl/bindings/handlers/steps.py +276 -0
- tigrbl/bindings/hooks.py +311 -0
- tigrbl/bindings/model.py +194 -0
- tigrbl/bindings/model_helpers.py +139 -0
- tigrbl/bindings/model_registry.py +77 -0
- tigrbl/bindings/rest/__init__.py +7 -0
- tigrbl/bindings/rest/attach.py +34 -0
- tigrbl/bindings/rest/collection.py +265 -0
- tigrbl/bindings/rest/common.py +116 -0
- tigrbl/bindings/rest/fastapi.py +76 -0
- tigrbl/bindings/rest/helpers.py +119 -0
- tigrbl/bindings/rest/io.py +317 -0
- tigrbl/bindings/rest/member.py +367 -0
- tigrbl/bindings/rest/router.py +292 -0
- tigrbl/bindings/rest/routing.py +133 -0
- tigrbl/bindings/rpc.py +364 -0
- tigrbl/bindings/schemas/__init__.py +11 -0
- tigrbl/bindings/schemas/builder.py +348 -0
- tigrbl/bindings/schemas/defaults.py +260 -0
- tigrbl/bindings/schemas/utils.py +193 -0
- tigrbl/column/README.md +62 -0
- tigrbl/column/__init__.py +72 -0
- tigrbl/column/_column.py +96 -0
- tigrbl/column/column_spec.py +40 -0
- tigrbl/column/field_spec.py +31 -0
- tigrbl/column/infer/__init__.py +25 -0
- tigrbl/column/infer/core.py +92 -0
- tigrbl/column/infer/jsonhints.py +44 -0
- tigrbl/column/infer/planning.py +133 -0
- tigrbl/column/infer/types.py +102 -0
- tigrbl/column/infer/utils.py +59 -0
- tigrbl/column/io_spec.py +133 -0
- tigrbl/column/mro_collect.py +59 -0
- tigrbl/column/shortcuts.py +89 -0
- tigrbl/column/storage_spec.py +65 -0
- tigrbl/config/__init__.py +19 -0
- tigrbl/config/constants.py +224 -0
- tigrbl/config/defaults.py +29 -0
- tigrbl/config/resolver.py +295 -0
- tigrbl/core/__init__.py +47 -0
- tigrbl/core/crud/__init__.py +36 -0
- tigrbl/core/crud/bulk.py +168 -0
- tigrbl/core/crud/helpers/__init__.py +76 -0
- tigrbl/core/crud/helpers/db.py +92 -0
- tigrbl/core/crud/helpers/enum.py +86 -0
- tigrbl/core/crud/helpers/filters.py +162 -0
- tigrbl/core/crud/helpers/model.py +123 -0
- tigrbl/core/crud/helpers/normalize.py +99 -0
- tigrbl/core/crud/ops.py +235 -0
- tigrbl/ddl/__init__.py +344 -0
- tigrbl/decorators.py +17 -0
- tigrbl/deps/__init__.py +20 -0
- tigrbl/deps/fastapi.py +45 -0
- tigrbl/deps/favicon.svg +4 -0
- tigrbl/deps/jinja.py +27 -0
- tigrbl/deps/pydantic.py +10 -0
- tigrbl/deps/sqlalchemy.py +94 -0
- tigrbl/deps/starlette.py +36 -0
- tigrbl/engine/__init__.py +26 -0
- tigrbl/engine/_engine.py +130 -0
- tigrbl/engine/bind.py +33 -0
- tigrbl/engine/builders.py +236 -0
- tigrbl/engine/collect.py +111 -0
- tigrbl/engine/decorators.py +108 -0
- tigrbl/engine/engine_spec.py +261 -0
- tigrbl/engine/resolver.py +224 -0
- tigrbl/engine/shortcuts.py +216 -0
- tigrbl/hook/__init__.py +21 -0
- tigrbl/hook/_hook.py +22 -0
- tigrbl/hook/decorators.py +28 -0
- tigrbl/hook/hook_spec.py +24 -0
- tigrbl/hook/mro_collect.py +98 -0
- tigrbl/hook/shortcuts.py +44 -0
- tigrbl/hook/types.py +76 -0
- tigrbl/op/__init__.py +50 -0
- tigrbl/op/_op.py +31 -0
- tigrbl/op/canonical.py +31 -0
- tigrbl/op/collect.py +11 -0
- tigrbl/op/decorators.py +238 -0
- tigrbl/op/model_registry.py +301 -0
- tigrbl/op/mro_collect.py +99 -0
- tigrbl/op/resolver.py +216 -0
- tigrbl/op/types.py +136 -0
- tigrbl/orm/__init__.py +1 -0
- tigrbl/orm/mixins/_RowBound.py +83 -0
- tigrbl/orm/mixins/__init__.py +95 -0
- tigrbl/orm/mixins/bootstrappable.py +113 -0
- tigrbl/orm/mixins/bound.py +47 -0
- tigrbl/orm/mixins/edges.py +40 -0
- tigrbl/orm/mixins/fields.py +165 -0
- tigrbl/orm/mixins/hierarchy.py +54 -0
- tigrbl/orm/mixins/key_digest.py +44 -0
- tigrbl/orm/mixins/lifecycle.py +115 -0
- tigrbl/orm/mixins/locks.py +51 -0
- tigrbl/orm/mixins/markers.py +16 -0
- tigrbl/orm/mixins/operations.py +57 -0
- tigrbl/orm/mixins/ownable.py +337 -0
- tigrbl/orm/mixins/principals.py +98 -0
- tigrbl/orm/mixins/tenant_bound.py +301 -0
- tigrbl/orm/mixins/upsertable.py +111 -0
- tigrbl/orm/mixins/utils.py +49 -0
- tigrbl/orm/tables/__init__.py +72 -0
- tigrbl/orm/tables/_base.py +8 -0
- tigrbl/orm/tables/audit.py +56 -0
- tigrbl/orm/tables/client.py +25 -0
- tigrbl/orm/tables/group.py +29 -0
- tigrbl/orm/tables/org.py +30 -0
- tigrbl/orm/tables/rbac.py +76 -0
- tigrbl/orm/tables/status.py +106 -0
- tigrbl/orm/tables/tenant.py +22 -0
- tigrbl/orm/tables/user.py +39 -0
- tigrbl/response/README.md +34 -0
- tigrbl/response/__init__.py +33 -0
- tigrbl/response/bind.py +12 -0
- tigrbl/response/decorators.py +37 -0
- tigrbl/response/resolver.py +83 -0
- tigrbl/response/shortcuts.py +144 -0
- tigrbl/response/types.py +49 -0
- tigrbl/rest/__init__.py +27 -0
- tigrbl/runtime/README.md +129 -0
- tigrbl/runtime/__init__.py +20 -0
- tigrbl/runtime/atoms/__init__.py +102 -0
- tigrbl/runtime/atoms/emit/__init__.py +42 -0
- tigrbl/runtime/atoms/emit/paired_post.py +158 -0
- tigrbl/runtime/atoms/emit/paired_pre.py +106 -0
- tigrbl/runtime/atoms/emit/readtime_alias.py +120 -0
- tigrbl/runtime/atoms/out/__init__.py +38 -0
- tigrbl/runtime/atoms/out/masking.py +135 -0
- tigrbl/runtime/atoms/refresh/__init__.py +38 -0
- tigrbl/runtime/atoms/refresh/demand.py +130 -0
- tigrbl/runtime/atoms/resolve/__init__.py +40 -0
- tigrbl/runtime/atoms/resolve/assemble.py +167 -0
- tigrbl/runtime/atoms/resolve/paired_gen.py +147 -0
- tigrbl/runtime/atoms/response/__init__.py +17 -0
- tigrbl/runtime/atoms/response/negotiate.py +30 -0
- tigrbl/runtime/atoms/response/negotiation.py +43 -0
- tigrbl/runtime/atoms/response/render.py +36 -0
- tigrbl/runtime/atoms/response/renderer.py +116 -0
- tigrbl/runtime/atoms/response/template.py +44 -0
- tigrbl/runtime/atoms/response/templates.py +88 -0
- tigrbl/runtime/atoms/schema/__init__.py +40 -0
- tigrbl/runtime/atoms/schema/collect_in.py +21 -0
- tigrbl/runtime/atoms/schema/collect_out.py +21 -0
- tigrbl/runtime/atoms/storage/__init__.py +38 -0
- tigrbl/runtime/atoms/storage/to_stored.py +167 -0
- tigrbl/runtime/atoms/wire/__init__.py +45 -0
- tigrbl/runtime/atoms/wire/build_in.py +166 -0
- tigrbl/runtime/atoms/wire/build_out.py +87 -0
- tigrbl/runtime/atoms/wire/dump.py +206 -0
- tigrbl/runtime/atoms/wire/validate_in.py +227 -0
- tigrbl/runtime/context.py +206 -0
- tigrbl/runtime/errors/__init__.py +61 -0
- tigrbl/runtime/errors/converters.py +214 -0
- tigrbl/runtime/errors/exceptions.py +124 -0
- tigrbl/runtime/errors/mappings.py +71 -0
- tigrbl/runtime/errors/utils.py +150 -0
- tigrbl/runtime/events.py +209 -0
- tigrbl/runtime/executor/__init__.py +6 -0
- tigrbl/runtime/executor/guards.py +132 -0
- tigrbl/runtime/executor/helpers.py +88 -0
- tigrbl/runtime/executor/invoke.py +150 -0
- tigrbl/runtime/executor/types.py +84 -0
- tigrbl/runtime/kernel.py +628 -0
- tigrbl/runtime/labels.py +353 -0
- tigrbl/runtime/opview.py +87 -0
- tigrbl/runtime/ordering.py +256 -0
- tigrbl/runtime/system.py +279 -0
- tigrbl/runtime/trace.py +330 -0
- tigrbl/schema/__init__.py +38 -0
- tigrbl/schema/_schema.py +27 -0
- tigrbl/schema/builder/__init__.py +17 -0
- tigrbl/schema/builder/build_schema.py +209 -0
- tigrbl/schema/builder/cache.py +24 -0
- tigrbl/schema/builder/compat.py +16 -0
- tigrbl/schema/builder/extras.py +85 -0
- tigrbl/schema/builder/helpers.py +51 -0
- tigrbl/schema/builder/list_params.py +117 -0
- tigrbl/schema/builder/strip_parent_fields.py +70 -0
- tigrbl/schema/collect.py +55 -0
- tigrbl/schema/decorators.py +68 -0
- tigrbl/schema/get_schema.py +86 -0
- tigrbl/schema/schema_spec.py +20 -0
- tigrbl/schema/shortcuts.py +42 -0
- tigrbl/schema/types.py +34 -0
- tigrbl/schema/utils.py +143 -0
- tigrbl/shortcuts.py +22 -0
- tigrbl/specs.py +44 -0
- tigrbl/system/__init__.py +12 -0
- tigrbl/system/diagnostics/__init__.py +24 -0
- tigrbl/system/diagnostics/compat.py +31 -0
- tigrbl/system/diagnostics/healthz.py +41 -0
- tigrbl/system/diagnostics/hookz.py +51 -0
- tigrbl/system/diagnostics/kernelz.py +20 -0
- tigrbl/system/diagnostics/methodz.py +43 -0
- tigrbl/system/diagnostics/router.py +73 -0
- tigrbl/system/diagnostics/utils.py +43 -0
- tigrbl/table/__init__.py +9 -0
- tigrbl/table/_base.py +237 -0
- tigrbl/table/_table.py +54 -0
- tigrbl/table/mro_collect.py +69 -0
- tigrbl/table/shortcuts.py +57 -0
- tigrbl/table/table_spec.py +28 -0
- tigrbl/transport/__init__.py +74 -0
- tigrbl/transport/jsonrpc/__init__.py +19 -0
- tigrbl/transport/jsonrpc/dispatcher.py +352 -0
- tigrbl/transport/jsonrpc/helpers.py +115 -0
- tigrbl/transport/jsonrpc/models.py +41 -0
- tigrbl/transport/rest/__init__.py +25 -0
- tigrbl/transport/rest/aggregator.py +132 -0
- tigrbl/types/__init__.py +174 -0
- tigrbl/types/allow_anon_provider.py +19 -0
- tigrbl/types/authn_abc.py +30 -0
- tigrbl/types/nested_path_provider.py +22 -0
- tigrbl/types/op.py +35 -0
- tigrbl/types/op_config_provider.py +17 -0
- tigrbl/types/op_verb_alias_provider.py +33 -0
- tigrbl/types/request_extras_provider.py +22 -0
- tigrbl/types/response_extras_provider.py +22 -0
- tigrbl/types/table_config_provider.py +13 -0
- tigrbl-0.3.0.dev3.dist-info/LICENSE +201 -0
- tigrbl-0.3.0.dev3.dist-info/METADATA +501 -0
- tigrbl-0.3.0.dev3.dist-info/RECORD +249 -0
- tigrbl/ExampleAgent.py +0 -1
- tigrbl-0.0.1.dev1.dist-info/METADATA +0 -18
- tigrbl-0.0.1.dev1.dist-info/RECORD +0 -5
- {tigrbl-0.0.1.dev1.dist-info → tigrbl-0.3.0.dev3.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,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
|
+
]
|