tigrbl-base 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tigrbl_base/_base/__init__.py +40 -0
- tigrbl_base/_base/_alias_base.py +51 -0
- tigrbl_base/_base/_app_base.py +150 -0
- tigrbl_base/_base/_assembly.py +58 -0
- tigrbl_base/_base/_binding_base.py +14 -0
- tigrbl_base/_base/_column_base.py +101 -0
- tigrbl_base/_base/_engine_base.py +15 -0
- tigrbl_base/_base/_engine_provider_base.py +15 -0
- tigrbl_base/_base/_headers_base.py +12 -0
- tigrbl_base/_base/_hook_base.py +25 -0
- tigrbl_base/_base/_mapping_access.py +19 -0
- tigrbl_base/_base/_middleware_base.py +145 -0
- tigrbl_base/_base/_op_base.py +12 -0
- tigrbl_base/_base/_request_base.py +53 -0
- tigrbl_base/_base/_response_base.py +103 -0
- tigrbl_base/_base/_rest_map.py +122 -0
- tigrbl_base/_base/_router_base.py +60 -0
- tigrbl_base/_base/_rpc_map.py +511 -0
- tigrbl_base/_base/_schema_base.py +52 -0
- tigrbl_base/_base/_security_base.py +69 -0
- tigrbl_base/_base/_session_abc.py +47 -0
- tigrbl_base/_base/_session_base.py +132 -0
- tigrbl_base/_base/_storage.py +18 -0
- tigrbl_base/_base/_table_base.py +539 -0
- tigrbl_base/_base/_table_registry_base.py +55 -0
- tigrbl_base/column/__init__.py +123 -0
- tigrbl_base/column/infer/__init__.py +25 -0
- tigrbl_base/column/infer/core.py +91 -0
- tigrbl_base/column/infer/jsonhints.py +44 -0
- tigrbl_base/column/infer/planning.py +124 -0
- tigrbl_base/column/infer/types.py +82 -0
- tigrbl_base/column/infer/utils.py +59 -0
- tigrbl_base-0.1.0.dist-info/METADATA +53 -0
- tigrbl_base-0.1.0.dist-info/RECORD +35 -0
- tigrbl_base-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Base class implementations for tigrbl internals."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from importlib import import_module
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
_EXPORTS = {
|
|
9
|
+
"AppBase": "_app_base",
|
|
10
|
+
"BindingBase": "_binding_base",
|
|
11
|
+
"BindingRegistryBase": "_binding_base",
|
|
12
|
+
"ColumnBase": "_column_base",
|
|
13
|
+
"AliasBase": "_alias_base",
|
|
14
|
+
"EngineBase": "_engine_base",
|
|
15
|
+
"EngineProviderBase": "_engine_provider_base",
|
|
16
|
+
"HookBase": "_hook_base",
|
|
17
|
+
"RouterBase": "_router_base",
|
|
18
|
+
"ForeignKeyBase": "_storage",
|
|
19
|
+
"StorageTransformBase": "_storage",
|
|
20
|
+
"OpBase": "_op_base",
|
|
21
|
+
"RequestBase": "_request_base",
|
|
22
|
+
"SchemaBase": "_schema_base",
|
|
23
|
+
"SessionABC": "_session_abc",
|
|
24
|
+
"TigrblSessionBase": "_session_base",
|
|
25
|
+
"TableBase": "_table_base",
|
|
26
|
+
"TableRegistryBase": "_table_registry_base",
|
|
27
|
+
"AttrDict": "_mapping_access",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
__all__ = list(_EXPORTS)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def __getattr__(name: str) -> Any:
|
|
34
|
+
module_name = _EXPORTS.get(name)
|
|
35
|
+
if module_name is None:
|
|
36
|
+
raise AttributeError(name)
|
|
37
|
+
module = import_module(f"{__name__}.{module_name}")
|
|
38
|
+
value = getattr(module, name)
|
|
39
|
+
globals()[name] = value
|
|
40
|
+
return value
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import TYPE_CHECKING, Any, Optional
|
|
5
|
+
|
|
6
|
+
from tigrbl_core._spec.alias_spec import AliasSpec
|
|
7
|
+
from tigrbl_core._spec.op_spec import Arity, PersistPolicy
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
10
|
+
from tigrbl_core._spec.schema_spec import SchemaArg
|
|
11
|
+
else:
|
|
12
|
+
SchemaArg = Any
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class AliasBase(AliasSpec):
|
|
17
|
+
"""Base alias contract implementation shared by concrete aliases."""
|
|
18
|
+
|
|
19
|
+
_alias: str
|
|
20
|
+
_request_schema: Optional[SchemaArg] = None
|
|
21
|
+
_response_schema: Optional[SchemaArg] = None
|
|
22
|
+
_persist: Optional[PersistPolicy] = None
|
|
23
|
+
_arity: Optional[Arity] = None
|
|
24
|
+
_rest: Optional[bool] = None
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def alias(self) -> str:
|
|
28
|
+
return self._alias
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def request_schema(self) -> Optional[SchemaArg]:
|
|
32
|
+
return self._request_schema
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def response_schema(self) -> Optional[SchemaArg]:
|
|
36
|
+
return self._response_schema
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def persist(self) -> Optional[PersistPolicy]:
|
|
40
|
+
return self._persist
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def arity(self) -> Optional[Arity]:
|
|
44
|
+
return self._arity
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def rest(self) -> Optional[bool]:
|
|
48
|
+
return self._rest
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
__all__ = ["AliasBase"]
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
import inspect
|
|
5
|
+
from typing import Any
|
|
6
|
+
from typing import Callable, Optional, Sequence
|
|
7
|
+
|
|
8
|
+
from tigrbl_core._spec.app_spec import AppSpec
|
|
9
|
+
from tigrbl_core._spec.engine_spec import EngineCfg
|
|
10
|
+
from tigrbl_core._spec.response_spec import ResponseSpec
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(eq=False)
|
|
14
|
+
class AppBase(AppSpec):
|
|
15
|
+
"""Base app configuration helpers shared by concrete app implementations."""
|
|
16
|
+
|
|
17
|
+
title: str = "Tigrbl"
|
|
18
|
+
description: str | None = None
|
|
19
|
+
version: str = "0.1.0"
|
|
20
|
+
engine: Optional[EngineCfg] = None
|
|
21
|
+
routers: Sequence[Any] = ()
|
|
22
|
+
ops: Sequence[Any] = ()
|
|
23
|
+
tables: Sequence[Any] = ()
|
|
24
|
+
schemas: Sequence[Any] = ()
|
|
25
|
+
hooks: Sequence[Callable[..., Any]] = ()
|
|
26
|
+
security_deps: Sequence[Callable[..., Any]] = ()
|
|
27
|
+
deps: Sequence[Callable[..., Any]] = ()
|
|
28
|
+
response: Optional[ResponseSpec] = None
|
|
29
|
+
jsonrpc_prefix: str = "/rpc"
|
|
30
|
+
system_prefix: str = "/system"
|
|
31
|
+
middlewares: Sequence[Any] = ()
|
|
32
|
+
lifespan: Optional[Callable[..., Any]] = None
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
def _is_bindable_child_class(candidate: Any) -> bool:
|
|
36
|
+
if not inspect.isclass(candidate):
|
|
37
|
+
return False
|
|
38
|
+
|
|
39
|
+
bindable_types: list[type[Any]] = []
|
|
40
|
+
for mod_name, type_name in (
|
|
41
|
+
("._table_base", "TableBase"),
|
|
42
|
+
("._op_base", "OpBase"),
|
|
43
|
+
("._hook_base", "HookBase"),
|
|
44
|
+
("._schema_base", "SchemaBase"),
|
|
45
|
+
):
|
|
46
|
+
try:
|
|
47
|
+
module = __import__(
|
|
48
|
+
f"tigrbl_base._base{mod_name}", fromlist=[type_name]
|
|
49
|
+
)
|
|
50
|
+
bindable = getattr(module, type_name, None)
|
|
51
|
+
if isinstance(bindable, type):
|
|
52
|
+
bindable_types.append(bindable)
|
|
53
|
+
except Exception:
|
|
54
|
+
continue
|
|
55
|
+
|
|
56
|
+
if not bindable_types:
|
|
57
|
+
return False
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
return issubclass(candidate, tuple(bindable_types))
|
|
61
|
+
except TypeError:
|
|
62
|
+
return False
|
|
63
|
+
|
|
64
|
+
@classmethod
|
|
65
|
+
def _bind_child_to_parent(cls, child: Any, *, parent: object | None) -> Any:
|
|
66
|
+
if parent is None:
|
|
67
|
+
return child
|
|
68
|
+
|
|
69
|
+
target = child
|
|
70
|
+
if cls._is_bindable_child_class(child):
|
|
71
|
+
try:
|
|
72
|
+
target = child()
|
|
73
|
+
except Exception:
|
|
74
|
+
target = child
|
|
75
|
+
|
|
76
|
+
for attr in ("app", "parent", "owner"):
|
|
77
|
+
if hasattr(target, attr):
|
|
78
|
+
try:
|
|
79
|
+
setattr(target, attr, parent)
|
|
80
|
+
except Exception:
|
|
81
|
+
pass
|
|
82
|
+
|
|
83
|
+
for attr in ("APP", "PARENT", "OWNER"):
|
|
84
|
+
if hasattr(target, attr):
|
|
85
|
+
try:
|
|
86
|
+
setattr(target, attr, parent)
|
|
87
|
+
except Exception:
|
|
88
|
+
pass
|
|
89
|
+
|
|
90
|
+
return target
|
|
91
|
+
|
|
92
|
+
@classmethod
|
|
93
|
+
def collect_spec(cls, app: type) -> AppSpec:
|
|
94
|
+
"""Collect and normalize an ``AppSpec`` snapshot from an app type."""
|
|
95
|
+
|
|
96
|
+
spec = AppSpec.collect(app)
|
|
97
|
+
routers = tuple(dict.fromkeys(tuple(spec.routers or ())))
|
|
98
|
+
return AppSpec(
|
|
99
|
+
title=spec.title,
|
|
100
|
+
description=spec.description,
|
|
101
|
+
version=spec.version,
|
|
102
|
+
engine=spec.engine,
|
|
103
|
+
routers=routers,
|
|
104
|
+
ops=tuple(spec.ops or ()),
|
|
105
|
+
tables=tuple(spec.tables or ()),
|
|
106
|
+
schemas=tuple(spec.schemas or ()),
|
|
107
|
+
hooks=tuple(spec.hooks or ()),
|
|
108
|
+
security_deps=tuple(spec.security_deps or ()),
|
|
109
|
+
deps=tuple(spec.deps or ()),
|
|
110
|
+
response=spec.response,
|
|
111
|
+
jsonrpc_prefix=spec.jsonrpc_prefix,
|
|
112
|
+
system_prefix=spec.system_prefix,
|
|
113
|
+
middlewares=tuple(spec.middlewares or ()),
|
|
114
|
+
lifespan=spec.lifespan,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
@classmethod
|
|
118
|
+
def _bind_mapped_children(
|
|
119
|
+
cls, children: Sequence[Any] | None, *, parent: object | None
|
|
120
|
+
) -> tuple[Any, ...]:
|
|
121
|
+
return tuple(
|
|
122
|
+
cls._bind_child_to_parent(child, parent=parent)
|
|
123
|
+
for child in tuple(children or ())
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
@classmethod
|
|
127
|
+
def bind_spec(cls, spec: AppSpec, *, parent: object | None = None) -> AppSpec:
|
|
128
|
+
"""Bind a collected AppSpec by attaching mapped children to the parent."""
|
|
129
|
+
|
|
130
|
+
return AppSpec(
|
|
131
|
+
title=str(spec.title or "Tigrbl"),
|
|
132
|
+
description=spec.description,
|
|
133
|
+
version=str(spec.version or "0.1.0"),
|
|
134
|
+
engine=spec.engine,
|
|
135
|
+
routers=cls._bind_mapped_children(spec.routers, parent=parent),
|
|
136
|
+
ops=cls._bind_mapped_children(spec.ops, parent=parent),
|
|
137
|
+
tables=cls._bind_mapped_children(spec.tables, parent=parent),
|
|
138
|
+
schemas=cls._bind_mapped_children(spec.schemas, parent=parent),
|
|
139
|
+
hooks=cls._bind_mapped_children(spec.hooks, parent=parent),
|
|
140
|
+
security_deps=tuple(spec.security_deps or ()),
|
|
141
|
+
deps=tuple(spec.deps or ()),
|
|
142
|
+
response=spec.response,
|
|
143
|
+
jsonrpc_prefix=str(spec.jsonrpc_prefix or "/rpc"),
|
|
144
|
+
system_prefix=str(spec.system_prefix or "/system"),
|
|
145
|
+
middlewares=tuple(spec.middlewares or ()),
|
|
146
|
+
lifespan=spec.lifespan,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
__all__ = ["AppBase"]
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from functools import singledispatch
|
|
4
|
+
from typing import Any, Iterable
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
_CHILD_ATTRS = ("routers", "tables", "ops", "hooks", "schemas", "columns")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def iter_children(spec: Any) -> Iterable[tuple[str, str, Any]]:
|
|
11
|
+
for namespace in _CHILD_ATTRS:
|
|
12
|
+
children = getattr(spec, namespace, ()) or ()
|
|
13
|
+
if isinstance(children, dict):
|
|
14
|
+
for key, child in children.items():
|
|
15
|
+
yield namespace, str(key), child
|
|
16
|
+
continue
|
|
17
|
+
for child in tuple(children):
|
|
18
|
+
key = str(
|
|
19
|
+
getattr(child, "name", None)
|
|
20
|
+
or getattr(child, "alias", None)
|
|
21
|
+
or id(child)
|
|
22
|
+
)
|
|
23
|
+
yield namespace, key, child
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def bind_child(parent: Any, namespace: str, key: str, child: Any) -> Any:
|
|
27
|
+
slot = getattr(parent, namespace, None)
|
|
28
|
+
if hasattr(slot, "__setitem__"):
|
|
29
|
+
try:
|
|
30
|
+
slot[key] = child
|
|
31
|
+
return child
|
|
32
|
+
except Exception:
|
|
33
|
+
pass
|
|
34
|
+
for attr in ("app", "parent", "owner"):
|
|
35
|
+
if hasattr(child, attr):
|
|
36
|
+
try:
|
|
37
|
+
setattr(child, attr, parent)
|
|
38
|
+
except Exception:
|
|
39
|
+
pass
|
|
40
|
+
return child
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@singledispatch
|
|
44
|
+
def maybe_convert(parent: Any, namespace: str, key: str, child: Any) -> Any:
|
|
45
|
+
return child
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def assemble(owner: Any) -> Any:
|
|
49
|
+
collector = getattr(type(owner), "collect", None)
|
|
50
|
+
spec = collector(owner) if callable(collector) else owner
|
|
51
|
+
for namespace, key, child in iter_children(spec):
|
|
52
|
+
assembled_child = assemble(child)
|
|
53
|
+
converted = maybe_convert(owner, namespace, key, assembled_child)
|
|
54
|
+
bind_child(owner, namespace, key, converted)
|
|
55
|
+
finalize = getattr(owner, "finalize", None)
|
|
56
|
+
if callable(finalize):
|
|
57
|
+
finalize(spec)
|
|
58
|
+
return owner
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from tigrbl_core._spec.binding_spec import BindingRegistrySpec, BindingSpec
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class BindingBase(BindingSpec):
|
|
7
|
+
"""Base named binding declaration."""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BindingRegistryBase(BindingRegistrySpec):
|
|
11
|
+
"""Base named binding registry."""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
__all__ = ["BindingBase", "BindingRegistryBase"]
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Callable, Optional
|
|
4
|
+
|
|
5
|
+
from sqlalchemy import ForeignKey
|
|
6
|
+
from sqlalchemy.orm import MappedColumn
|
|
7
|
+
|
|
8
|
+
from tigrbl_core._spec.column_spec import ColumnSpec
|
|
9
|
+
from tigrbl_core._spec.field_spec import FieldSpec as F
|
|
10
|
+
from tigrbl_core._spec.io_spec import IOSpec as IO
|
|
11
|
+
from tigrbl_core._spec.storage_spec import StorageSpec as S
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ColumnBase(ColumnSpec, MappedColumn):
|
|
15
|
+
"""Base SQLAlchemy column descriptor implementing :class:`ColumnSpec`."""
|
|
16
|
+
|
|
17
|
+
__slots__ = ()
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
*,
|
|
22
|
+
spec: ColumnSpec | None = None,
|
|
23
|
+
storage: S | None = None,
|
|
24
|
+
field: F | None = None,
|
|
25
|
+
io: IO | None = None,
|
|
26
|
+
default_factory: Optional[Callable[[dict], Any]] = None,
|
|
27
|
+
read_producer: Optional[Callable[[object, dict], Any]] = None,
|
|
28
|
+
**kw: Any,
|
|
29
|
+
) -> None:
|
|
30
|
+
if spec is not None and any(
|
|
31
|
+
x is not None for x in (storage, field, io, default_factory, read_producer)
|
|
32
|
+
):
|
|
33
|
+
raise ValueError("Provide either spec or individual components, not both.")
|
|
34
|
+
|
|
35
|
+
if spec is None:
|
|
36
|
+
spec = ColumnSpec(
|
|
37
|
+
storage=storage,
|
|
38
|
+
field=field,
|
|
39
|
+
io=io,
|
|
40
|
+
default_factory=default_factory,
|
|
41
|
+
read_producer=read_producer,
|
|
42
|
+
)
|
|
43
|
+
else:
|
|
44
|
+
storage = spec.storage
|
|
45
|
+
field = spec.field
|
|
46
|
+
io = spec.io
|
|
47
|
+
default_factory = spec.default_factory
|
|
48
|
+
read_producer = spec.read_producer
|
|
49
|
+
|
|
50
|
+
s = storage
|
|
51
|
+
if s is not None:
|
|
52
|
+
args: list[Any] = [s.type_]
|
|
53
|
+
fk = getattr(s, "fk", None)
|
|
54
|
+
if fk is not None:
|
|
55
|
+
args.append(
|
|
56
|
+
ForeignKey(
|
|
57
|
+
fk.target,
|
|
58
|
+
ondelete=fk.on_delete,
|
|
59
|
+
onupdate=fk.on_update,
|
|
60
|
+
deferrable=fk.deferrable,
|
|
61
|
+
initially="DEFERRED" if fk.initially_deferred else "IMMEDIATE",
|
|
62
|
+
match=fk.match,
|
|
63
|
+
)
|
|
64
|
+
)
|
|
65
|
+
MappedColumn.__init__(
|
|
66
|
+
self,
|
|
67
|
+
*args,
|
|
68
|
+
primary_key=s.primary_key,
|
|
69
|
+
nullable=s.nullable,
|
|
70
|
+
unique=s.unique,
|
|
71
|
+
index=s.index,
|
|
72
|
+
default=s.default,
|
|
73
|
+
autoincrement=s.autoincrement,
|
|
74
|
+
server_default=s.server_default,
|
|
75
|
+
onupdate=s.onupdate,
|
|
76
|
+
comment=s.comment,
|
|
77
|
+
**kw,
|
|
78
|
+
)
|
|
79
|
+
else:
|
|
80
|
+
MappedColumn.__init__(self, **kw)
|
|
81
|
+
|
|
82
|
+
self.storage = s
|
|
83
|
+
self.field = field if field is not None else F()
|
|
84
|
+
self.io = io if io is not None else IO()
|
|
85
|
+
self.default_factory = default_factory
|
|
86
|
+
self.read_producer = read_producer
|
|
87
|
+
|
|
88
|
+
def __set_name__(self, owner: type, name: str) -> None:
|
|
89
|
+
parent = getattr(super(), "__set_name__", None)
|
|
90
|
+
if parent:
|
|
91
|
+
parent(owner, name)
|
|
92
|
+
|
|
93
|
+
colspecs = owner.__dict__.get("__tigrbl_colspecs__")
|
|
94
|
+
if colspecs is None:
|
|
95
|
+
base_specs = getattr(owner, "__tigrbl_colspecs__", {})
|
|
96
|
+
colspecs = dict(base_specs)
|
|
97
|
+
setattr(owner, "__tigrbl_colspecs__", colspecs)
|
|
98
|
+
colspecs[name] = self
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
__all__ = ["ColumnBase"]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class EngineBase:
|
|
7
|
+
"""Base contract for concrete engine façade implementations."""
|
|
8
|
+
|
|
9
|
+
spec: Any
|
|
10
|
+
|
|
11
|
+
def to_provider(self) -> Any: # pragma: no cover - interface contract
|
|
12
|
+
raise NotImplementedError
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
__all__ = ["EngineBase"]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Protocol, runtime_checkable
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@runtime_checkable
|
|
7
|
+
class EngineProviderBase(Protocol):
|
|
8
|
+
"""Provider-like runtime contract for engine provider objects."""
|
|
9
|
+
|
|
10
|
+
spec: Any
|
|
11
|
+
|
|
12
|
+
def to_provider(self) -> "EngineProviderBase": ...
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
__all__ = ["EngineProviderBase"]
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class HeadersBase(dict[str, str]):
|
|
5
|
+
"""Base header mapping type for response implementations."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class HeaderCookiesBase(dict[str, str]):
|
|
9
|
+
"""Base cookie mapping type for response implementations."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
__all__ = ["HeaderCookiesBase", "HeadersBase"]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Base runtime hook wrapper for Tigrbl v3."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Iterable, Optional, Union
|
|
7
|
+
|
|
8
|
+
from tigrbl_core._spec.hook_spec import HookSpec
|
|
9
|
+
from tigrbl_atoms import HookPhase, StepFn
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True, slots=True)
|
|
13
|
+
class HookBase(HookSpec):
|
|
14
|
+
"""Base hook bound to a phase and one or more ops."""
|
|
15
|
+
|
|
16
|
+
phase: HookPhase
|
|
17
|
+
fn: StepFn
|
|
18
|
+
ops: Union[str, Iterable[str]] = "*"
|
|
19
|
+
order: int = 0
|
|
20
|
+
when: Optional[object] = None
|
|
21
|
+
name: Optional[str] = None
|
|
22
|
+
description: Optional[str] = None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
__all__ = ["HookBase"]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class AttrDict(dict):
|
|
7
|
+
"""Dictionary that also supports attribute-style access."""
|
|
8
|
+
|
|
9
|
+
def __getattr__(self, item: str) -> Any: # pragma: no cover - trivial
|
|
10
|
+
try:
|
|
11
|
+
return self[item]
|
|
12
|
+
except KeyError as e: # pragma: no cover - debug helper
|
|
13
|
+
raise AttributeError(item) from e
|
|
14
|
+
|
|
15
|
+
def __setattr__(self, key: str, value: Any) -> None: # pragma: no cover - trivial
|
|
16
|
+
self[key] = value
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
__all__ = ["AttrDict"]
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Request/response middleware base with ``dispatch`` + ``call_next`` semantics."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
from urllib.parse import urlencode
|
|
7
|
+
|
|
8
|
+
from tigrbl_core._spec.middleware_spec import (
|
|
9
|
+
ASGIApp,
|
|
10
|
+
ASGIReceive,
|
|
11
|
+
ASGISend,
|
|
12
|
+
Message,
|
|
13
|
+
MiddlewareSpec,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
from ._request_base import RequestBase
|
|
17
|
+
from ._response_base import ResponseBase
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def finalize_transport_response(
|
|
21
|
+
scope: dict[str, Any],
|
|
22
|
+
status_code: int,
|
|
23
|
+
raw_headers: list[tuple[bytes, bytes]],
|
|
24
|
+
body: bytes,
|
|
25
|
+
) -> tuple[list[tuple[bytes, bytes]], bytes]:
|
|
26
|
+
"""Base transport finalization hook with pass-through defaults."""
|
|
27
|
+
|
|
28
|
+
del scope, status_code
|
|
29
|
+
return raw_headers, body
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class MiddlewareBase(MiddlewareSpec):
|
|
33
|
+
"""Base middleware for intercepting HTTP requests in ASGI mode."""
|
|
34
|
+
|
|
35
|
+
def __init__(self, app: ASGIApp, **kwargs: Any) -> None:
|
|
36
|
+
self.app = app
|
|
37
|
+
self.kwargs = kwargs
|
|
38
|
+
|
|
39
|
+
async def dispatch(self, request: RequestBase, call_next: Any) -> ResponseBase:
|
|
40
|
+
return await call_next(request)
|
|
41
|
+
|
|
42
|
+
@staticmethod
|
|
43
|
+
def _scope_from_request(
|
|
44
|
+
scope: dict[str, Any], request: RequestBase
|
|
45
|
+
) -> dict[str, Any]:
|
|
46
|
+
query_string = urlencode(
|
|
47
|
+
[
|
|
48
|
+
(name, value)
|
|
49
|
+
for name, values in request.query.items()
|
|
50
|
+
for value in values
|
|
51
|
+
],
|
|
52
|
+
doseq=True,
|
|
53
|
+
).encode("latin-1")
|
|
54
|
+
return {
|
|
55
|
+
**scope,
|
|
56
|
+
"method": request.method,
|
|
57
|
+
"path": request.path,
|
|
58
|
+
"query_string": query_string,
|
|
59
|
+
"headers": [
|
|
60
|
+
(key.encode("latin-1"), value.encode("latin-1"))
|
|
61
|
+
for key, value in request.headers.items()
|
|
62
|
+
],
|
|
63
|
+
"root_path": request.script_name,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async def asgi(
|
|
67
|
+
self, scope: dict[str, Any], receive: ASGIReceive, send: ASGISend
|
|
68
|
+
) -> None:
|
|
69
|
+
if scope.get("type") != "http":
|
|
70
|
+
await self.app(scope, receive, send)
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
request_body = b""
|
|
74
|
+
more_body = True
|
|
75
|
+
while more_body:
|
|
76
|
+
message = await receive()
|
|
77
|
+
request_body += message.get("body", b"")
|
|
78
|
+
more_body = message.get("more_body", False)
|
|
79
|
+
|
|
80
|
+
request = RequestBase.from_scope(scope)
|
|
81
|
+
request.body = request_body
|
|
82
|
+
|
|
83
|
+
async def call_next(forward_request: RequestBase | None = None) -> ResponseBase:
|
|
84
|
+
target_request = forward_request or request
|
|
85
|
+
target_scope = self._scope_from_request(scope, target_request)
|
|
86
|
+
|
|
87
|
+
messages: list[Message] = []
|
|
88
|
+
body_sent = False
|
|
89
|
+
|
|
90
|
+
async def receive_for_app() -> Message:
|
|
91
|
+
nonlocal body_sent
|
|
92
|
+
if body_sent:
|
|
93
|
+
return {"type": "http.request", "body": b"", "more_body": False}
|
|
94
|
+
body_sent = True
|
|
95
|
+
return {
|
|
96
|
+
"type": "http.request",
|
|
97
|
+
"body": target_request.body,
|
|
98
|
+
"more_body": False,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async def send_from_app(message: Message) -> None:
|
|
102
|
+
messages.append(message)
|
|
103
|
+
|
|
104
|
+
await self.app(target_scope, receive_for_app, send_from_app)
|
|
105
|
+
|
|
106
|
+
start = next(m for m in messages if m.get("type") == "http.response.start")
|
|
107
|
+
raw_headers = list(start.get("headers", []))
|
|
108
|
+
body = b"".join(
|
|
109
|
+
m.get("body", b"")
|
|
110
|
+
for m in messages
|
|
111
|
+
if m.get("type") == "http.response.body"
|
|
112
|
+
)
|
|
113
|
+
headers, finalized_body = finalize_transport_response(
|
|
114
|
+
target_scope,
|
|
115
|
+
int(start.get("status", 200)),
|
|
116
|
+
raw_headers,
|
|
117
|
+
body,
|
|
118
|
+
)
|
|
119
|
+
return ResponseBase(
|
|
120
|
+
status_code=int(start.get("status", 200)),
|
|
121
|
+
headers=[
|
|
122
|
+
(key.decode("latin-1"), value.decode("latin-1"))
|
|
123
|
+
for key, value in headers
|
|
124
|
+
],
|
|
125
|
+
body=finalized_body,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
response = await self.dispatch(request, call_next)
|
|
129
|
+
headers, finalized_body = finalize_transport_response(
|
|
130
|
+
scope, response.status_code, response.raw_headers, response.body
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
await send(
|
|
134
|
+
{
|
|
135
|
+
"type": "http.response.start",
|
|
136
|
+
"status": response.status_code,
|
|
137
|
+
"headers": headers,
|
|
138
|
+
}
|
|
139
|
+
)
|
|
140
|
+
await send(
|
|
141
|
+
{"type": "http.response.body", "body": finalized_body, "more_body": False}
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
__all__ = ["MiddlewareBase"]
|