tigrbl-core 0.1.0.dev1__tar.gz → 0.1.0.dev6__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_core-0.1.0.dev1 → tigrbl_core-0.1.0.dev6}/PKG-INFO +7 -3
- {tigrbl_core-0.1.0.dev1 → tigrbl_core-0.1.0.dev6}/README.md +2 -2
- {tigrbl_core-0.1.0.dev1 → tigrbl_core-0.1.0.dev6}/pyproject.toml +5 -1
- {tigrbl_core-0.1.0.dev1 → tigrbl_core-0.1.0.dev6}/tigrbl_core/_spec/__init__.py +31 -10
- {tigrbl_core-0.1.0.dev1 → tigrbl_core-0.1.0.dev6}/tigrbl_core/_spec/alias_spec.py +1 -1
- {tigrbl_core-0.1.0.dev1 → tigrbl_core-0.1.0.dev6}/tigrbl_core/_spec/app_spec.py +83 -4
- {tigrbl_core-0.1.0.dev1 → tigrbl_core-0.1.0.dev6}/tigrbl_core/_spec/binding_spec.py +19 -19
- tigrbl_core-0.1.0.dev6/tigrbl_core/_spec/column_spec.py +183 -0
- {tigrbl_core-0.1.0.dev1 → tigrbl_core-0.1.0.dev6}/tigrbl_core/_spec/engine_spec.py +78 -53
- {tigrbl_core-0.1.0.dev1 → tigrbl_core-0.1.0.dev6}/tigrbl_core/_spec/field_spec.py +19 -2
- tigrbl_core-0.1.0.dev6/tigrbl_core/_spec/hook_spec.py +42 -0
- tigrbl_core-0.1.0.dev6/tigrbl_core/_spec/hook_types.py +34 -0
- {tigrbl_core-0.1.0.dev1 → tigrbl_core-0.1.0.dev6}/tigrbl_core/_spec/io_spec.py +3 -2
- tigrbl_core-0.1.0.dev6/tigrbl_core/_spec/op_spec.py +452 -0
- tigrbl_core-0.1.0.dev6/tigrbl_core/_spec/op_utils.py +43 -0
- tigrbl_core-0.1.0.dev6/tigrbl_core/_spec/plugins.py +44 -0
- tigrbl_core-0.1.0.dev6/tigrbl_core/_spec/registry.py +38 -0
- {tigrbl_core-0.1.0.dev1 → tigrbl_core-0.1.0.dev6}/tigrbl_core/_spec/request_spec.py +3 -1
- tigrbl_core-0.1.0.dev6/tigrbl_core/_spec/response_resolver.py +63 -0
- {tigrbl_core-0.1.0.dev1 → tigrbl_core-0.1.0.dev6}/tigrbl_core/_spec/response_spec.py +4 -2
- tigrbl_core-0.1.0.dev6/tigrbl_core/_spec/router_spec.py +102 -0
- tigrbl_core-0.1.0.dev6/tigrbl_core/_spec/schema_spec.py +47 -0
- tigrbl_core-0.1.0.dev6/tigrbl_core/_spec/serde.py +164 -0
- {tigrbl_core-0.1.0.dev1 → tigrbl_core-0.1.0.dev6}/tigrbl_core/_spec/session_spec.py +3 -15
- {tigrbl_core-0.1.0.dev1 → tigrbl_core-0.1.0.dev6}/tigrbl_core/_spec/storage_spec.py +11 -4
- {tigrbl_core-0.1.0.dev1 → tigrbl_core-0.1.0.dev6}/tigrbl_core/_spec/table_registry_spec.py +3 -1
- {tigrbl_core-0.1.0.dev1 → tigrbl_core-0.1.0.dev6}/tigrbl_core/_spec/table_spec.py +53 -8
- tigrbl_core-0.1.0.dev6/tigrbl_core/config/__init__.py +6 -0
- tigrbl_core-0.1.0.dev6/tigrbl_core/config/constants.py +224 -0
- tigrbl_core-0.1.0.dev6/tigrbl_core/config/defaults.py +32 -0
- tigrbl_core-0.1.0.dev6/tigrbl_core/config/engine_traversal.py +102 -0
- tigrbl_core-0.1.0.dev6/tigrbl_core/config/resolver.py +276 -0
- tigrbl_core-0.1.0.dev6/tigrbl_core/core/__init__.py +7 -0
- tigrbl_core-0.1.0.dev6/tigrbl_core/op/__init__.py +17 -0
- tigrbl_core-0.1.0.dev6/tigrbl_core/op/canonical.py +39 -0
- tigrbl_core-0.1.0.dev6/tigrbl_core/op/collect.py +20 -0
- tigrbl_core-0.1.0.dev6/tigrbl_core/op/types.py +29 -0
- tigrbl_core-0.1.0.dev6/tigrbl_core/schema/__init__.py +28 -0
- tigrbl_core-0.1.0.dev6/tigrbl_core/schema/builder/__init__.py +17 -0
- tigrbl_core-0.1.0.dev6/tigrbl_core/schema/builder/build_schema.py +307 -0
- tigrbl_core-0.1.0.dev6/tigrbl_core/schema/builder/cache.py +24 -0
- tigrbl_core-0.1.0.dev6/tigrbl_core/schema/builder/extras.py +85 -0
- tigrbl_core-0.1.0.dev6/tigrbl_core/schema/builder/helpers.py +87 -0
- tigrbl_core-0.1.0.dev6/tigrbl_core/schema/builder/list_params.py +117 -0
- tigrbl_core-0.1.0.dev6/tigrbl_core/schema/builder/strip_parent_fields.py +70 -0
- tigrbl_core-0.1.0.dev6/tigrbl_core/schema/get_schema.py +85 -0
- tigrbl_core-0.1.0.dev6/tigrbl_core/schema/utils.py +142 -0
- tigrbl_core-0.1.0.dev1/tigrbl_core/_spec/column_spec.py +0 -40
- tigrbl_core-0.1.0.dev1/tigrbl_core/_spec/hook_spec.py +0 -24
- tigrbl_core-0.1.0.dev1/tigrbl_core/_spec/op_spec.py +0 -110
- tigrbl_core-0.1.0.dev1/tigrbl_core/_spec/router_spec.py +0 -25
- tigrbl_core-0.1.0.dev1/tigrbl_core/_spec/schema_spec.py +0 -20
- tigrbl_core-0.1.0.dev1/tigrbl_core/_spec/shortcuts_spec.py +0 -8
- tigrbl_core-0.1.0.dev1/tigrbl_core/core/__init__.py +0 -47
- tigrbl_core-0.1.0.dev1/tigrbl_core/core/crud/__init__.py +0 -43
- tigrbl_core-0.1.0.dev1/tigrbl_core/core/crud/bulk.py +0 -167
- tigrbl_core-0.1.0.dev1/tigrbl_core/core/crud/helpers/__init__.py +0 -76
- tigrbl_core-0.1.0.dev1/tigrbl_core/core/crud/helpers/db.py +0 -91
- tigrbl_core-0.1.0.dev1/tigrbl_core/core/crud/helpers/enum.py +0 -85
- tigrbl_core-0.1.0.dev1/tigrbl_core/core/crud/helpers/filters.py +0 -161
- tigrbl_core-0.1.0.dev1/tigrbl_core/core/crud/helpers/model.py +0 -136
- tigrbl_core-0.1.0.dev1/tigrbl_core/core/crud/helpers/normalize.py +0 -98
- tigrbl_core-0.1.0.dev1/tigrbl_core/core/crud/ops.py +0 -298
- tigrbl_core-0.1.0.dev1/tigrbl_core/core/crud/params.py +0 -50
- {tigrbl_core-0.1.0.dev1 → tigrbl_core-0.1.0.dev6}/tigrbl_core/_spec/middleware_spec.py +0 -0
- {tigrbl_core-0.1.0.dev1 → tigrbl_core-0.1.0.dev6}/tigrbl_core/_spec/response_types.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tigrbl-core
|
|
3
|
-
Version: 0.1.0.
|
|
3
|
+
Version: 0.1.0.dev6
|
|
4
4
|
Summary: Core specifications and primitives for the Tigrbl framework.
|
|
5
5
|
License-Expression: Apache-2.0
|
|
6
6
|
Keywords: tigrbl,sdk,standards,framework
|
|
@@ -15,7 +15,11 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
15
15
|
Classifier: Programming Language :: Python
|
|
16
16
|
Classifier: Programming Language :: Python :: 3
|
|
17
17
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
+
Requires-Dist: pydantic (>=2.10,<3)
|
|
19
|
+
Requires-Dist: pyyaml
|
|
18
20
|
Requires-Dist: tigrbl-typing
|
|
21
|
+
Requires-Dist: tomli (>=2.0.1) ; python_version < "3.11"
|
|
22
|
+
Requires-Dist: tomli-w
|
|
19
23
|
Description-Content-Type: text/markdown
|
|
20
24
|
|
|
21
25
|

|
|
@@ -26,9 +30,9 @@ Description-Content-Type: text/markdown
|
|
|
26
30
|
|
|
27
31
|
## Features
|
|
28
32
|
|
|
29
|
-
-
|
|
33
|
+
- Core specification and primitive contracts for Tigrbl.
|
|
34
|
+
- Operational implementations now live in `tigrbl-ops-oltp`.
|
|
30
35
|
- Supports Python 3.10 through 3.12.
|
|
31
|
-
- Distributed as part of the swarmauri-sdk workspace.
|
|
32
36
|
|
|
33
37
|
## Installation
|
|
34
38
|
|
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
9
|
-
-
|
|
9
|
+
- Core specification and primitive contracts for Tigrbl.
|
|
10
|
+
- Operational implementations now live in `tigrbl-ops-oltp`.
|
|
10
11
|
- Supports Python 3.10 through 3.12.
|
|
11
|
-
- Distributed as part of the swarmauri-sdk workspace.
|
|
12
12
|
|
|
13
13
|
## Installation
|
|
14
14
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "tigrbl-core"
|
|
3
|
-
version = "0.1.0.
|
|
3
|
+
version = "0.1.0.dev6"
|
|
4
4
|
description = "Core specifications and primitives for the Tigrbl framework."
|
|
5
5
|
license = "Apache-2.0"
|
|
6
6
|
readme = "README.md"
|
|
@@ -19,6 +19,10 @@ classifiers = [
|
|
|
19
19
|
authors = [{ name = "Jacob Stewart", email = "jacob@swarmauri.com" }]
|
|
20
20
|
dependencies = [
|
|
21
21
|
"tigrbl-typing",
|
|
22
|
+
"pydantic>=2.10,<3",
|
|
23
|
+
"pyyaml",
|
|
24
|
+
"tomli-w",
|
|
25
|
+
"tomli>=2.0.1; python_version < '3.11'",
|
|
22
26
|
]
|
|
23
27
|
keywords = ["tigrbl", "sdk", "standards", "framework"]
|
|
24
28
|
|
|
@@ -11,53 +11,74 @@ from typing import Any
|
|
|
11
11
|
_EXPORTS = {
|
|
12
12
|
"AliasSpec": "alias_spec",
|
|
13
13
|
"AppSpec": "app_spec",
|
|
14
|
-
"Binding": "binding_spec",
|
|
15
|
-
"BindingRegistry": "binding_spec",
|
|
16
14
|
"BindingSpec": "binding_spec",
|
|
15
|
+
"BindingRegistrySpec": "binding_spec",
|
|
16
|
+
"TransportBindingSpec": "binding_spec",
|
|
17
17
|
"HttpRestBindingSpec": "binding_spec",
|
|
18
18
|
"HttpJsonRpcBindingSpec": "binding_spec",
|
|
19
19
|
"WsBindingSpec": "binding_spec",
|
|
20
20
|
"resolve_rest_nested_prefix": "binding_spec",
|
|
21
21
|
"ColumnSpec": "column_spec",
|
|
22
22
|
"EngineSpec": "engine_spec",
|
|
23
|
+
"EngineProviderSpec": "engine_spec",
|
|
23
24
|
"FieldSpec": "field_spec",
|
|
25
|
+
"F": "field_spec",
|
|
24
26
|
"HookSpec": "hook_spec",
|
|
25
27
|
"IOSpec": "io_spec",
|
|
28
|
+
"IO": "io_spec",
|
|
26
29
|
"MiddlewareSpec": "middleware_spec",
|
|
27
30
|
"OpSpec": "op_spec",
|
|
31
|
+
"Arity": "op_spec",
|
|
32
|
+
"TargetOp": "op_spec",
|
|
33
|
+
"PersistPolicy": "op_spec",
|
|
28
34
|
"PHASE": "op_spec",
|
|
29
35
|
"PHASES": "op_spec",
|
|
36
|
+
"HookPhase": "hook_spec",
|
|
30
37
|
"TemplateSpec": "response_spec",
|
|
31
38
|
"ResponseSpec": "response_spec",
|
|
39
|
+
"resolve_response_spec": "response_resolver",
|
|
32
40
|
"RequestSpec": "request_spec",
|
|
33
41
|
"RouterSpec": "router_spec",
|
|
34
42
|
"SchemaSpec": "schema_spec",
|
|
35
43
|
"SchemaRef": "schema_spec",
|
|
44
|
+
"SchemaArg": "schema_spec",
|
|
36
45
|
"SessionSpec": "session_spec",
|
|
37
46
|
"session_spec": "session_spec",
|
|
38
47
|
"tx_read_committed": "session_spec",
|
|
39
48
|
"tx_repeatable_read": "session_spec",
|
|
40
49
|
"tx_serializable": "session_spec",
|
|
41
50
|
"readonly": "session_spec",
|
|
42
|
-
"wrap_sessionmaker": "session_spec",
|
|
43
51
|
"StorageSpec": "storage_spec",
|
|
52
|
+
"S": "storage_spec",
|
|
53
|
+
"StorageTransformSpec": "storage_spec",
|
|
44
54
|
"StorageTransform": "storage_spec",
|
|
45
55
|
"ForeignKeySpec": "storage_spec",
|
|
46
56
|
"TableSpec": "table_spec",
|
|
47
57
|
"TableRegistrySpec": "table_registry_spec",
|
|
48
|
-
"F": "shortcuts_spec",
|
|
49
|
-
"IO": "shortcuts_spec",
|
|
50
|
-
"S": "shortcuts_spec",
|
|
51
|
-
"makeColumn": "shortcuts_spec",
|
|
52
|
-
"makeVirtualColumn": "shortcuts_spec",
|
|
53
|
-
"acol": "shortcuts_spec",
|
|
54
|
-
"vcol": "shortcuts_spec",
|
|
55
58
|
}
|
|
56
59
|
|
|
57
60
|
__all__ = list(_EXPORTS)
|
|
58
61
|
|
|
59
62
|
|
|
60
63
|
def __getattr__(name: str) -> Any:
|
|
64
|
+
if name in {"PHASE", "PHASES"}:
|
|
65
|
+
from .hook_types import HookPhase, HookPhases
|
|
66
|
+
|
|
67
|
+
value = HookPhase if name == "PHASE" else HookPhases
|
|
68
|
+
globals()[name] = value
|
|
69
|
+
return value
|
|
70
|
+
if name in {"F", "IO", "S"}:
|
|
71
|
+
aliases = {
|
|
72
|
+
"F": ("field_spec", "FieldSpec"),
|
|
73
|
+
"IO": ("io_spec", "IOSpec"),
|
|
74
|
+
"S": ("storage_spec", "StorageSpec"),
|
|
75
|
+
}
|
|
76
|
+
module_name, attr = aliases[name]
|
|
77
|
+
module = import_module(f"{__name__}.{module_name}")
|
|
78
|
+
value = getattr(module, attr)
|
|
79
|
+
globals()[name] = value
|
|
80
|
+
return value
|
|
81
|
+
|
|
61
82
|
module_name = _EXPORTS.get(name)
|
|
62
83
|
if module_name is None:
|
|
63
84
|
raise AttributeError(name)
|
|
@@ -1,14 +1,91 @@
|
|
|
1
1
|
# pkgs/standards/tigrbl_core/tigrbl/_spec/app_spec.py
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
from dataclasses import dataclass, field
|
|
4
|
-
from typing import Any, Callable, Optional, Sequence
|
|
4
|
+
from typing import Any, Callable, Iterable, Mapping, Optional, Sequence
|
|
5
5
|
|
|
6
6
|
from .._spec.engine_spec import EngineCfg
|
|
7
7
|
from .._spec.response_spec import ResponseSpec
|
|
8
|
+
from .serde import SerdeMixin
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _seqify(value: Any) -> tuple[Any, ...]:
|
|
12
|
+
"""Normalize sequence-like inputs while treating scalars as a single item."""
|
|
13
|
+
|
|
14
|
+
if value is None:
|
|
15
|
+
return ()
|
|
16
|
+
if isinstance(value, tuple):
|
|
17
|
+
return value
|
|
18
|
+
if isinstance(value, (str, bytes, bytearray)):
|
|
19
|
+
return (value,)
|
|
20
|
+
if isinstance(value, Mapping):
|
|
21
|
+
return (value,)
|
|
22
|
+
if isinstance(value, Iterable):
|
|
23
|
+
return tuple(value)
|
|
24
|
+
return (value,)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def merge_seq_attr(
|
|
28
|
+
owner: type,
|
|
29
|
+
attr: str,
|
|
30
|
+
*,
|
|
31
|
+
include_inherited: bool = False,
|
|
32
|
+
reverse: bool = False,
|
|
33
|
+
dedupe: bool = True,
|
|
34
|
+
) -> tuple[Any, ...]:
|
|
35
|
+
"""Merge sequence-like class attributes over the MRO."""
|
|
36
|
+
|
|
37
|
+
values: list[Any] = []
|
|
38
|
+
seen_hashable: set[Any] = set()
|
|
39
|
+
mro = reversed(owner.__mro__) if reverse else owner.__mro__
|
|
40
|
+
for base in mro:
|
|
41
|
+
if include_inherited:
|
|
42
|
+
if not hasattr(base, attr):
|
|
43
|
+
continue
|
|
44
|
+
seq = getattr(base, attr) or ()
|
|
45
|
+
else:
|
|
46
|
+
seq = base.__dict__.get(attr, ()) or ()
|
|
47
|
+
for item in _seqify(seq):
|
|
48
|
+
if dedupe:
|
|
49
|
+
try:
|
|
50
|
+
if item in seen_hashable:
|
|
51
|
+
continue
|
|
52
|
+
seen_hashable.add(item)
|
|
53
|
+
except TypeError:
|
|
54
|
+
if any(item == existing for existing in values):
|
|
55
|
+
continue
|
|
56
|
+
values.append(item)
|
|
57
|
+
return tuple(values)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def normalize_app_spec(spec: "AppSpec") -> "AppSpec":
|
|
61
|
+
"""Return a normalized app spec snapshot with stable sequence fields."""
|
|
62
|
+
|
|
63
|
+
routers = _seqify(spec.routers)
|
|
64
|
+
tables = _seqify(spec.tables)
|
|
65
|
+
ops = _seqify(spec.ops)
|
|
66
|
+
|
|
67
|
+
return AppSpec(
|
|
68
|
+
title=str(spec.title or "Tigrbl"),
|
|
69
|
+
description=spec.description,
|
|
70
|
+
version=str(spec.version or "0.1.0"),
|
|
71
|
+
engine=spec.engine,
|
|
72
|
+
routers=routers,
|
|
73
|
+
ops=ops,
|
|
74
|
+
tables=tables,
|
|
75
|
+
schemas=_seqify(spec.schemas),
|
|
76
|
+
hooks=_seqify(spec.hooks),
|
|
77
|
+
security_deps=_seqify(spec.security_deps),
|
|
78
|
+
deps=_seqify(spec.deps),
|
|
79
|
+
middlewares=_seqify(spec.middlewares),
|
|
80
|
+
response=spec.response,
|
|
81
|
+
jsonrpc_prefix=str(spec.jsonrpc_prefix or "/rpc"),
|
|
82
|
+
system_prefix=str(spec.system_prefix or "/system"),
|
|
83
|
+
lifespan=spec.lifespan,
|
|
84
|
+
)
|
|
8
85
|
|
|
9
86
|
|
|
10
87
|
@dataclass(eq=False)
|
|
11
|
-
class AppSpec:
|
|
88
|
+
class AppSpec(SerdeMixin):
|
|
12
89
|
"""
|
|
13
90
|
Used to *produce an App subclass* via App.from_spec().
|
|
14
91
|
"""
|
|
@@ -43,9 +120,11 @@ class AppSpec:
|
|
|
43
120
|
lifespan: Optional[Callable[..., Any]] = None
|
|
44
121
|
|
|
45
122
|
@classmethod
|
|
46
|
-
def
|
|
47
|
-
|
|
123
|
+
def from_dict(cls, payload: dict[str, Any]) -> "AppSpec":
|
|
124
|
+
return super().from_dict(payload)
|
|
48
125
|
|
|
126
|
+
@classmethod
|
|
127
|
+
def collect(cls, app: type) -> "AppSpec":
|
|
49
128
|
sentinel = object()
|
|
50
129
|
title: Any = sentinel
|
|
51
130
|
version: Any = sentinel
|
|
@@ -1,60 +1,60 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from dataclasses import dataclass
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
4
|
from typing import Literal, Optional, Type, Union
|
|
5
5
|
|
|
6
6
|
from ..config.constants import TIGRBL_NESTED_PATHS_ATTR
|
|
7
|
+
from .serde import SerdeMixin
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
@dataclass(frozen=True, slots=True)
|
|
10
|
-
class HttpRestBindingSpec:
|
|
11
|
+
class HttpRestBindingSpec(SerdeMixin):
|
|
11
12
|
proto: Literal["http.rest", "https.rest"]
|
|
12
13
|
methods: tuple[str, ...]
|
|
13
14
|
path: str
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
@dataclass(frozen=True, slots=True)
|
|
17
|
-
class HttpJsonRpcBindingSpec:
|
|
18
|
+
class HttpJsonRpcBindingSpec(SerdeMixin):
|
|
18
19
|
proto: Literal["http.jsonrpc", "https.jsonrpc"]
|
|
19
20
|
rpc_method: str
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
@dataclass(frozen=True, slots=True)
|
|
23
|
-
class WsBindingSpec:
|
|
24
|
+
class WsBindingSpec(SerdeMixin):
|
|
24
25
|
proto: Literal["ws", "wss"]
|
|
25
26
|
path: str
|
|
26
27
|
subprotocols: tuple[str, ...] = ()
|
|
27
28
|
|
|
28
29
|
|
|
29
|
-
|
|
30
|
+
TransportBindingSpec = Union[
|
|
31
|
+
HttpRestBindingSpec,
|
|
32
|
+
HttpJsonRpcBindingSpec,
|
|
33
|
+
WsBindingSpec,
|
|
34
|
+
]
|
|
30
35
|
|
|
31
36
|
|
|
32
37
|
@dataclass(frozen=True, slots=True)
|
|
33
|
-
class
|
|
38
|
+
class BindingSpec(SerdeMixin):
|
|
34
39
|
"""Named binding declaration used for registry composition."""
|
|
35
40
|
|
|
36
|
-
"""Named binding wrapper used by registries and planners."""
|
|
37
|
-
|
|
38
41
|
name: str
|
|
39
|
-
spec:
|
|
42
|
+
spec: TransportBindingSpec
|
|
40
43
|
|
|
41
44
|
|
|
42
45
|
@dataclass(slots=True)
|
|
43
|
-
class
|
|
46
|
+
class BindingRegistrySpec(SerdeMixin):
|
|
44
47
|
"""Simple in-memory registry for named transport bindings."""
|
|
45
48
|
|
|
46
|
-
_bindings: dict[str,
|
|
47
|
-
|
|
48
|
-
def __init__(self) -> None:
|
|
49
|
-
self._bindings = {}
|
|
49
|
+
_bindings: dict[str, BindingSpec] = field(default_factory=dict)
|
|
50
50
|
|
|
51
|
-
def register(self, binding:
|
|
51
|
+
def register(self, binding: BindingSpec) -> None:
|
|
52
52
|
self._bindings[binding.name] = binding
|
|
53
53
|
|
|
54
|
-
def get(self, name: str) -> Optional[
|
|
54
|
+
def get(self, name: str) -> Optional[BindingSpec]:
|
|
55
55
|
return self._bindings.get(name)
|
|
56
56
|
|
|
57
|
-
def values(self) -> tuple[
|
|
57
|
+
def values(self) -> tuple[BindingSpec, ...]:
|
|
58
58
|
return tuple(self._bindings.values())
|
|
59
59
|
|
|
60
60
|
|
|
@@ -68,11 +68,11 @@ def resolve_rest_nested_prefix(model: Type) -> Optional[str]:
|
|
|
68
68
|
|
|
69
69
|
|
|
70
70
|
__all__ = [
|
|
71
|
-
"Binding",
|
|
72
|
-
"BindingRegistry",
|
|
73
71
|
"BindingSpec",
|
|
72
|
+
"BindingRegistrySpec",
|
|
74
73
|
"HttpJsonRpcBindingSpec",
|
|
75
74
|
"HttpRestBindingSpec",
|
|
75
|
+
"TransportBindingSpec",
|
|
76
76
|
"WsBindingSpec",
|
|
77
77
|
"resolve_rest_nested_prefix",
|
|
78
78
|
]
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from functools import lru_cache
|
|
5
|
+
from types import SimpleNamespace
|
|
6
|
+
from typing import Any, Callable, Dict, Optional
|
|
7
|
+
|
|
8
|
+
from .._spec.field_spec import FieldSpec as F
|
|
9
|
+
from .._spec.io_spec import IOSpec as IO
|
|
10
|
+
from .._spec.storage_spec import StorageSpec as S
|
|
11
|
+
from .serde import SerdeMixin
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger("uvicorn")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ColumnSpec(SerdeMixin):
|
|
17
|
+
"""Aggregate configuration for a model attribute.
|
|
18
|
+
|
|
19
|
+
A :class:`ColumnSpec` brings together the three lower-level specs used by
|
|
20
|
+
Tigrbl's declarative column system:
|
|
21
|
+
|
|
22
|
+
- ``storage`` (:class:`~tigrbl._spec.storage_spec.StorageSpec`) controls
|
|
23
|
+
how the value is persisted in the database.
|
|
24
|
+
- ``field`` (:class:`~tigrbl._spec.field_spec.FieldSpec`) describes the
|
|
25
|
+
Python type and any schema metadata.
|
|
26
|
+
- ``io`` (:class:`~tigrbl._spec.io_spec.IOSpec`) governs inbound and
|
|
27
|
+
outbound API exposure.
|
|
28
|
+
|
|
29
|
+
Optional ``default_factory`` and ``read_producer`` callables allow for
|
|
30
|
+
programmatic defaults and virtual read-time values respectively.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
*,
|
|
36
|
+
storage: S | None,
|
|
37
|
+
field: F | None = None,
|
|
38
|
+
io: IO | None = None,
|
|
39
|
+
default_factory: Optional[Callable[[dict], Any]] = None,
|
|
40
|
+
read_producer: Optional[Callable[[object, dict], Any]] = None,
|
|
41
|
+
) -> None:
|
|
42
|
+
self.storage = storage
|
|
43
|
+
self.field = field if field is not None else F()
|
|
44
|
+
self.io = io if io is not None else IO()
|
|
45
|
+
self.default_factory = default_factory
|
|
46
|
+
self.read_producer = read_producer
|
|
47
|
+
|
|
48
|
+
# Default inbound/outbound verbs for columns lacking an explicit ColumnSpec.
|
|
49
|
+
#
|
|
50
|
+
# Without this, plain SQLAlchemy ``Column`` definitions are omitted from the
|
|
51
|
+
# collected spec map, causing downstream components to treat their values as
|
|
52
|
+
# unknown. By seeding such columns with a permissive IO spec we ensure they
|
|
53
|
+
# participate in canonical CRUD operations just like columns defined via
|
|
54
|
+
# ``acol``.
|
|
55
|
+
_DEFAULT_IO = IO(
|
|
56
|
+
in_verbs=("create", "update", "replace"),
|
|
57
|
+
out_verbs=("read", "list"),
|
|
58
|
+
mutable_verbs=("create", "update", "replace"),
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
@staticmethod
|
|
62
|
+
def _model_label(model: object) -> str:
|
|
63
|
+
return str(
|
|
64
|
+
getattr(model, "__name__", None)
|
|
65
|
+
or getattr(model, "name", None)
|
|
66
|
+
or type(model).__name__
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
@staticmethod
|
|
70
|
+
def _coerce_columns_iterable(columns: object) -> tuple[object, ...]:
|
|
71
|
+
"""Normalize model/table column containers to an iterable tuple.
|
|
72
|
+
|
|
73
|
+
Some table classes expose ``columns`` as a ``SimpleNamespace`` of
|
|
74
|
+
``ColumnSpec`` objects for convenience. Runtime collectors should treat
|
|
75
|
+
that namespace as a mapping and iterate over its values rather than
|
|
76
|
+
trying to iterate the namespace object directly.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
if isinstance(columns, SimpleNamespace):
|
|
80
|
+
return tuple(columns.__dict__.values())
|
|
81
|
+
if isinstance(columns, dict):
|
|
82
|
+
return tuple(columns.values())
|
|
83
|
+
try:
|
|
84
|
+
return tuple(columns) # type: ignore[arg-type]
|
|
85
|
+
except TypeError:
|
|
86
|
+
return ()
|
|
87
|
+
|
|
88
|
+
@staticmethod
|
|
89
|
+
@lru_cache(maxsize=None)
|
|
90
|
+
def _mro_collect_columns_cached(
|
|
91
|
+
model: object, _cache_bust: int
|
|
92
|
+
) -> Dict[str, "ColumnSpec"]:
|
|
93
|
+
"""Collect ColumnSpecs declared on *model* and all mixins.
|
|
94
|
+
|
|
95
|
+
Iterates across the model's MRO so that mixin-defined columns are
|
|
96
|
+
included in the resulting mapping. Later definitions take precedence
|
|
97
|
+
over earlier ones in the MRO. Any table-backed columns lacking a spec
|
|
98
|
+
are populated with a default ColumnSpec so they participate in opviews
|
|
99
|
+
and schema generation.
|
|
100
|
+
"""
|
|
101
|
+
logger.debug("Collecting columns for %s", ColumnSpec._model_label(model))
|
|
102
|
+
out: Dict[str, ColumnSpec] = {}
|
|
103
|
+
mro = getattr(model, "__mro__", ()) or ()
|
|
104
|
+
for base in reversed(mro):
|
|
105
|
+
mapping = getattr(base, "__tigrbl_colspecs__", None)
|
|
106
|
+
if isinstance(mapping, dict):
|
|
107
|
+
out.update(mapping)
|
|
108
|
+
mapping = getattr(base, "__tigrbl_cols__", None)
|
|
109
|
+
if isinstance(mapping, dict):
|
|
110
|
+
out.update(mapping)
|
|
111
|
+
|
|
112
|
+
cols = None
|
|
113
|
+
table = getattr(model, "__table__", None)
|
|
114
|
+
if table is not None:
|
|
115
|
+
cols = getattr(table, "columns", None)
|
|
116
|
+
elif hasattr(model, "columns"):
|
|
117
|
+
cols = getattr(model, "columns", None)
|
|
118
|
+
|
|
119
|
+
if cols is not None:
|
|
120
|
+
for col in ColumnSpec._coerce_columns_iterable(cols):
|
|
121
|
+
name = getattr(col, "key", None) or getattr(col, "name", None)
|
|
122
|
+
if not isinstance(name, str):
|
|
123
|
+
continue
|
|
124
|
+
out.setdefault(name, ColumnSpec(storage=S(), io=ColumnSpec._DEFAULT_IO))
|
|
125
|
+
else:
|
|
126
|
+
# Declarative models can be inspected before SQLAlchemy finishes
|
|
127
|
+
# materializing ``__table__``. In that transient state plain
|
|
128
|
+
# ``Column(...)`` declarations still exist on the class body and
|
|
129
|
+
# should participate in schema inference.
|
|
130
|
+
for base in reversed(mro):
|
|
131
|
+
for attr_name, value in vars(base).items():
|
|
132
|
+
if attr_name.startswith("_") or attr_name in out:
|
|
133
|
+
continue
|
|
134
|
+
if hasattr(value, "type") and hasattr(value, "nullable"):
|
|
135
|
+
out[attr_name] = ColumnSpec(
|
|
136
|
+
storage=S(), io=ColumnSpec._DEFAULT_IO
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
logger.debug(
|
|
140
|
+
"Collected %d columns for %s", len(out), ColumnSpec._model_label(model)
|
|
141
|
+
)
|
|
142
|
+
return out
|
|
143
|
+
|
|
144
|
+
@classmethod
|
|
145
|
+
def collect(
|
|
146
|
+
cls, model: object, *, _cache_bust: int | None = None
|
|
147
|
+
) -> Dict[str, "ColumnSpec"]:
|
|
148
|
+
"""Collect ColumnSpecs for *model* with topology-aware cache busting."""
|
|
149
|
+
|
|
150
|
+
if _cache_bust is None:
|
|
151
|
+
table = getattr(model, "__table__", None)
|
|
152
|
+
cols = getattr(table, "columns", None) if table is not None else None
|
|
153
|
+
col_count = (
|
|
154
|
+
len(cls._coerce_columns_iterable(cols)) if cols is not None else 0
|
|
155
|
+
)
|
|
156
|
+
_cache_bust = hash(
|
|
157
|
+
(
|
|
158
|
+
id(getattr(model, "__tigrbl_colspecs__", None)),
|
|
159
|
+
id(getattr(model, "__tigrbl_cols__", None)),
|
|
160
|
+
id(table),
|
|
161
|
+
col_count,
|
|
162
|
+
)
|
|
163
|
+
)
|
|
164
|
+
return cls._mro_collect_columns_cached(model, _cache_bust)
|
|
165
|
+
|
|
166
|
+
@classmethod
|
|
167
|
+
def mro_collect_columns(
|
|
168
|
+
cls, model: object, *, _cache_bust: int | None = None
|
|
169
|
+
) -> Dict[str, "ColumnSpec"]:
|
|
170
|
+
"""Backward-compatible alias for :meth:`collect`."""
|
|
171
|
+
|
|
172
|
+
return cls.collect(model, _cache_bust=_cache_bust)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def mro_collect_columns(
|
|
176
|
+
model: object, *, _cache_bust: int | None = None
|
|
177
|
+
) -> Dict[str, ColumnSpec]:
|
|
178
|
+
return ColumnSpec.collect(model, _cache_bust=_cache_bust)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
mro_collect_columns.cache_clear = ColumnSpec._mro_collect_columns_cached.cache_clear
|
|
182
|
+
|
|
183
|
+
__all__ = ["ColumnSpec", "mro_collect_columns"]
|