tigrbl-typing 0.1.0.dev5__tar.gz

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.
@@ -0,0 +1,50 @@
1
+ Metadata-Version: 2.4
2
+ Name: tigrbl-typing
3
+ Version: 0.1.0.dev5
4
+ Summary: Typing protocols and shared type helpers for the Tigrbl framework.
5
+ License-Expression: Apache-2.0
6
+ Keywords: tigrbl,sdk,standards,framework
7
+ Author: Jacob Stewart
8
+ Author-email: jacob@swarmauri.com
9
+ Requires-Python: >=3.10,<3.13
10
+ Classifier: License :: OSI Approved :: Apache Software License
11
+ Classifier: Development Status :: 1 - Planning
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Requires-Dist: pydantic (>=2.10,<3)
19
+ Description-Content-Type: text/markdown
20
+
21
+ ![Tigrbl branding](https://github.com/swarmauri/swarmauri-sdk/blob/a170683ecda8ca1c4f912c966d4499649ffb8224/assets/tigrbl.brand.theme.svg)
22
+
23
+ # tigrbl-typing
24
+
25
+ ![PyPI - Downloads](https://img.shields.io/pypi/dm/tigrbl-typing.svg) ![Hits](https://hits.sh/github.com/swarmauri/swarmauri-sdk.svg) ![Python Versions](https://img.shields.io/pypi/pyversions/tigrbl-typing.svg) ![License](https://img.shields.io/pypi/l/tigrbl-typing.svg) ![Version](https://img.shields.io/pypi/v/tigrbl-typing.svg)
26
+
27
+ ## Features
28
+
29
+ - Modular package in the Tigrbl namespace.
30
+ - Supports Python 3.10 through 3.12.
31
+ - Distributed as part of the swarmauri-sdk workspace.
32
+
33
+ ## Installation
34
+
35
+ ### uv
36
+
37
+ ```bash
38
+ uv add tigrbl-typing
39
+ ```
40
+
41
+ ### pip
42
+
43
+ ```bash
44
+ pip install tigrbl-typing
45
+ ```
46
+
47
+ ## Usage
48
+
49
+ Import from the shared package-specific module namespaces after installation in your environment.
50
+
@@ -0,0 +1,29 @@
1
+ ![Tigrbl branding](https://github.com/swarmauri/swarmauri-sdk/blob/a170683ecda8ca1c4f912c966d4499649ffb8224/assets/tigrbl.brand.theme.svg)
2
+
3
+ # tigrbl-typing
4
+
5
+ ![PyPI - Downloads](https://img.shields.io/pypi/dm/tigrbl-typing.svg) ![Hits](https://hits.sh/github.com/swarmauri/swarmauri-sdk.svg) ![Python Versions](https://img.shields.io/pypi/pyversions/tigrbl-typing.svg) ![License](https://img.shields.io/pypi/l/tigrbl-typing.svg) ![Version](https://img.shields.io/pypi/v/tigrbl-typing.svg)
6
+
7
+ ## Features
8
+
9
+ - Modular package in the Tigrbl namespace.
10
+ - Supports Python 3.10 through 3.12.
11
+ - Distributed as part of the swarmauri-sdk workspace.
12
+
13
+ ## Installation
14
+
15
+ ### uv
16
+
17
+ ```bash
18
+ uv add tigrbl-typing
19
+ ```
20
+
21
+ ### pip
22
+
23
+ ```bash
24
+ pip install tigrbl-typing
25
+ ```
26
+
27
+ ## Usage
28
+
29
+ Import from the shared package-specific module namespaces after installation in your environment.
@@ -0,0 +1,41 @@
1
+ [project]
2
+ name = "tigrbl-typing"
3
+ version = "0.1.0.dev5"
4
+ description = "Typing protocols and shared type helpers for the Tigrbl framework."
5
+ license = "Apache-2.0"
6
+ readme = "README.md"
7
+ repository = "http://github.com/swarmauri/swarmauri-sdk"
8
+ requires-python = ">=3.10,<3.13"
9
+ classifiers = [
10
+ "License :: OSI Approved :: Apache Software License",
11
+ "Development Status :: 1 - Planning",
12
+ "Programming Language :: Python :: 3.10",
13
+ "Programming Language :: Python :: 3.11",
14
+ "Programming Language :: Python :: 3.12",
15
+ "Programming Language :: Python",
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3 :: Only",
18
+ ]
19
+ authors = [{ name = "Jacob Stewart", email = "jacob@swarmauri.com" }]
20
+ dependencies = [
21
+ "pydantic>=2.10,<3",
22
+ ]
23
+ keywords = ["tigrbl", "sdk", "standards", "framework"]
24
+
25
+ [build-system]
26
+ requires = ["poetry-core>=1.0.0"]
27
+ build-backend = "poetry.core.masonry.api"
28
+
29
+
30
+ [tool.poetry]
31
+ packages = [
32
+ { include = "tigrbl_typing" },
33
+ ]
34
+
35
+ [dependency-groups]
36
+ dev = [
37
+ "pytest>=8.0",
38
+ "pytest-asyncio>=0.24.0",
39
+ "pytest-timeout>=2.3.1",
40
+ "ruff>=0.9",
41
+ ]
@@ -0,0 +1,5 @@
1
+ """Public exports for ``tigrbl_typing``."""
2
+
3
+ from .types import PgUUID
4
+
5
+ __all__ = ["PgUUID"]
@@ -0,0 +1,3 @@
1
+ from .raw import GwRawEnvelope, GwRouteEnvelope
2
+
3
+ __all__ = ["GwRawEnvelope", "GwRouteEnvelope"]
@@ -0,0 +1,26 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any, Awaitable, Callable, Literal, Mapping, Sequence
5
+
6
+
7
+ @dataclass(slots=True)
8
+ class GwRawEnvelope:
9
+ kind: Literal["asgi3"]
10
+ scope: dict[str, Any]
11
+ receive: Callable[[], Awaitable[dict[str, Any]]]
12
+ send: Callable[[dict[str, Any]], Awaitable[None]]
13
+
14
+
15
+ @dataclass(frozen=True, slots=True)
16
+ class GwRouteEnvelope:
17
+ transport: Literal["http", "ws", "sse", "stream", "http3"]
18
+ scheme: Literal["http", "https", "ws", "wss", "h3"]
19
+ kind: Literal["rest", "jsonrpc", "maybe-jsonrpc", "unknown"]
20
+ method: str | None
21
+ path: str | None
22
+ headers: Mapping[str, str]
23
+ query: Mapping[str, Sequence[str]]
24
+ body: bytes | None
25
+ ws_event: Any | None
26
+ rpc: Mapping[str, Any] | None
@@ -0,0 +1,110 @@
1
+ from __future__ import annotations
2
+
3
+ from enum import Enum
4
+ from typing import Literal, Tuple
5
+
6
+ HookPhase = Literal[
7
+ "PRE_TX_BEGIN",
8
+ "START_TX",
9
+ "PRE_HANDLER",
10
+ "HANDLER",
11
+ "POST_HANDLER",
12
+ "PRE_COMMIT",
13
+ "END_TX",
14
+ "POST_COMMIT",
15
+ "POST_RESPONSE",
16
+ "ON_ERROR",
17
+ "ON_PRE_TX_BEGIN_ERROR",
18
+ "ON_START_TX_ERROR",
19
+ "ON_PRE_HANDLER_ERROR",
20
+ "ON_HANDLER_ERROR",
21
+ "ON_POST_HANDLER_ERROR",
22
+ "ON_PRE_COMMIT_ERROR",
23
+ "ON_END_TX_ERROR",
24
+ "ON_POST_COMMIT_ERROR",
25
+ "ON_POST_RESPONSE_ERROR",
26
+ "ON_ROLLBACK",
27
+ ]
28
+
29
+ Phase = Literal[
30
+ "INGRESS_BEGIN",
31
+ "INGRESS_PARSE",
32
+ "INGRESS_ROUTE",
33
+ "PRE_TX_BEGIN",
34
+ "START_TX",
35
+ "PRE_HANDLER",
36
+ "HANDLER",
37
+ "POST_HANDLER",
38
+ "PRE_COMMIT",
39
+ "END_TX",
40
+ "POST_COMMIT",
41
+ "EGRESS_SHAPE",
42
+ "EGRESS_FINALIZE",
43
+ "POST_RESPONSE",
44
+ "ON_ERROR",
45
+ "ON_PRE_TX_BEGIN_ERROR",
46
+ "ON_START_TX_ERROR",
47
+ "ON_PRE_HANDLER_ERROR",
48
+ "ON_HANDLER_ERROR",
49
+ "ON_POST_HANDLER_ERROR",
50
+ "ON_PRE_COMMIT_ERROR",
51
+ "ON_END_TX_ERROR",
52
+ "ON_POST_COMMIT_ERROR",
53
+ "ON_POST_RESPONSE_ERROR",
54
+ "ON_ROLLBACK",
55
+ ]
56
+
57
+
58
+ class PHASE(str, Enum):
59
+ PRE_TX_BEGIN = "PRE_TX_BEGIN"
60
+ START_TX = "START_TX"
61
+ PRE_HANDLER = "PRE_HANDLER"
62
+ HANDLER = "HANDLER"
63
+ POST_HANDLER = "POST_HANDLER"
64
+ PRE_COMMIT = "PRE_COMMIT"
65
+ END_TX = "END_TX"
66
+ POST_COMMIT = "POST_COMMIT"
67
+ POST_RESPONSE = "POST_RESPONSE"
68
+ ON_ERROR = "ON_ERROR"
69
+ ON_PRE_TX_BEGIN_ERROR = "ON_PRE_TX_BEGIN_ERROR"
70
+ ON_START_TX_ERROR = "ON_START_TX_ERROR"
71
+ ON_PRE_HANDLER_ERROR = "ON_PRE_HANDLER_ERROR"
72
+ ON_HANDLER_ERROR = "ON_HANDLER_ERROR"
73
+ ON_POST_HANDLER_ERROR = "ON_POST_HANDLER_ERROR"
74
+ ON_PRE_COMMIT_ERROR = "ON_PRE_COMMIT_ERROR"
75
+ ON_END_TX_ERROR = "ON_END_TX_ERROR"
76
+ ON_POST_COMMIT_ERROR = "ON_POST_COMMIT_ERROR"
77
+ ON_POST_RESPONSE_ERROR = "ON_POST_RESPONSE_ERROR"
78
+ ON_ROLLBACK = "ON_ROLLBACK"
79
+
80
+
81
+ HOOK_PHASES: Tuple[HookPhase, ...] = tuple(p.value for p in PHASE)
82
+ PHASES: Tuple[Phase, ...] = (
83
+ "INGRESS_BEGIN",
84
+ "INGRESS_PARSE",
85
+ "INGRESS_ROUTE",
86
+ "PRE_TX_BEGIN",
87
+ "START_TX",
88
+ "PRE_HANDLER",
89
+ "HANDLER",
90
+ "POST_HANDLER",
91
+ "PRE_COMMIT",
92
+ "END_TX",
93
+ "POST_COMMIT",
94
+ "EGRESS_SHAPE",
95
+ "EGRESS_FINALIZE",
96
+ "POST_RESPONSE",
97
+ "ON_ERROR",
98
+ "ON_PRE_TX_BEGIN_ERROR",
99
+ "ON_START_TX_ERROR",
100
+ "ON_PRE_HANDLER_ERROR",
101
+ "ON_HANDLER_ERROR",
102
+ "ON_POST_HANDLER_ERROR",
103
+ "ON_PRE_COMMIT_ERROR",
104
+ "ON_END_TX_ERROR",
105
+ "ON_POST_COMMIT_ERROR",
106
+ "ON_POST_RESPONSE_ERROR",
107
+ "ON_ROLLBACK",
108
+ )
109
+
110
+ __all__ = ["PHASE", "PHASES", "HOOK_PHASES", "Phase", "HookPhase"]
@@ -0,0 +1,45 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Protocol, runtime_checkable
4
+
5
+
6
+ @runtime_checkable
7
+ class RequestLike(Protocol):
8
+ query_params: dict[str, Any]
9
+ path_params: dict[str, Any]
10
+ headers: dict[str, Any]
11
+
12
+ def json_sync(self) -> Any: ...
13
+
14
+
15
+ @runtime_checkable
16
+ class DependencyLike(Protocol):
17
+ dependency: Any
18
+
19
+
20
+ @runtime_checkable
21
+ class ResponseLike(Protocol):
22
+ status_code: int
23
+ raw_headers: list[tuple[bytes, bytes]]
24
+ body: bytes | None
25
+
26
+
27
+ def is_dependency_like(obj: Any) -> bool:
28
+ return isinstance(obj, DependencyLike) and callable(
29
+ getattr(obj, "dependency", None)
30
+ )
31
+
32
+
33
+ def is_response_like(obj: Any) -> bool:
34
+ if not isinstance(obj, ResponseLike):
35
+ return False
36
+ return isinstance(getattr(obj, "raw_headers", None), list)
37
+
38
+
39
+ __all__ = [
40
+ "RequestLike",
41
+ "DependencyLike",
42
+ "ResponseLike",
43
+ "is_dependency_like",
44
+ "is_response_like",
45
+ ]
@@ -0,0 +1,33 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any
5
+
6
+
7
+ @dataclass(frozen=True)
8
+ class URL:
9
+ path: str
10
+ query: dict[str, list[str]]
11
+ script_name: str = ""
12
+
13
+ def __str__(self) -> str:
14
+ base = (self.script_name or "").rstrip("/")
15
+ query_string = "&".join(
16
+ f"{name}={value}" for name, values in self.query.items() for value in values
17
+ )
18
+ path = f"{base}{self.path}" if base else self.path
19
+ return f"{path}?{query_string}" if query_string else path
20
+
21
+
22
+ @dataclass(frozen=True)
23
+ class AwaitableValue:
24
+ value: Any
25
+
26
+ def __await__(self):
27
+ async def _value() -> Any:
28
+ return self.value
29
+
30
+ return _value().__await__()
31
+
32
+
33
+ __all__ = ["URL", "AwaitableValue"]
@@ -0,0 +1,63 @@
1
+ from __future__ import annotations
2
+
3
+ from .exceptions import HTTPException, StatusDetailError
4
+ from .mappings import (
5
+ status,
6
+ HTTP_ERROR_MESSAGES,
7
+ ERROR_MESSAGES,
8
+ _HTTP_TO_RPC,
9
+ _RPC_TO_HTTP,
10
+ )
11
+ from .converters import (
12
+ http_exc_to_rpc,
13
+ rpc_error_to_http,
14
+ _http_exc_to_rpc,
15
+ _rpc_error_to_http,
16
+ create_standardized_error,
17
+ create_standardized_error_from_status,
18
+ to_rpc_error_payload,
19
+ )
20
+ from .exceptions import (
21
+ TigrblError,
22
+ PlanningError,
23
+ LabelError,
24
+ ConfigError,
25
+ SystemStepError,
26
+ ValidationError,
27
+ TransformError,
28
+ DeriveError,
29
+ KernelAbort,
30
+ coerce_runtime_error,
31
+ raise_for_in_errors,
32
+ )
33
+
34
+ __all__ = [
35
+ "HTTPException",
36
+ "StatusDetailError",
37
+ "status",
38
+ # maps & messages
39
+ "HTTP_ERROR_MESSAGES",
40
+ "ERROR_MESSAGES",
41
+ "_HTTP_TO_RPC",
42
+ "_RPC_TO_HTTP",
43
+ # conversions
44
+ "http_exc_to_rpc",
45
+ "rpc_error_to_http",
46
+ "_http_exc_to_rpc",
47
+ "_rpc_error_to_http",
48
+ "create_standardized_error",
49
+ "create_standardized_error_from_status",
50
+ "to_rpc_error_payload",
51
+ # typed errors + helpers
52
+ "TigrblError",
53
+ "PlanningError",
54
+ "LabelError",
55
+ "ConfigError",
56
+ "SystemStepError",
57
+ "ValidationError",
58
+ "TransformError",
59
+ "DeriveError",
60
+ "KernelAbort",
61
+ "coerce_runtime_error",
62
+ "raise_for_in_errors",
63
+ ]
@@ -0,0 +1,222 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Tuple
4
+
5
+ from .utils import (
6
+ PydanticValidationError,
7
+ RequestValidationError,
8
+ IntegrityError,
9
+ DBAPIError,
10
+ OperationalError,
11
+ NoResultFound,
12
+ _is_asyncpg_constraint_error,
13
+ _stringify_exc,
14
+ _format_validation,
15
+ )
16
+ from .exceptions import HTTPException, TigrblError
17
+ from .mappings import (
18
+ status,
19
+ _HTTP_TO_RPC,
20
+ _RPC_TO_HTTP,
21
+ ERROR_MESSAGES,
22
+ HTTP_ERROR_MESSAGES,
23
+ )
24
+
25
+
26
+ def http_exc_to_rpc(exc: HTTPException) -> tuple[int, str, Any | None]:
27
+ """Convert HTTPException → (rpc_code, message, data)."""
28
+ code = _HTTP_TO_RPC.get(exc.status_code, -32603)
29
+ detail = exc.detail
30
+ if isinstance(detail, (dict, list)):
31
+ return code, ERROR_MESSAGES.get(code, "Unknown error"), detail
32
+ msg = getattr(exc, "rpc_message", None) or (
33
+ detail if isinstance(detail, str) else None
34
+ )
35
+ if not msg:
36
+ msg = ERROR_MESSAGES.get(
37
+ code, HTTP_ERROR_MESSAGES.get(exc.status_code, "Unknown error")
38
+ )
39
+ data = getattr(exc, "rpc_data", None)
40
+ return code, msg, data
41
+
42
+
43
+ def rpc_error_to_http(
44
+ rpc_code: int, message: str | None = None, data: Any | None = None
45
+ ) -> HTTPException:
46
+ """Convert JSON-RPC error code (and optional message/data) → HTTPException."""
47
+ http_status = _RPC_TO_HTTP.get(rpc_code, 500)
48
+ msg = (
49
+ message
50
+ or HTTP_ERROR_MESSAGES.get(http_status)
51
+ or ERROR_MESSAGES.get(rpc_code, "Unknown error")
52
+ )
53
+ http_exc = HTTPException(status_code=http_status, detail=msg)
54
+ setattr(http_exc, "rpc_code", rpc_code)
55
+ setattr(http_exc, "rpc_message", msg)
56
+ setattr(http_exc, "rpc_data", data)
57
+ return http_exc
58
+
59
+
60
+ def _http_exc_to_rpc(exc: HTTPException) -> tuple[int, str, Any | None]:
61
+ """Alias for :func:`http_exc_to_rpc` to preserve older import paths."""
62
+ return http_exc_to_rpc(exc)
63
+
64
+
65
+ def _rpc_error_to_http(
66
+ rpc_code: int, message: str | None = None, data: Any | None = None
67
+ ) -> HTTPException:
68
+ """Alias for :func:`rpc_error_to_http` to preserve older import paths."""
69
+ return rpc_error_to_http(rpc_code, message, data)
70
+
71
+
72
+ def _classify_exception(
73
+ exc: BaseException,
74
+ ) -> Tuple[int, str | dict | list, Any | None]:
75
+ """
76
+ Return (http_status, detail_or_message, data) suitable for HTTPException and JSON-RPC mapping.
77
+ `detail_or_message` may be a string OR a structured dict/list (validation).
78
+ """
79
+ # 0) Typed Tigrbl errors
80
+ if isinstance(exc, TigrblError):
81
+ status_code = getattr(exc, "status", 400) or 400
82
+ details = getattr(exc, "details", None)
83
+ if isinstance(details, (dict, list)):
84
+ return status_code, details, details
85
+ return status_code, str(exc) or exc.code, None
86
+
87
+ # 1) Pass-through HTTPException preserving detail
88
+ if isinstance(exc, HTTPException):
89
+ return exc.status_code, exc.detail, getattr(exc, "rpc_data", None)
90
+
91
+ # 1b) Compatibility shim for framework-style exceptions that expose
92
+ # ``status_code`` and ``detail`` but are not our HTTPException type.
93
+ status_code = getattr(exc, "status_code", None)
94
+ if isinstance(status_code, int):
95
+ detail = getattr(exc, "detail", None)
96
+ if detail in (None, ""):
97
+ detail = _stringify_exc(exc)
98
+ return int(status_code), detail, None
99
+
100
+ # 2) Validation errors → 422 with structured data
101
+ if (PydanticValidationError is not None) and isinstance(
102
+ exc, PydanticValidationError
103
+ ):
104
+ return (
105
+ status.HTTP_422_UNPROCESSABLE_ENTITY,
106
+ HTTP_ERROR_MESSAGES.get(422, "Validation failed"),
107
+ _format_validation(exc),
108
+ )
109
+ if (RequestValidationError is not None) and isinstance(exc, RequestValidationError):
110
+ return (
111
+ status.HTTP_422_UNPROCESSABLE_ENTITY,
112
+ HTTP_ERROR_MESSAGES.get(422, "Validation failed"),
113
+ _format_validation(exc),
114
+ )
115
+
116
+ # 3) Common client errors
117
+ if isinstance(exc, (ValueError, TypeError, KeyError)):
118
+ return status.HTTP_400_BAD_REQUEST, _stringify_exc(exc), None
119
+ if isinstance(exc, PermissionError):
120
+ return status.HTTP_403_FORBIDDEN, _stringify_exc(exc), None
121
+ if isinstance(exc, NotImplementedError):
122
+ return status.HTTP_501_NOT_IMPLEMENTED, _stringify_exc(exc), None
123
+ if isinstance(exc, TimeoutError):
124
+ return status.HTTP_504_GATEWAY_TIMEOUT, _stringify_exc(exc), None
125
+
126
+ # 4) ORM/DB mapping
127
+ if (NoResultFound is not None) and isinstance(exc, NoResultFound):
128
+ return status.HTTP_404_NOT_FOUND, "Resource not found", None
129
+
130
+ if _is_asyncpg_constraint_error(exc):
131
+ return status.HTTP_409_CONFLICT, _stringify_exc(exc), None
132
+
133
+ if (IntegrityError is not None) and isinstance(exc, IntegrityError):
134
+ msg = _stringify_exc(exc)
135
+ lower_msg = msg.lower()
136
+ if "not null constraint" in lower_msg or "check constraint" in lower_msg:
137
+ return status.HTTP_422_UNPROCESSABLE_ENTITY, msg, None
138
+ return status.HTTP_409_CONFLICT, msg, None
139
+
140
+ if (OperationalError is not None) and isinstance(exc, OperationalError):
141
+ return status.HTTP_503_SERVICE_UNAVAILABLE, _stringify_exc(exc), None
142
+
143
+ if (DBAPIError is not None) and isinstance(exc, DBAPIError):
144
+ return status.HTTP_500_INTERNAL_SERVER_ERROR, _stringify_exc(exc), None
145
+
146
+ # 5) Fallback
147
+ return status.HTTP_500_INTERNAL_SERVER_ERROR, _stringify_exc(exc), None
148
+
149
+
150
+ def create_standardized_error(exc: BaseException) -> HTTPException:
151
+ """
152
+ Normalize any exception → HTTPException with attached RPC context:
153
+ • .rpc_code
154
+ • .rpc_message
155
+ • .rpc_data
156
+ """
157
+ http_status, detail_or_message, data = _classify_exception(exc)
158
+ rpc_code = _HTTP_TO_RPC.get(http_status, -32603)
159
+ if isinstance(detail_or_message, (dict, list)):
160
+ http_detail = detail_or_message
161
+ rpc_message = ERROR_MESSAGES.get(
162
+ rpc_code, HTTP_ERROR_MESSAGES.get(http_status, "Unknown error")
163
+ )
164
+ else:
165
+ http_detail = detail_or_message
166
+ rpc_message = detail_or_message or ERROR_MESSAGES.get(
167
+ rpc_code, HTTP_ERROR_MESSAGES.get(http_status, "Unknown error")
168
+ )
169
+ http_exc = HTTPException(status_code=http_status, detail=http_detail)
170
+ setattr(http_exc, "rpc_code", rpc_code)
171
+ setattr(http_exc, "rpc_message", rpc_message)
172
+ setattr(http_exc, "rpc_data", data)
173
+ return http_exc
174
+
175
+
176
+ def create_standardized_error_from_status(
177
+ http_status: int,
178
+ message: str | None = None,
179
+ *,
180
+ rpc_code: int | None = None,
181
+ data: Any | None = None,
182
+ ) -> tuple[HTTPException, int, str]:
183
+ """Explicit constructor used by code paths that already decided on an HTTP status."""
184
+ if rpc_code is None:
185
+ rpc_code = _HTTP_TO_RPC.get(http_status, -32603)
186
+ if message is None:
187
+ http_message = HTTP_ERROR_MESSAGES.get(http_status) or ERROR_MESSAGES.get(
188
+ rpc_code, "Unknown error"
189
+ )
190
+ rpc_message = ERROR_MESSAGES.get(rpc_code) or HTTP_ERROR_MESSAGES.get(
191
+ http_status, "Unknown error"
192
+ )
193
+ else:
194
+ http_message = rpc_message = message
195
+ http_exc = HTTPException(status_code=http_status, detail=http_message)
196
+ setattr(http_exc, "rpc_code", rpc_code)
197
+ setattr(http_exc, "rpc_message", rpc_message)
198
+ setattr(http_exc, "rpc_data", data)
199
+ return http_exc, rpc_code, rpc_message
200
+
201
+
202
+ def to_rpc_error_payload(exc: HTTPException) -> dict:
203
+ """Produce a JSON-RPC error object from an HTTPException (with or without rpc_* attrs)."""
204
+ code, msg, data = http_exc_to_rpc(exc)
205
+ payload = {"code": code, "message": msg}
206
+ if data is not None:
207
+ payload["data"] = data
208
+ else:
209
+ if isinstance(exc.detail, (dict, list)):
210
+ payload["data"] = exc.detail
211
+ return payload
212
+
213
+
214
+ __all__ = [
215
+ "http_exc_to_rpc",
216
+ "rpc_error_to_http",
217
+ "_http_exc_to_rpc",
218
+ "_rpc_error_to_http",
219
+ "create_standardized_error",
220
+ "create_standardized_error_from_status",
221
+ "to_rpc_error_payload",
222
+ ]
@@ -0,0 +1,149 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict, Mapping, Optional
4
+
5
+ from .utils import _read_in_errors, _has_in_errors
6
+
7
+
8
+ class StatusDetailError(Exception):
9
+ def __init__(
10
+ self,
11
+ status_code: int,
12
+ detail: Any = "",
13
+ headers: Mapping[str, str] | None = None,
14
+ ) -> None:
15
+ super().__init__(detail)
16
+ self.status_code = int(status_code)
17
+ self.detail = detail
18
+ self.headers = dict(headers or {})
19
+
20
+
21
+ class HTTPException(StatusDetailError):
22
+ def __init__(
23
+ self,
24
+ status_code: int,
25
+ detail: Any = "",
26
+ headers: Mapping[str, str] | None = None,
27
+ ) -> None:
28
+ super().__init__(status_code=status_code, detail=detail, headers=headers)
29
+
30
+
31
+ class TigrblError(Exception):
32
+ """Base class for runtime errors in Tigrbl v3."""
33
+
34
+ code: str = "tigrbl_error"
35
+ status: int = 400
36
+
37
+ def __init__(
38
+ self,
39
+ message: str = "",
40
+ *,
41
+ code: Optional[str] = None,
42
+ status: Optional[int] = None,
43
+ details: Any = None,
44
+ cause: Optional[BaseException] = None,
45
+ ):
46
+ super().__init__(message)
47
+ if cause is not None:
48
+ self.__cause__ = cause
49
+ if code is not None:
50
+ self.code = code
51
+ if status is not None:
52
+ self.status = status
53
+ self.details = details
54
+
55
+ def to_dict(self) -> Dict[str, Any]:
56
+ d = {
57
+ "type": self.__class__.__name__,
58
+ "code": self.code,
59
+ "status": self.status,
60
+ "message": str(self),
61
+ }
62
+ if self.details is not None:
63
+ d["details"] = self.details
64
+ return d
65
+
66
+
67
+ class PlanningError(TigrblError):
68
+ code = "planning_error"
69
+ status = 500
70
+
71
+
72
+ class LabelError(TigrblError):
73
+ code = "label_error"
74
+ status = 400
75
+
76
+
77
+ class ConfigError(TigrblError):
78
+ code = "config_error"
79
+ status = 400
80
+
81
+
82
+ class SystemStepError(TigrblError):
83
+ code = "system_step_error"
84
+ status = 500
85
+
86
+
87
+ class ValidationError(TigrblError):
88
+ code = "validation_error"
89
+ status = 422
90
+
91
+ @staticmethod
92
+ def from_ctx(
93
+ ctx: Any, message: str = "Input validation failed."
94
+ ) -> "ValidationError":
95
+ return ValidationError(message, status=422, details=_read_in_errors(ctx))
96
+
97
+
98
+ class TransformError(TigrblError):
99
+ code = "transform_error"
100
+ status = 400
101
+
102
+
103
+ class DeriveError(TigrblError):
104
+ code = "derive_error"
105
+ status = 400
106
+
107
+
108
+ class KernelAbort(TigrblError):
109
+ code = "kernel_abort"
110
+ status = 403
111
+
112
+
113
+ def coerce_runtime_error(exc: BaseException, ctx: Any | None = None) -> TigrblError:
114
+ """
115
+ Map arbitrary exceptions to a typed TigrblError for consistent kernel handling.
116
+ - Already TigrblError → return as-is
117
+ - ValueError + ctx.temp['in_errors'] → ValidationError
118
+ - Otherwise → generic TigrblError
119
+ """
120
+ if isinstance(exc, TigrblError):
121
+ return exc
122
+ if isinstance(exc, ValueError) and ctx is not None and _has_in_errors(ctx):
123
+ return ValidationError.from_ctx(
124
+ ctx, message=str(exc) or "Input validation failed."
125
+ )
126
+ return TigrblError(str(exc) or exc.__class__.__name__)
127
+
128
+
129
+ def raise_for_in_errors(ctx: Any) -> None:
130
+ """Raise a typed ValidationError if ctx.temp['in_errors'] indicates invalid input."""
131
+ if _has_in_errors(ctx):
132
+ raise ValidationError.from_ctx(ctx)
133
+
134
+
135
+ __all__ = [
136
+ "StatusDetailError",
137
+ "HTTPException",
138
+ "TigrblError",
139
+ "PlanningError",
140
+ "LabelError",
141
+ "ConfigError",
142
+ "SystemStepError",
143
+ "ValidationError",
144
+ "TransformError",
145
+ "DeriveError",
146
+ "KernelAbort",
147
+ "coerce_runtime_error",
148
+ "raise_for_in_errors",
149
+ ]
@@ -0,0 +1,94 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ class _Status:
5
+ HTTP_200_OK = 200
6
+ HTTP_201_CREATED = 201
7
+ HTTP_204_NO_CONTENT = 204
8
+ HTTP_400_BAD_REQUEST = 400
9
+ HTTP_401_UNAUTHORIZED = 401
10
+ HTTP_403_FORBIDDEN = 403
11
+ HTTP_404_NOT_FOUND = 404
12
+ HTTP_405_METHOD_NOT_ALLOWED = 405
13
+ HTTP_409_CONFLICT = 409
14
+ HTTP_422_UNPROCESSABLE_ENTITY = 422
15
+ HTTP_429_TOO_MANY_REQUESTS = 429
16
+ HTTP_500_INTERNAL_SERVER_ERROR = 500
17
+ HTTP_501_NOT_IMPLEMENTED = 501
18
+ HTTP_503_SERVICE_UNAVAILABLE = 503
19
+ HTTP_504_GATEWAY_TIMEOUT = 504
20
+
21
+
22
+ status = _Status()
23
+
24
+ # HTTP → JSON-RPC code map
25
+ _HTTP_TO_RPC: dict[int, int] = {
26
+ 400: -32602,
27
+ 401: -32001,
28
+ 403: -32002,
29
+ 404: -32003,
30
+ 409: -32004,
31
+ 422: -32602,
32
+ 500: -32603,
33
+ 501: -32603,
34
+ 503: -32603,
35
+ 504: -32603,
36
+ }
37
+
38
+ # JSON-RPC → HTTP status map
39
+ _RPC_TO_HTTP: dict[int, int] = {
40
+ -32700: 400,
41
+ -32600: 400,
42
+ -32601: 404,
43
+ -32602: 400,
44
+ -32603: 500,
45
+ -32001: 401,
46
+ -32002: 403,
47
+ -32003: 404,
48
+ -32004: 409,
49
+ }
50
+
51
+ # Standardized error messages
52
+ ERROR_MESSAGES: dict[int, str] = {
53
+ -32700: "Parse error",
54
+ -32600: "Invalid Request",
55
+ -32601: "Method not found",
56
+ -32602: "Invalid params",
57
+ -32603: "Internal error",
58
+ -32001: "Authentication required",
59
+ -32002: "Insufficient permissions",
60
+ -32003: "Resource not found",
61
+ -32004: "Resource conflict",
62
+ -32000: "Server error",
63
+ -32099: "Duplicate key constraint violation",
64
+ -32098: "Data constraint violation",
65
+ -32097: "Foreign key constraint violation",
66
+ -32096: "Authentication required",
67
+ -32095: "Authorization failed",
68
+ -32094: "Resource not found",
69
+ -32093: "Validation error",
70
+ -32092: "Transaction failed",
71
+ }
72
+
73
+ # HTTP status code → standardized message
74
+ HTTP_ERROR_MESSAGES: dict[int, str] = {
75
+ 400: "Bad Request: malformed input",
76
+ 401: "Unauthorized: authentication required",
77
+ 403: "Forbidden: insufficient permissions",
78
+ 404: "Not Found: resource does not exist",
79
+ 409: "Conflict: duplicate key or constraint violation",
80
+ 422: "Unprocessable Entity: validation failed",
81
+ 500: "Internal Server Error: unexpected server error",
82
+ 501: "Not Implemented",
83
+ 503: "Service Unavailable",
84
+ 504: "Gateway Timeout",
85
+ }
86
+
87
+ __all__ = [
88
+ "_Status",
89
+ "status",
90
+ "_HTTP_TO_RPC",
91
+ "_RPC_TO_HTTP",
92
+ "ERROR_MESSAGES",
93
+ "HTTP_ERROR_MESSAGES",
94
+ ]
@@ -0,0 +1,114 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict, Iterable, List, Mapping
4
+ import logging
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+ # Optional imports – code must run even if these packages aren’t installed.
9
+ try:
10
+ from pydantic import ValidationError as PydanticValidationError # v2
11
+ except Exception: # pragma: no cover
12
+ PydanticValidationError = None # type: ignore
13
+
14
+ try:
15
+ import importlib
16
+
17
+ _http_exc = importlib.import_module("fast" + "router.exceptions")
18
+ RequestValidationError = _http_exc.RequestValidationError
19
+ except Exception: # pragma: no cover
20
+ RequestValidationError = None # type: ignore
21
+
22
+ try:
23
+ # SQLAlchemy v1/v2 exception sets
24
+ from sqlalchemy.exc import IntegrityError, DBAPIError, OperationalError
25
+ from sqlalchemy.orm.exc import NoResultFound # type: ignore
26
+ except Exception: # pragma: no cover
27
+ IntegrityError = DBAPIError = OperationalError = NoResultFound = None # type: ignore
28
+
29
+
30
+ # Detect asyncpg constraint errors without importing asyncpg (optional dep).
31
+ _ASYNCPG_CONSTRAINT_NAMES = {
32
+ "UniqueViolationError",
33
+ "ForeignKeyViolationError",
34
+ "NotNullViolationError",
35
+ "CheckViolationError",
36
+ "ExclusionViolationError",
37
+ }
38
+
39
+
40
+ def _is_asyncpg_constraint_error(exc: BaseException) -> bool:
41
+ cls = type(exc)
42
+ return (cls.__module__ or "").startswith("asyncpg") and (
43
+ cls.__name__ in _ASYNCPG_CONSTRAINT_NAMES
44
+ )
45
+
46
+
47
+ def _limit(s: str, n: int = 4000) -> str:
48
+ return s if len(s) <= n else s[: n - 3] + "..."
49
+
50
+
51
+ def _stringify_exc(exc: BaseException) -> str:
52
+ detail = getattr(exc, "detail", None)
53
+ if detail:
54
+ return _limit(str(detail))
55
+ return _limit(f"{exc.__class__!r}: {str(exc) or repr(exc)}")
56
+
57
+
58
+ def _format_validation(err: Any) -> Any:
59
+ try:
60
+ items = err.errors() # pydantic / asgi RequestValidationError
61
+ if isinstance(items, Iterable):
62
+ return list(items)
63
+ except Exception: # pragma: no cover
64
+ pass
65
+ return _limit(str(err))
66
+
67
+
68
+ def _get_temp(ctx: Any) -> Mapping[str, Any]:
69
+ tmp = getattr(ctx, "temp", None)
70
+ return tmp if isinstance(tmp, Mapping) else {}
71
+
72
+
73
+ def _has_in_errors(ctx: Any) -> bool:
74
+ tmp = _get_temp(ctx)
75
+ if tmp.get("in_invalid") is True:
76
+ return True
77
+ errs = tmp.get("in_errors")
78
+ return isinstance(errs, (list, tuple)) and len(errs) > 0
79
+
80
+
81
+ def _read_in_errors(ctx: Any) -> List[Dict[str, Any]]:
82
+ tmp = _get_temp(ctx)
83
+ errs = tmp.get("in_errors")
84
+ if isinstance(errs, list):
85
+ norm: List[Dict[str, Any]] = []
86
+ for e in errs:
87
+ if isinstance(e, Mapping):
88
+ field = e.get("field")
89
+ code = e.get("code") or "invalid"
90
+ msg = e.get("message") or "Invalid value."
91
+ entry = {"field": field, "code": code, "message": msg}
92
+ for k, v in e.items():
93
+ if k not in entry:
94
+ entry[k] = v
95
+ norm.append(entry)
96
+ return norm
97
+ return []
98
+
99
+
100
+ __all__ = [
101
+ "PydanticValidationError",
102
+ "RequestValidationError",
103
+ "IntegrityError",
104
+ "DBAPIError",
105
+ "OperationalError",
106
+ "NoResultFound",
107
+ "_is_asyncpg_constraint_error",
108
+ "_limit",
109
+ "_stringify_exc",
110
+ "_format_validation",
111
+ "_get_temp",
112
+ "_has_in_errors",
113
+ "_read_in_errors",
114
+ ]
@@ -0,0 +1,129 @@
1
+ # ── Standard Library ─────────────────────────────────────────────────────
2
+ from types import MethodType, SimpleNamespace
3
+ from uuid import uuid4, UUID
4
+
5
+ # ── Third-party Dependencies (via deps module) ───────────────────────────
6
+ from ..vendor.sqlalchemy import (
7
+ # Core SQLAlchemy
8
+ Boolean,
9
+ Column,
10
+ _DateTime,
11
+ SAEnum,
12
+ Text,
13
+ ForeignKey,
14
+ Index,
15
+ Integer,
16
+ JSON,
17
+ Numeric,
18
+ String,
19
+ LargeBinary,
20
+ UniqueConstraint,
21
+ CheckConstraint,
22
+ create_engine,
23
+ event,
24
+ # PostgreSQL dialect
25
+ ARRAY,
26
+ PgEnum,
27
+ JSONB,
28
+ TSVECTOR,
29
+ # ORM
30
+ Mapped,
31
+ declarative_mixin,
32
+ declared_attr,
33
+ foreign,
34
+ mapped_column,
35
+ relationship,
36
+ remote,
37
+ column_property,
38
+ Session,
39
+ sessionmaker,
40
+ InstrumentedAttribute,
41
+ # Extensions
42
+ MutableDict,
43
+ MutableList,
44
+ hybrid_property,
45
+ StaticPool,
46
+ TypeDecorator,
47
+ )
48
+
49
+
50
+ from ..vendor.pydantic import (
51
+ BaseModel,
52
+ Field,
53
+ ValidationError,
54
+ )
55
+
56
+ from ..status.exceptions import StatusDetailError
57
+
58
+ # ── Local Package ─────────────────────────────────────────────────────────
59
+ from .op import _Op, _SchemaVerb
60
+ from .uuid import PgUUID, SqliteUUID
61
+ from .authn_abc import AuthNProvider
62
+
63
+ # ── Generics / Extensions ─────────────────────────────────────────────────
64
+ DateTime = _DateTime(timezone=False)
65
+ TZDateTime = _DateTime(timezone=True)
66
+
67
+
68
+ # ── Public Re-exports (Backwards Compatibility) ──────────────────────────
69
+ __all__: list[str] = [
70
+ # local
71
+ "_Op",
72
+ "_SchemaVerb",
73
+ "AuthNProvider",
74
+ # add ons
75
+ "SqliteUUID",
76
+ # builtin types
77
+ "MethodType",
78
+ "SimpleNamespace",
79
+ "uuid4",
80
+ "UUID",
81
+ # sqlalchemy core (from deps.sqlalchemy)
82
+ "Boolean",
83
+ "Column",
84
+ "DateTime",
85
+ "TZDateTime",
86
+ "Text",
87
+ "SAEnum",
88
+ "ForeignKey",
89
+ "Index",
90
+ "Integer",
91
+ "JSON",
92
+ "Numeric",
93
+ "String",
94
+ "LargeBinary",
95
+ "UniqueConstraint",
96
+ "CheckConstraint",
97
+ "create_engine",
98
+ "event",
99
+ # sqlalchemy.dialects.postgresql (from deps.sqlalchemy)
100
+ "ARRAY",
101
+ "PgEnum",
102
+ "JSONB",
103
+ "PgUUID",
104
+ "TSVECTOR",
105
+ # sqlalchemy.orm (from deps.sqlalchemy)
106
+ "Mapped",
107
+ "declarative_mixin",
108
+ "declared_attr",
109
+ "foreign",
110
+ "mapped_column",
111
+ "column_property",
112
+ "hybrid_property",
113
+ "relationship",
114
+ "remote",
115
+ "Session",
116
+ "sessionmaker",
117
+ "InstrumentedAttribute",
118
+ # sqlalchemy.ext.mutable (from deps.sqlalchemy)
119
+ "MutableDict",
120
+ "MutableList",
121
+ "StaticPool",
122
+ "TypeDecorator",
123
+ # pydantic schema support (from deps.pydantic)
124
+ "BaseModel",
125
+ "Field",
126
+ "ValidationError",
127
+ # status
128
+ "StatusDetailError",
129
+ ]
@@ -0,0 +1,33 @@
1
+ # tigrbl/types/authn_abc.py
2
+ from __future__ import annotations
3
+ from abc import ABC, abstractmethod
4
+ from typing import TYPE_CHECKING
5
+
6
+ if TYPE_CHECKING:
7
+ from tigrbl import Request
8
+
9
+
10
+ class AuthNProvider(ABC):
11
+ """
12
+ Marker‑interface that any AuthN extension must implement
13
+ so that Tigrbl can plug itself in at run‑time.
14
+ """
15
+
16
+ # ---------- ASGI dependency ----------
17
+ @abstractmethod
18
+ async def get_principal(self, request: "Request"): # -> dict[str, str]
19
+ """Return {"sub": user_id, "tid": tenant_id, ...} or raise HTTP 401."""
20
+
21
+
22
+ __all__ = ["AuthNProvider"]
23
+
24
+
25
+ for _name in list(globals()):
26
+ if _name not in __all__ and not _name.startswith("__"):
27
+ del globals()[_name]
28
+
29
+
30
+ def __dir__():
31
+ """Tighten ``dir()`` output for interactive sessions."""
32
+
33
+ return sorted(__all__)
@@ -0,0 +1,35 @@
1
+ """
2
+ tigrbl/types/op.py
3
+ Pure structural helpers."""
4
+
5
+ from typing import Any, Callable, NamedTuple, Type, Literal, TypeAlias
6
+
7
+ _SchemaVerb: TypeAlias = Literal[
8
+ "create",
9
+ "read",
10
+ "update",
11
+ "replace",
12
+ "merge",
13
+ "delete",
14
+ "list",
15
+ "clear",
16
+ ]
17
+
18
+ # need to add clear
19
+ # need to add support for bulk create, update, delete
20
+
21
+
22
+ class _Op(NamedTuple):
23
+ """
24
+ Metadata for one REST/RPC operation registered by Tigrbl.
25
+ """
26
+
27
+ verb: str # e.g. "create", "list"
28
+ http: str # "POST" | "GET" | "PATCH" | …
29
+ path: str # URL suffix, e.g. "/{item_id}"
30
+ In: Type | None # Pydantic input model (or None)
31
+ Out: Type # Pydantic output model
32
+ core: Callable[..., Any] # The actual implementation
33
+
34
+
35
+ __all__ = ["_Op", "_SchemaVerb"]
@@ -0,0 +1,55 @@
1
+ # ── Standard Library ─────────────────────────────────────────────────────
2
+ from __future__ import annotations
3
+
4
+ from typing import Any
5
+ import uuid
6
+
7
+ # ── Third-party Dependencies ────────────────────────────────────────────
8
+ from sqlalchemy.types import TypeDecorator
9
+
10
+ # ── Local Package ───────────────────────────────────────────────────────
11
+ from ..vendor.sqlalchemy import String, _PgUUID
12
+
13
+
14
+ class PgUUID(_PgUUID):
15
+ @property
16
+ def hex(self):
17
+ return self.as_uuid.hex
18
+
19
+
20
+ class SqliteUUID(TypeDecorator):
21
+ """UUID type that stores hyphenated strings on SQLite to avoid numeric coercion."""
22
+
23
+ impl = String(36)
24
+ cache_ok = True
25
+
26
+ def __init__(self, as_uuid: bool = True):
27
+ super().__init__()
28
+ self.as_uuid = as_uuid
29
+
30
+ @property
31
+ def python_type(self) -> type:
32
+ return uuid.UUID if self.as_uuid else str
33
+
34
+ def load_dialect_impl(self, dialect) -> Any:
35
+ if dialect.name == "postgresql":
36
+ return dialect.type_descriptor(PgUUID(as_uuid=self.as_uuid))
37
+ return dialect.type_descriptor(String(36))
38
+
39
+ def process_bind_param(self, value: Any, dialect) -> Any:
40
+ if value is None:
41
+ return None
42
+ if self.as_uuid:
43
+ if not isinstance(value, uuid.UUID):
44
+ value = uuid.UUID(str(value))
45
+ return value if dialect.name == "postgresql" else str(value)
46
+ return str(value)
47
+
48
+ def process_result_value(self, value: Any, dialect) -> Any:
49
+ if value is None:
50
+ return None
51
+ if self.as_uuid:
52
+ if isinstance(value, uuid.UUID):
53
+ return value
54
+ return uuid.UUID(str(value))
55
+ return str(value)
@@ -0,0 +1 @@
1
+ """Vendor dependency re-export namespace."""
@@ -0,0 +1,5 @@
1
+ """Pydantic symbols re-exported for typing compatibility."""
2
+
3
+ from pydantic import BaseModel, Field, ValidationError
4
+
5
+ __all__ = ["BaseModel", "Field", "ValidationError"]
@@ -0,0 +1,84 @@
1
+ """SQLAlchemy symbols re-exported for typing compatibility."""
2
+
3
+ from sqlalchemy import (
4
+ Boolean,
5
+ CheckConstraint,
6
+ Column,
7
+ DateTime as _DateTime,
8
+ Enum as SAEnum,
9
+ ForeignKey,
10
+ Index,
11
+ Integer,
12
+ JSON,
13
+ LargeBinary,
14
+ Numeric,
15
+ String,
16
+ Text,
17
+ TypeDecorator,
18
+ UniqueConstraint,
19
+ create_engine,
20
+ event,
21
+ )
22
+ from sqlalchemy.dialects.postgresql import (
23
+ ARRAY,
24
+ ENUM as PgEnum,
25
+ JSONB,
26
+ TSVECTOR,
27
+ UUID as _PgUUID,
28
+ )
29
+ from sqlalchemy.ext.hybrid import hybrid_property
30
+ from sqlalchemy.ext.mutable import MutableDict, MutableList
31
+ from sqlalchemy.orm import (
32
+ InstrumentedAttribute,
33
+ Mapped,
34
+ Session,
35
+ column_property,
36
+ declarative_mixin,
37
+ declared_attr,
38
+ foreign,
39
+ mapped_column,
40
+ relationship,
41
+ remote,
42
+ sessionmaker,
43
+ )
44
+ from sqlalchemy.pool import StaticPool
45
+
46
+ __all__ = [
47
+ "Boolean",
48
+ "Column",
49
+ "_DateTime",
50
+ "SAEnum",
51
+ "Text",
52
+ "ForeignKey",
53
+ "Index",
54
+ "Integer",
55
+ "JSON",
56
+ "Numeric",
57
+ "String",
58
+ "LargeBinary",
59
+ "UniqueConstraint",
60
+ "CheckConstraint",
61
+ "create_engine",
62
+ "event",
63
+ "ARRAY",
64
+ "PgEnum",
65
+ "JSONB",
66
+ "TSVECTOR",
67
+ "_PgUUID",
68
+ "Mapped",
69
+ "declarative_mixin",
70
+ "declared_attr",
71
+ "foreign",
72
+ "mapped_column",
73
+ "relationship",
74
+ "remote",
75
+ "column_property",
76
+ "Session",
77
+ "sessionmaker",
78
+ "InstrumentedAttribute",
79
+ "MutableDict",
80
+ "MutableList",
81
+ "hybrid_property",
82
+ "StaticPool",
83
+ "TypeDecorator",
84
+ ]