tigrbl-canon 0.1.0__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.
- tigrbl_canon-0.1.0/PKG-INFO +55 -0
- tigrbl_canon-0.1.0/README.md +29 -0
- tigrbl_canon-0.1.0/pyproject.toml +52 -0
- tigrbl_canon-0.1.0/tigrbl_canon/__init__.py +14 -0
- tigrbl_canon-0.1.0/tigrbl_canon/column/infer/__init__.py +25 -0
- tigrbl_canon-0.1.0/tigrbl_canon/column/infer/core.py +92 -0
- tigrbl_canon-0.1.0/tigrbl_canon/column/infer/jsonhints.py +44 -0
- tigrbl_canon-0.1.0/tigrbl_canon/column/infer/planning.py +133 -0
- tigrbl_canon-0.1.0/tigrbl_canon/column/infer/types.py +102 -0
- tigrbl_canon-0.1.0/tigrbl_canon/column/infer/utils.py +59 -0
- tigrbl_canon-0.1.0/tigrbl_canon/mapping/__init__.py +65 -0
- tigrbl_canon-0.1.0/tigrbl_canon/mapping/app_mro_collect.py +14 -0
- tigrbl_canon-0.1.0/tigrbl_canon/mapping/apply.py +98 -0
- tigrbl_canon-0.1.0/tigrbl_canon/mapping/collect.py +60 -0
- tigrbl_canon-0.1.0/tigrbl_canon/mapping/collect_decorated_schemas.py +78 -0
- tigrbl_canon-0.1.0/tigrbl_canon/mapping/column_mro_collect.py +3 -0
- tigrbl_canon-0.1.0/tigrbl_canon/mapping/columns.py +53 -0
- tigrbl_canon-0.1.0/tigrbl_canon/mapping/config_resolver.py +276 -0
- tigrbl_canon-0.1.0/tigrbl_canon/mapping/context.py +48 -0
- tigrbl_canon-0.1.0/tigrbl_canon/mapping/core_resolver.py +95 -0
- tigrbl_canon-0.1.0/tigrbl_canon/mapping/defaults.py +24 -0
- tigrbl_canon-0.1.0/tigrbl_canon/mapping/diagnostics.py +20 -0
- tigrbl_canon-0.1.0/tigrbl_canon/mapping/engine_resolver.py +385 -0
- tigrbl_canon-0.1.0/tigrbl_canon/mapping/handlers/__init__.py +10 -0
- tigrbl_canon-0.1.0/tigrbl_canon/mapping/handlers/builder.py +118 -0
- tigrbl_canon-0.1.0/tigrbl_canon/mapping/handlers/ctx.py +85 -0
- tigrbl_canon-0.1.0/tigrbl_canon/mapping/handlers/identifiers.py +227 -0
- tigrbl_canon-0.1.0/tigrbl_canon/mapping/handlers/namespaces.py +50 -0
- tigrbl_canon-0.1.0/tigrbl_canon/mapping/handlers/steps.py +121 -0
- tigrbl_canon-0.1.0/tigrbl_canon/mapping/hook_mro_collect.py +122 -0
- tigrbl_canon-0.1.0/tigrbl_canon/mapping/hooks.py +327 -0
- tigrbl_canon-0.1.0/tigrbl_canon/mapping/mapping_resolver.py +5 -0
- tigrbl_canon-0.1.0/tigrbl_canon/mapping/model.py +33 -0
- tigrbl_canon-0.1.0/tigrbl_canon/mapping/model_helpers.py +151 -0
- tigrbl_canon-0.1.0/tigrbl_canon/mapping/model_registry.py +85 -0
- tigrbl_canon-0.1.0/tigrbl_canon/mapping/op_mro_collect.py +135 -0
- tigrbl_canon-0.1.0/tigrbl_canon/mapping/op_resolver.py +3 -0
- tigrbl_canon-0.1.0/tigrbl_canon/mapping/passes.py +74 -0
- tigrbl_canon-0.1.0/tigrbl_canon/mapping/plan.py +54 -0
- tigrbl_canon-0.1.0/tigrbl_canon/mapping/precedence.py +45 -0
- tigrbl_canon-0.1.0/tigrbl_canon/mapping/responses_resolver.py +14 -0
- tigrbl_canon-0.1.0/tigrbl_canon/mapping/rest/__init__.py +7 -0
- tigrbl_canon-0.1.0/tigrbl_canon/mapping/rest/attach.py +25 -0
- tigrbl_canon-0.1.0/tigrbl_canon/mapping/rest/collection.py +290 -0
- tigrbl_canon-0.1.0/tigrbl_canon/mapping/rest/common.py +121 -0
- tigrbl_canon-0.1.0/tigrbl_canon/mapping/rest/helpers.py +29 -0
- tigrbl_canon-0.1.0/tigrbl_canon/mapping/rest/io.py +335 -0
- tigrbl_canon-0.1.0/tigrbl_canon/mapping/rest/io_headers.py +49 -0
- tigrbl_canon-0.1.0/tigrbl_canon/mapping/rest/member.py +200 -0
- tigrbl_canon-0.1.0/tigrbl_canon/mapping/rest/router.py +332 -0
- tigrbl_canon-0.1.0/tigrbl_canon/mapping/rest/routing.py +137 -0
- tigrbl_canon-0.1.0/tigrbl_canon/mapping/router/common.py +100 -0
- tigrbl_canon-0.1.0/tigrbl_canon/mapping/router/include.py +464 -0
- tigrbl_canon-0.1.0/tigrbl_canon/mapping/router/resource_proxy.py +13 -0
- tigrbl_canon-0.1.0/tigrbl_canon/mapping/router/rpc.py +13 -0
- tigrbl_canon-0.1.0/tigrbl_canon/mapping/router_mro_collect.py +45 -0
- tigrbl_canon-0.1.0/tigrbl_canon/mapping/rpc.py +13 -0
- tigrbl_canon-0.1.0/tigrbl_canon/mapping/runtime_routes.py +20 -0
- tigrbl_canon-0.1.0/tigrbl_canon/mapping/schemas/__init__.py +10 -0
- tigrbl_canon-0.1.0/tigrbl_canon/mapping/schemas/builder.py +354 -0
- tigrbl_canon-0.1.0/tigrbl_canon/mapping/schemas/defaults.py +288 -0
- tigrbl_canon-0.1.0/tigrbl_canon/mapping/schemas/utils.py +192 -0
- tigrbl_canon-0.1.0/tigrbl_canon/mapping/table.py +5 -0
- tigrbl_canon-0.1.0/tigrbl_canon/mapping/table_mro_collect.py +14 -0
- tigrbl_canon-0.1.0/tigrbl_canon/mapping/traversal.py +196 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tigrbl-canon
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Canonical mapping and routing resolution utilities for Tigrbl.
|
|
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: tigrbl
|
|
19
|
+
Requires-Dist: tigrbl-atoms
|
|
20
|
+
Requires-Dist: tigrbl-base
|
|
21
|
+
Requires-Dist: tigrbl-core
|
|
22
|
+
Requires-Dist: tigrbl-ops-oltp
|
|
23
|
+
Requires-Dist: tigrbl-runtime
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+

|
|
27
|
+
|
|
28
|
+
# tigrbl-canon
|
|
29
|
+
|
|
30
|
+
    
|
|
31
|
+
|
|
32
|
+
## Features
|
|
33
|
+
|
|
34
|
+
- Modular package in the Tigrbl namespace.
|
|
35
|
+
- Supports Python 3.10 through 3.12.
|
|
36
|
+
- Distributed as part of the swarmauri-sdk workspace.
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
### uv
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
uv add tigrbl-canon
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### pip
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install tigrbl-canon
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Usage
|
|
53
|
+
|
|
54
|
+
Import from the shared package-specific module namespaces after installation in your environment.
|
|
55
|
+
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+

|
|
2
|
+
|
|
3
|
+
# tigrbl-canon
|
|
4
|
+
|
|
5
|
+
    
|
|
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-canon
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### pip
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install tigrbl-canon
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
Import from the shared package-specific module namespaces after installation in your environment.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "tigrbl-canon"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Canonical mapping and routing resolution utilities for Tigrbl."
|
|
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
|
+
"tigrbl-ops-oltp",
|
|
22
|
+
"tigrbl-base",
|
|
23
|
+
"tigrbl-core",
|
|
24
|
+
"tigrbl-runtime",
|
|
25
|
+
"tigrbl-atoms",
|
|
26
|
+
"tigrbl",
|
|
27
|
+
]
|
|
28
|
+
keywords = ["tigrbl", "sdk", "standards", "framework"]
|
|
29
|
+
|
|
30
|
+
[tool.uv.sources]
|
|
31
|
+
"tigrbl-ops-oltp" = { workspace = true }
|
|
32
|
+
"tigrbl-base" = { workspace = true }
|
|
33
|
+
"tigrbl-core" = { workspace = true }
|
|
34
|
+
"tigrbl-runtime" = { workspace = true }
|
|
35
|
+
"tigrbl-atoms" = { workspace = true }
|
|
36
|
+
"tigrbl" = { workspace = true }
|
|
37
|
+
|
|
38
|
+
[build-system]
|
|
39
|
+
requires = ["poetry-core>=1.0.0"]
|
|
40
|
+
build-backend = "poetry.core.masonry.api"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
[tool.poetry]
|
|
44
|
+
packages = [
|
|
45
|
+
{ include = "tigrbl_canon" },
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
[dependency-groups]
|
|
49
|
+
dev = [
|
|
50
|
+
"pytest>=8.0",
|
|
51
|
+
"ruff>=0.9",
|
|
52
|
+
]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Tigrbl canon compatibility package."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import warnings
|
|
6
|
+
|
|
7
|
+
_DEPRECATION_MESSAGE = (
|
|
8
|
+
"tigrbl_canon is deprecated, not supported anymore, and likely to break. "
|
|
9
|
+
"Migrate away from tigrbl_canon imports as soon as possible."
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
warnings.warn(_DEPRECATION_MESSAGE, DeprecationWarning, stacklevel=2)
|
|
13
|
+
|
|
14
|
+
__all__ = ["_DEPRECATION_MESSAGE"]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from .core import infer
|
|
2
|
+
from .types import (
|
|
3
|
+
Email,
|
|
4
|
+
Phone,
|
|
5
|
+
DataKind,
|
|
6
|
+
PyTypeInfo,
|
|
7
|
+
SATypePlan,
|
|
8
|
+
JsonHint,
|
|
9
|
+
Inferred,
|
|
10
|
+
InferenceError,
|
|
11
|
+
UnsupportedType,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"infer",
|
|
16
|
+
"Email",
|
|
17
|
+
"Phone",
|
|
18
|
+
"DataKind",
|
|
19
|
+
"PyTypeInfo",
|
|
20
|
+
"SATypePlan",
|
|
21
|
+
"JsonHint",
|
|
22
|
+
"Inferred",
|
|
23
|
+
"InferenceError",
|
|
24
|
+
"UnsupportedType",
|
|
25
|
+
]
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, Optional, get_origin
|
|
4
|
+
|
|
5
|
+
from .types import DataKind, PyTypeInfo, Inferred
|
|
6
|
+
from .utils import _strip_optional, _strip_annotated, _array_item, _is_enum
|
|
7
|
+
from .planning import _plan_sa_type
|
|
8
|
+
from .jsonhints import _json_hint
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def infer(
|
|
12
|
+
annotation: Any,
|
|
13
|
+
*,
|
|
14
|
+
prefer_dialect: Optional[str] = "postgresql",
|
|
15
|
+
max_length: Optional[int] = None,
|
|
16
|
+
decimal_precision: Optional[int] = None,
|
|
17
|
+
decimal_scale: Optional[int] = None,
|
|
18
|
+
) -> Inferred:
|
|
19
|
+
"""Bind-time inference from Python annotation to DataKind and hints."""
|
|
20
|
+
base, is_opt = _strip_optional(annotation)
|
|
21
|
+
base, meta = _strip_annotated(base)
|
|
22
|
+
|
|
23
|
+
enum_cls = _is_enum(base)
|
|
24
|
+
item_tp = _array_item(base)
|
|
25
|
+
|
|
26
|
+
array_item_info: Optional[PyTypeInfo] = None
|
|
27
|
+
if item_tp is not None:
|
|
28
|
+
nested = infer(
|
|
29
|
+
item_tp,
|
|
30
|
+
prefer_dialect=prefer_dialect,
|
|
31
|
+
max_length=max_length,
|
|
32
|
+
decimal_precision=decimal_precision,
|
|
33
|
+
decimal_scale=decimal_scale,
|
|
34
|
+
)
|
|
35
|
+
array_item_info = nested.py
|
|
36
|
+
|
|
37
|
+
py_info = PyTypeInfo(
|
|
38
|
+
base=base,
|
|
39
|
+
is_optional=is_opt,
|
|
40
|
+
enum_cls=enum_cls,
|
|
41
|
+
array_item=array_item_info,
|
|
42
|
+
annotated=meta,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
import datetime as _dt
|
|
46
|
+
import decimal as _dc
|
|
47
|
+
import uuid as _uuid
|
|
48
|
+
|
|
49
|
+
origin = get_origin(base)
|
|
50
|
+
|
|
51
|
+
if enum_cls is not None:
|
|
52
|
+
kind = DataKind.ENUM
|
|
53
|
+
elif item_tp is not None:
|
|
54
|
+
kind = DataKind.ARRAY
|
|
55
|
+
elif base in (str,):
|
|
56
|
+
kind = DataKind.STRING
|
|
57
|
+
elif base in (bytes, bytearray, memoryview):
|
|
58
|
+
kind = DataKind.BYTES
|
|
59
|
+
elif base in (bool,):
|
|
60
|
+
kind = DataKind.BOOL
|
|
61
|
+
elif base in (int,):
|
|
62
|
+
kind = DataKind.INT
|
|
63
|
+
elif base in (float,):
|
|
64
|
+
kind = DataKind.FLOAT
|
|
65
|
+
elif base in (_dc.Decimal,):
|
|
66
|
+
kind = DataKind.DECIMAL
|
|
67
|
+
elif base in (_dt.datetime,):
|
|
68
|
+
kind = DataKind.DATETIME
|
|
69
|
+
elif base in (_dt.date,):
|
|
70
|
+
kind = DataKind.DATE
|
|
71
|
+
elif base in (_dt.time,):
|
|
72
|
+
kind = DataKind.TIME
|
|
73
|
+
elif base in (_uuid.UUID,):
|
|
74
|
+
kind = DataKind.UUID
|
|
75
|
+
else:
|
|
76
|
+
if origin in (dict, Dict):
|
|
77
|
+
kind = DataKind.JSON
|
|
78
|
+
else:
|
|
79
|
+
kind = DataKind.JSON
|
|
80
|
+
|
|
81
|
+
sa = _plan_sa_type(
|
|
82
|
+
kind,
|
|
83
|
+
py_info,
|
|
84
|
+
prefer_dialect=prefer_dialect,
|
|
85
|
+
max_length=max_length,
|
|
86
|
+
decimal_precision=decimal_precision,
|
|
87
|
+
decimal_scale=decimal_scale,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
jh = _json_hint(kind, py_info, max_length=max_length)
|
|
91
|
+
|
|
92
|
+
return Inferred(kind=kind, py=py_info, sa=sa, json=jh, nullable=is_opt)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from .types import DataKind, PyTypeInfo, JsonHint, Email, Phone
|
|
6
|
+
from .planning import _nested_kind_from_py
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _json_hint(
|
|
10
|
+
kind: DataKind, py: PyTypeInfo, *, max_length: Optional[int]
|
|
11
|
+
) -> JsonHint:
|
|
12
|
+
if kind is DataKind.STRING:
|
|
13
|
+
fmt = None
|
|
14
|
+
if Email in py.annotated:
|
|
15
|
+
fmt = "email"
|
|
16
|
+
if Phone in py.annotated:
|
|
17
|
+
fmt = "phone"
|
|
18
|
+
return JsonHint(type="string", format=fmt, maxLength=max_length)
|
|
19
|
+
if kind is DataKind.BYTES:
|
|
20
|
+
return JsonHint(type="string", format="byte")
|
|
21
|
+
if kind is DataKind.BOOL:
|
|
22
|
+
return JsonHint(type="boolean")
|
|
23
|
+
if kind is DataKind.INT or kind is DataKind.BIGINT:
|
|
24
|
+
return JsonHint(type="integer")
|
|
25
|
+
if kind is DataKind.FLOAT or kind is DataKind.DECIMAL:
|
|
26
|
+
return JsonHint(type="number")
|
|
27
|
+
if kind is DataKind.DATE:
|
|
28
|
+
return JsonHint(type="string", format="date")
|
|
29
|
+
if kind is DataKind.TIME:
|
|
30
|
+
return JsonHint(type="string", format="time")
|
|
31
|
+
if kind is DataKind.DATETIME:
|
|
32
|
+
return JsonHint(type="string", format="date-time")
|
|
33
|
+
if kind is DataKind.UUID:
|
|
34
|
+
return JsonHint(type="string", format="uuid")
|
|
35
|
+
if kind is DataKind.JSON:
|
|
36
|
+
return JsonHint(type="object")
|
|
37
|
+
if kind is DataKind.ENUM and py.enum_cls:
|
|
38
|
+
return JsonHint(type="string", enum=[e.name for e in py.enum_cls])
|
|
39
|
+
if kind is DataKind.ARRAY and py.array_item:
|
|
40
|
+
elem_kind = _nested_kind_from_py(py.array_item)
|
|
41
|
+
return JsonHint(
|
|
42
|
+
type="array", items=_json_hint(elem_kind, py.array_item, max_length=None)
|
|
43
|
+
)
|
|
44
|
+
return JsonHint(type="string")
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, Optional
|
|
4
|
+
|
|
5
|
+
from .types import DataKind, PyTypeInfo, SATypePlan, InferenceError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _plan_sa_type(
|
|
9
|
+
kind: DataKind,
|
|
10
|
+
py: PyTypeInfo,
|
|
11
|
+
*,
|
|
12
|
+
prefer_dialect: Optional[str],
|
|
13
|
+
max_length: Optional[int],
|
|
14
|
+
decimal_precision: Optional[int],
|
|
15
|
+
decimal_scale: Optional[int],
|
|
16
|
+
) -> SATypePlan:
|
|
17
|
+
d = prefer_dialect
|
|
18
|
+
|
|
19
|
+
if kind is DataKind.STRING:
|
|
20
|
+
if max_length and max_length > 0:
|
|
21
|
+
return SATypePlan(
|
|
22
|
+
name="String", args=(max_length,), kwargs={}, dialect=None
|
|
23
|
+
)
|
|
24
|
+
return SATypePlan(name="String", args=(), kwargs={}, dialect=None)
|
|
25
|
+
|
|
26
|
+
if kind is DataKind.TEXT:
|
|
27
|
+
return SATypePlan(name="Text", args=(), kwargs={}, dialect=None)
|
|
28
|
+
|
|
29
|
+
if kind is DataKind.BYTES:
|
|
30
|
+
return SATypePlan(name="LargeBinary", args=(), kwargs={}, dialect=None)
|
|
31
|
+
|
|
32
|
+
if kind is DataKind.BOOL:
|
|
33
|
+
return SATypePlan(name="Boolean", args=(), kwargs={}, dialect=None)
|
|
34
|
+
|
|
35
|
+
if kind is DataKind.INT:
|
|
36
|
+
return SATypePlan(name="Integer", args=(), kwargs={}, dialect=None)
|
|
37
|
+
|
|
38
|
+
if kind is DataKind.BIGINT:
|
|
39
|
+
return SATypePlan(name="BigInteger", args=(), kwargs={}, dialect=None)
|
|
40
|
+
|
|
41
|
+
if kind is DataKind.FLOAT:
|
|
42
|
+
return SATypePlan(name="Float", args=(), kwargs={}, dialect=None)
|
|
43
|
+
|
|
44
|
+
if kind is DataKind.DECIMAL:
|
|
45
|
+
kwargs: Dict[str, Any] = {}
|
|
46
|
+
if decimal_precision is not None:
|
|
47
|
+
kwargs["precision"] = decimal_precision
|
|
48
|
+
if decimal_scale is not None:
|
|
49
|
+
kwargs["scale"] = decimal_scale
|
|
50
|
+
return SATypePlan(name="Numeric", args=(), kwargs=kwargs, dialect=None)
|
|
51
|
+
|
|
52
|
+
if kind is DataKind.DATE:
|
|
53
|
+
return SATypePlan(name="Date", args=(), kwargs={}, dialect=None)
|
|
54
|
+
|
|
55
|
+
if kind is DataKind.TIME:
|
|
56
|
+
return SATypePlan(name="Time", args=(), kwargs={"timezone": True}, dialect=None)
|
|
57
|
+
|
|
58
|
+
if kind is DataKind.DATETIME:
|
|
59
|
+
return SATypePlan(
|
|
60
|
+
name="DateTime", args=(), kwargs={"timezone": True}, dialect=None
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
if kind is DataKind.UUID:
|
|
64
|
+
if d == "postgresql":
|
|
65
|
+
return SATypePlan(
|
|
66
|
+
name="UUID", args=(), kwargs={"as_uuid": True}, dialect="postgresql"
|
|
67
|
+
)
|
|
68
|
+
return SATypePlan(name="String", args=(36,), kwargs={}, dialect=None)
|
|
69
|
+
|
|
70
|
+
if kind is DataKind.JSON:
|
|
71
|
+
if d == "postgresql":
|
|
72
|
+
return SATypePlan(name="JSONB", args=(), kwargs={}, dialect="postgresql")
|
|
73
|
+
return SATypePlan(name="JSON", args=(), kwargs={}, dialect=None)
|
|
74
|
+
|
|
75
|
+
if kind is DataKind.ENUM:
|
|
76
|
+
if not py.enum_cls:
|
|
77
|
+
raise InferenceError("ENUM kind requires enum_cls in PyTypeInfo")
|
|
78
|
+
return SATypePlan(
|
|
79
|
+
name="Enum", args=(py.enum_cls,), kwargs={"native_enum": True}, dialect=None
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
if kind is DataKind.ARRAY:
|
|
83
|
+
if not py.array_item:
|
|
84
|
+
raise InferenceError("ARRAY kind requires array_item in PyTypeInfo")
|
|
85
|
+
elem_plan = _plan_sa_type(
|
|
86
|
+
_nested_kind_from_py(py.array_item),
|
|
87
|
+
py.array_item,
|
|
88
|
+
prefer_dialect=prefer_dialect,
|
|
89
|
+
max_length=None,
|
|
90
|
+
decimal_precision=None,
|
|
91
|
+
decimal_scale=None,
|
|
92
|
+
)
|
|
93
|
+
if prefer_dialect == "postgresql":
|
|
94
|
+
return SATypePlan(
|
|
95
|
+
name="ARRAY",
|
|
96
|
+
args=(elem_plan.name,),
|
|
97
|
+
kwargs={},
|
|
98
|
+
dialect="postgresql",
|
|
99
|
+
)
|
|
100
|
+
return SATypePlan(name="JSON", args=(), kwargs={}, dialect=None)
|
|
101
|
+
|
|
102
|
+
raise InferenceError(f"Cannot plan SA type for kind={kind!r}")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _nested_kind_from_py(nested_py: PyTypeInfo) -> DataKind:
|
|
106
|
+
if nested_py.enum_cls is not None:
|
|
107
|
+
return DataKind.ENUM
|
|
108
|
+
b = nested_py.base
|
|
109
|
+
import datetime as _dt
|
|
110
|
+
import decimal as _dc
|
|
111
|
+
import uuid as _uuid
|
|
112
|
+
|
|
113
|
+
if b is str:
|
|
114
|
+
return DataKind.STRING
|
|
115
|
+
if b in (bytes, bytearray, memoryview):
|
|
116
|
+
return DataKind.BYTES
|
|
117
|
+
if b is bool:
|
|
118
|
+
return DataKind.BOOL
|
|
119
|
+
if b is int:
|
|
120
|
+
return DataKind.INT
|
|
121
|
+
if b is float:
|
|
122
|
+
return DataKind.FLOAT
|
|
123
|
+
if b is _dc.Decimal:
|
|
124
|
+
return DataKind.DECIMAL
|
|
125
|
+
if b is _dt.datetime:
|
|
126
|
+
return DataKind.DATETIME
|
|
127
|
+
if b is _dt.date:
|
|
128
|
+
return DataKind.DATE
|
|
129
|
+
if b is _dt.time:
|
|
130
|
+
return DataKind.TIME
|
|
131
|
+
if b is _uuid.UUID:
|
|
132
|
+
return DataKind.UUID
|
|
133
|
+
return DataKind.JSON
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from typing import Any, Dict, List, Optional, Tuple, Type
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# ---------------------------------------------------------------------------
|
|
9
|
+
# Public markers (wire/adapters provide validation/normalization at runtime)
|
|
10
|
+
# ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Email:
|
|
14
|
+
"""Marker indicating email string semantics."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Phone:
|
|
18
|
+
"""Marker indicating E.164 phone string semantics."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
# Portable data-kind (DB- and adapter-agnostic)
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class DataKind(str, Enum):
|
|
27
|
+
STRING = "string"
|
|
28
|
+
TEXT = "text"
|
|
29
|
+
BYTES = "bytes"
|
|
30
|
+
BOOL = "bool"
|
|
31
|
+
INT = "int"
|
|
32
|
+
BIGINT = "bigint"
|
|
33
|
+
FLOAT = "float"
|
|
34
|
+
DECIMAL = "decimal"
|
|
35
|
+
DATE = "date"
|
|
36
|
+
TIME = "time"
|
|
37
|
+
DATETIME = "datetime"
|
|
38
|
+
UUID = "uuid"
|
|
39
|
+
JSON = "json"
|
|
40
|
+
ENUM = "enum"
|
|
41
|
+
ARRAY = "array"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# ---------------------------------------------------------------------------
|
|
45
|
+
# Structured outputs
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True)
|
|
50
|
+
class PyTypeInfo:
|
|
51
|
+
"""Normalized info about the Python-side type annotation."""
|
|
52
|
+
|
|
53
|
+
base: Any
|
|
54
|
+
is_optional: bool = False
|
|
55
|
+
enum_cls: Optional[Type[Enum]] = None
|
|
56
|
+
array_item: Optional["PyTypeInfo"] = None
|
|
57
|
+
annotated: Tuple[Any, ...] = ()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass(frozen=True)
|
|
61
|
+
class SATypePlan:
|
|
62
|
+
"""Declarative plan for constructing an SA column type downstream."""
|
|
63
|
+
|
|
64
|
+
name: str # e.g., "UUID", "String", "JSONB", "Enum", "ARRAY"
|
|
65
|
+
args: Tuple[Any, ...] # positional args (e.g., (enum_cls,))
|
|
66
|
+
kwargs: Dict[str, Any] # keyword args (e.g., {"as_uuid": True})
|
|
67
|
+
dialect: Optional[str] = None # e.g., "postgresql" for JSONB/UUID/ARRAY
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass(frozen=True)
|
|
71
|
+
class JsonHint:
|
|
72
|
+
"""Minimal JSON Schema-ish hints for docs."""
|
|
73
|
+
|
|
74
|
+
type: str
|
|
75
|
+
format: Optional[str] = None
|
|
76
|
+
maxLength: Optional[int] = None
|
|
77
|
+
enum: Optional[List[str]] = None
|
|
78
|
+
items: Optional["JsonHint"] = None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass(frozen=True)
|
|
82
|
+
class Inferred:
|
|
83
|
+
"""Primary product of inference."""
|
|
84
|
+
|
|
85
|
+
kind: DataKind
|
|
86
|
+
py: PyTypeInfo
|
|
87
|
+
sa: SATypePlan
|
|
88
|
+
json: JsonHint
|
|
89
|
+
nullable: bool
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# ---------------------------------------------------------------------------
|
|
93
|
+
# Errors
|
|
94
|
+
# ---------------------------------------------------------------------------
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class InferenceError(ValueError):
|
|
98
|
+
"""Base class for inference-related errors."""
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class UnsupportedType(InferenceError):
|
|
102
|
+
"""Raised when a type cannot be inferred."""
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import (
|
|
5
|
+
Any,
|
|
6
|
+
List,
|
|
7
|
+
Optional,
|
|
8
|
+
Tuple,
|
|
9
|
+
Type,
|
|
10
|
+
Union,
|
|
11
|
+
get_args,
|
|
12
|
+
get_origin,
|
|
13
|
+
Annotated,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _strip_optional(tp: Any) -> Tuple[Any, bool]:
|
|
18
|
+
"""Return (inner_type, is_optional) for Optional[T] / Union[T, None]."""
|
|
19
|
+
origin = get_origin(tp)
|
|
20
|
+
if origin is Union:
|
|
21
|
+
args = tuple(a for a in get_args(tp))
|
|
22
|
+
if len(args) == 2 and type(None) in args:
|
|
23
|
+
inner = args[0] if args[1] is type(None) else args[1]
|
|
24
|
+
return inner, True
|
|
25
|
+
return tp, False
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _strip_annotated(tp: Any) -> Tuple[Any, Tuple[Any, ...]]:
|
|
29
|
+
"""Return (base, metadata) for Annotated[base, *meta]; otherwise (tp, ())."""
|
|
30
|
+
origin = get_origin(tp)
|
|
31
|
+
if origin is Annotated:
|
|
32
|
+
args = get_args(tp)
|
|
33
|
+
if len(args) >= 1:
|
|
34
|
+
base, meta = args[0], tuple(args[1:])
|
|
35
|
+
return base, meta
|
|
36
|
+
return tp, ()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _array_item(tp: Any) -> Optional[Any]:
|
|
40
|
+
origin = get_origin(tp)
|
|
41
|
+
if origin in (list, List, tuple, Tuple, set, frozenset):
|
|
42
|
+
args = get_args(tp)
|
|
43
|
+
if not args:
|
|
44
|
+
return Any
|
|
45
|
+
if origin in (tuple, Tuple) and len(args) == 2 and args[1] is Ellipsis:
|
|
46
|
+
return args[0]
|
|
47
|
+
if len(args) == 1:
|
|
48
|
+
return args[0]
|
|
49
|
+
return Any
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _is_enum(tp: Any) -> Optional[Type[Enum]]:
|
|
54
|
+
try:
|
|
55
|
+
if isinstance(tp, type) and issubclass(tp, Enum):
|
|
56
|
+
return tp
|
|
57
|
+
except Exception:
|
|
58
|
+
pass
|
|
59
|
+
return None
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from importlib import import_module
|
|
4
|
+
import warnings
|
|
5
|
+
|
|
6
|
+
from tigrbl_canon import _DEPRECATION_MESSAGE
|
|
7
|
+
|
|
8
|
+
warnings.warn(_DEPRECATION_MESSAGE, DeprecationWarning, stacklevel=2)
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"bind",
|
|
12
|
+
"rebind",
|
|
13
|
+
"build_schemas",
|
|
14
|
+
"build_hooks",
|
|
15
|
+
"build_handlers",
|
|
16
|
+
"register_rpc",
|
|
17
|
+
"build_rest",
|
|
18
|
+
"bind_response",
|
|
19
|
+
"include_table",
|
|
20
|
+
"include_tables",
|
|
21
|
+
"rpc_call",
|
|
22
|
+
"MRO_COLLECTORS",
|
|
23
|
+
"RESOLVERS",
|
|
24
|
+
"INSTALLS",
|
|
25
|
+
"collect",
|
|
26
|
+
"install",
|
|
27
|
+
"collect_engine_bindings",
|
|
28
|
+
"install_engine_bindings",
|
|
29
|
+
"install_from_objects",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def __getattr__(name: str):
|
|
34
|
+
if name in {"bind", "rebind"}:
|
|
35
|
+
return getattr(import_module(".model", __name__), name)
|
|
36
|
+
if name == "build_schemas":
|
|
37
|
+
return import_module(".schemas", __name__).build_and_attach
|
|
38
|
+
if name == "build_hooks":
|
|
39
|
+
return import_module(".hooks", __name__).normalize_and_attach
|
|
40
|
+
if name == "build_handlers":
|
|
41
|
+
return import_module(".handlers", __name__).build_and_attach
|
|
42
|
+
if name == "register_rpc":
|
|
43
|
+
return import_module(".rpc", __name__).register_and_attach
|
|
44
|
+
if name == "build_rest":
|
|
45
|
+
return import_module(".rest", __name__).build_router_and_attach
|
|
46
|
+
if name == "bind_response":
|
|
47
|
+
return import_module(".responses_resolver", __name__).resolve_response_spec
|
|
48
|
+
if name in {"include_table", "include_tables"}:
|
|
49
|
+
return getattr(import_module(".router.include", __name__), name)
|
|
50
|
+
if name == "rpc_call":
|
|
51
|
+
# ``tigrbl.mapping.rpc_call`` should execute an RPC op end-to-end and
|
|
52
|
+
# return the serialized operation result.
|
|
53
|
+
return getattr(import_module(".router.rpc", __name__), name)
|
|
54
|
+
if name in {
|
|
55
|
+
"INSTALLS",
|
|
56
|
+
"MRO_COLLECTORS",
|
|
57
|
+
"RESOLVERS",
|
|
58
|
+
"collect",
|
|
59
|
+
"install",
|
|
60
|
+
"collect_engine_bindings",
|
|
61
|
+
"install_engine_bindings",
|
|
62
|
+
"install_from_objects",
|
|
63
|
+
}:
|
|
64
|
+
return getattr(import_module(".traversal", __name__), name)
|
|
65
|
+
raise AttributeError(name)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from functools import lru_cache
|
|
4
|
+
|
|
5
|
+
from tigrbl_core._spec.app_spec import AppSpec
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@lru_cache(maxsize=None)
|
|
9
|
+
def mro_collect_app_spec(app: type) -> AppSpec:
|
|
10
|
+
"""Collect AppSpec-like declarations across the app's MRO."""
|
|
11
|
+
return AppSpec.collect(app)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
__all__ = ["mro_collect_app_spec"]
|