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.
@@ -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"]
@@ -0,0 +1,12 @@
1
+ """Base operation descriptor implementation for Tigrbl."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from tigrbl_core._spec.op_spec import OpSpec
6
+
7
+
8
+ class OpBase(OpSpec):
9
+ """Base operation descriptor type."""
10
+
11
+
12
+ __all__ = ["OpBase"]