tigrbl-base 0.1.0.dev1__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,24 @@
1
+ """Base class implementations for tigrbl internals."""
2
+
3
+ from ._hook_base import HookBase
4
+ from ._storage import ForeignKeyBase
5
+ from ._op_base import OpBase
6
+ from ._request_base import RequestBase
7
+ from ._schema_base import SchemaBase
8
+ from ._session_abc import SessionABC
9
+ from ._session_base import TigrblSessionBase
10
+ from ._table_base import TableBase
11
+ from ._table_registry_base import TableRegistryBase
12
+
13
+
14
+ __all__ = [
15
+ "HookBase",
16
+ "ForeignKeyBase",
17
+ "OpBase",
18
+ "RequestBase",
19
+ "SchemaBase",
20
+ "SessionABC",
21
+ "TigrblSessionBase",
22
+ "TableBase",
23
+ "TableRegistryBase",
24
+ ]
@@ -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_runtime.runtime.hook_types 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,146 @@
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_concrete._concrete._request import Request
9
+ from tigrbl_concrete._concrete._request_adapters import request_from_asgi
10
+ from tigrbl_concrete._concrete._response import Response
11
+ from tigrbl_atoms.atoms.egress.asgi_send import finalize_transport_response
12
+
13
+ from tigrbl_core._spec.middleware_spec import (
14
+ ASGIReceive,
15
+ ASGISend,
16
+ Message,
17
+ MiddlewareSpec,
18
+ )
19
+
20
+
21
+ class MiddlewareBase(MiddlewareSpec):
22
+ """Base middleware for intercepting HTTP requests in ASGI mode."""
23
+
24
+ async def dispatch(
25
+ self,
26
+ request: Request,
27
+ call_next: Any,
28
+ ) -> Response:
29
+ """Process the request and optionally delegate to downstream middleware/app."""
30
+
31
+ return await call_next(request)
32
+
33
+ @staticmethod
34
+ def _scope_from_request(scope: dict[str, Any], request: Request) -> dict[str, Any]:
35
+ query_string = urlencode(
36
+ [
37
+ (name, value)
38
+ for name, values in request.query.items()
39
+ for value in values
40
+ ],
41
+ doseq=True,
42
+ ).encode("latin-1")
43
+ return {
44
+ **scope,
45
+ "method": request.method,
46
+ "path": request.path,
47
+ "query_string": query_string,
48
+ "headers": [
49
+ (key.encode("latin-1"), value.encode("latin-1"))
50
+ for key, value in request.headers.items()
51
+ ],
52
+ "root_path": request.script_name,
53
+ }
54
+
55
+ async def asgi(
56
+ self,
57
+ scope: dict[str, Any],
58
+ receive: ASGIReceive,
59
+ send: ASGISend,
60
+ ) -> None:
61
+ if scope.get("type") != "http":
62
+ await self.app(scope, receive, send)
63
+ return
64
+
65
+ request_body = b""
66
+ more_body = True
67
+ while more_body:
68
+ message = await receive()
69
+ request_body += message.get("body", b"")
70
+ more_body = message.get("more_body", False)
71
+
72
+ request = request_from_asgi(None, scope, request_body)
73
+
74
+ async def call_next(forward_request: Request | None = None) -> Response:
75
+ target_request = forward_request or request
76
+ target_scope = self._scope_from_request(scope, target_request)
77
+
78
+ messages: list[Message] = []
79
+ body_sent = False
80
+
81
+ async def receive_for_app() -> Message:
82
+ nonlocal body_sent
83
+ if body_sent:
84
+ return {"type": "http.request", "body": b"", "more_body": False}
85
+ body_sent = True
86
+ return {
87
+ "type": "http.request",
88
+ "body": target_request.body,
89
+ "more_body": False,
90
+ }
91
+
92
+ async def send_from_app(message: Message) -> None:
93
+ messages.append(message)
94
+
95
+ await self.app(target_scope, receive_for_app, send_from_app)
96
+
97
+ start = next(
98
+ message
99
+ for message in messages
100
+ if message.get("type") == "http.response.start"
101
+ )
102
+ raw_headers = list(start.get("headers", []))
103
+ body = b"".join(
104
+ message.get("body", b"")
105
+ for message in messages
106
+ if message.get("type") == "http.response.body"
107
+ )
108
+ headers, finalized_body = finalize_transport_response(
109
+ target_scope,
110
+ int(start.get("status", 200)),
111
+ raw_headers,
112
+ body,
113
+ )
114
+ return Response(
115
+ status_code=int(start.get("status", 200)),
116
+ headers=[
117
+ (key.decode("latin-1"), value.decode("latin-1"))
118
+ for key, value in headers
119
+ ],
120
+ body=finalized_body,
121
+ )
122
+
123
+ response = await self.dispatch(request, call_next)
124
+ headers, finalized_body = finalize_transport_response(
125
+ scope,
126
+ response.status_code,
127
+ response.raw_headers,
128
+ response.body,
129
+ )
130
+ await send(
131
+ {
132
+ "type": "http.response.start",
133
+ "status": response.status_code,
134
+ "headers": headers,
135
+ }
136
+ )
137
+ await send(
138
+ {
139
+ "type": "http.response.body",
140
+ "body": finalized_body,
141
+ "more_body": False,
142
+ }
143
+ )
144
+
145
+
146
+ __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"]
@@ -0,0 +1,32 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from tigrbl_core._spec.request_spec import RequestSpec
6
+
7
+
8
+ class RequestBase(RequestSpec):
9
+ """Base request model behavior shared by concrete request implementations."""
10
+
11
+ @classmethod
12
+ def from_scope(
13
+ cls,
14
+ scope: dict[str, Any],
15
+ receive: Any | None = None,
16
+ *,
17
+ app: Any | None = None,
18
+ state: Any | None = None,
19
+ ) -> "RequestBase":
20
+ """Construct a request from an ASGI scope.
21
+
22
+ Middleware/tests may resolve ``Request`` through base/spec surfaces during
23
+ import cycles. Delegate to the concrete request model to keep
24
+ ``Request.from_scope(...)`` consistently available.
25
+ """
26
+
27
+ from tigrbl_concrete._concrete._request import Request
28
+
29
+ return Request.from_scope(scope, receive, app=app, state=state)
30
+
31
+
32
+ __all__ = ["RequestBase"]
@@ -0,0 +1,231 @@
1
+ from __future__ import annotations
2
+
3
+ import json as json_module
4
+ from dataclasses import dataclass
5
+ from http.cookies import SimpleCookie
6
+ from typing import Any, Mapping
7
+
8
+ from tigrbl_concrete._concrete._headers import HeaderCookies, Headers
9
+
10
+ from tigrbl_core._spec.response_spec import ResponseSpec, TemplateSpec
11
+
12
+
13
+ class _JSONDualMethod:
14
+ def __get__(
15
+ self,
16
+ obj: "ResponseBase" | None,
17
+ owner: type["ResponseBase"],
18
+ ):
19
+ if obj is None:
20
+
21
+ def _factory(
22
+ data: Any,
23
+ status_code: int = 200,
24
+ headers: Mapping[str, str] | None = None,
25
+ ) -> "ResponseBase":
26
+ return owner.from_json(data, status_code=status_code, headers=headers)
27
+
28
+ return _factory
29
+
30
+ def _instance_json() -> Any:
31
+ return obj.json_body()
32
+
33
+ return _instance_json
34
+
35
+
36
+ class ResponseBase(ResponseSpec):
37
+ """Concrete HTTP response object that also implements ``ResponseSpec``."""
38
+
39
+ json = _JSONDualMethod()
40
+
41
+ def __init__(
42
+ self,
43
+ *,
44
+ status_code: int = 200,
45
+ headers: Mapping[str, str] | list[tuple[str, str]] | None = None,
46
+ body: bytes | None = None,
47
+ content: bytes | None = None,
48
+ media_type: str | None = None,
49
+ kind: str = "auto",
50
+ envelope: bool | None = None,
51
+ template: TemplateSpec | None = None,
52
+ filename: str | None = None,
53
+ download: bool | None = None,
54
+ etag: str | None = None,
55
+ cache_control: str | None = None,
56
+ redirect_to: str | None = None,
57
+ ) -> None:
58
+ if body is not None and content is not None:
59
+ raise TypeError(
60
+ "ResponseBase: provide either 'body' or 'content', not both"
61
+ )
62
+
63
+ payload = (
64
+ body if body is not None else (content if content is not None else b"")
65
+ )
66
+
67
+ super().__init__(
68
+ kind=kind,
69
+ media_type=media_type,
70
+ status_code=status_code,
71
+ headers={
72
+ k: v
73
+ for k, v in (
74
+ headers.items() if hasattr(headers, "items") else (headers or [])
75
+ )
76
+ },
77
+ envelope=envelope,
78
+ template=template,
79
+ filename=filename,
80
+ download=download,
81
+ etag=etag,
82
+ cache_control=cache_control,
83
+ redirect_to=redirect_to,
84
+ )
85
+ self.status_code = status_code
86
+ self.headers = Headers(headers or {})
87
+ self.body = payload
88
+ self.media_type = media_type
89
+ self._headers = self.headers
90
+
91
+ @staticmethod
92
+ def _status_text(code: int) -> str:
93
+ return {
94
+ 200: "OK",
95
+ 201: "Created",
96
+ 205: "Reset Content",
97
+ 204: "No Content",
98
+ 301: "Moved Permanently",
99
+ 302: "Found",
100
+ 307: "Temporary Redirect",
101
+ 308: "Permanent Redirect",
102
+ 400: "Bad Request",
103
+ 401: "Unauthorized",
104
+ 403: "Forbidden",
105
+ 404: "Not Found",
106
+ 405: "Method Not Allowed",
107
+ 422: "Unprocessable Entity",
108
+ 500: "Internal Server Error",
109
+ }.get(code, "OK")
110
+
111
+ def status_line(self) -> str:
112
+ return f"{self.status_code} {self._status_text(self.status_code)}"
113
+
114
+ @property
115
+ def raw_headers(self) -> list[tuple[bytes, bytes]]:
116
+ return [
117
+ (k.encode("latin-1"), v.encode("latin-1")) for k, v in self.headers.items()
118
+ ]
119
+
120
+ @property
121
+ def headers_map(self) -> Headers:
122
+ return self.headers
123
+
124
+ @property
125
+ def body_text(self) -> str:
126
+ return self.body.decode("utf-8")
127
+
128
+ def json_body(self) -> Any:
129
+ if not self.body:
130
+ return None
131
+ return json_module.loads(self.body.decode("utf-8"))
132
+
133
+ @property
134
+ def cookies(self) -> HeaderCookies:
135
+ cookie = SimpleCookie()
136
+ for name, value in self.headers.items():
137
+ if name == "set-cookie":
138
+ cookie.load(value)
139
+ return HeaderCookies({name: morsel.value for name, morsel in cookie.items()})
140
+
141
+ def set_cookie(
142
+ self,
143
+ key: str,
144
+ value: str,
145
+ *,
146
+ path: str = "/",
147
+ domain: str | None = None,
148
+ secure: bool = False,
149
+ httponly: bool = False,
150
+ samesite: str | None = None,
151
+ max_age: int | None = None,
152
+ expires: str | None = None,
153
+ ) -> None:
154
+ cookie = SimpleCookie()
155
+ cookie[key] = value
156
+ morsel = cookie[key]
157
+ morsel["path"] = path
158
+ if domain is not None:
159
+ morsel["domain"] = domain
160
+ if secure:
161
+ morsel["secure"] = True
162
+ if httponly:
163
+ morsel["httponly"] = True
164
+ if samesite is not None:
165
+ morsel["samesite"] = samesite
166
+ if max_age is not None:
167
+ morsel["max-age"] = str(max_age)
168
+ if expires is not None:
169
+ morsel["expires"] = expires
170
+ self.headers["set-cookie"] = cookie.output(header="").strip()
171
+
172
+ @classmethod
173
+ def from_json(
174
+ cls,
175
+ data: Any,
176
+ status_code: int = 200,
177
+ headers: Mapping[str, str] | None = None,
178
+ ) -> "ResponseBase":
179
+ payload = json_module.dumps(
180
+ data, ensure_ascii=False, separators=(",", ":"), default=str
181
+ ).encode("utf-8")
182
+ hdrs = [("content-type", "application/json; charset=utf-8")]
183
+ for k, v in (headers or {}).items():
184
+ hdrs.append((k.lower(), v))
185
+ return cls(
186
+ status_code=status_code,
187
+ headers=hdrs,
188
+ body=payload,
189
+ media_type="application/json",
190
+ )
191
+
192
+ @classmethod
193
+ def html(
194
+ cls,
195
+ html: str,
196
+ status_code: int = 200,
197
+ headers: Mapping[str, str] | None = None,
198
+ ) -> "ResponseBase":
199
+ payload = html.encode("utf-8")
200
+ hdrs = [("content-type", "text/html; charset=utf-8")]
201
+ for k, v in (headers or {}).items():
202
+ hdrs.append((k.lower(), v))
203
+ return cls(
204
+ status_code=status_code,
205
+ headers=hdrs,
206
+ body=payload,
207
+ media_type="text/html",
208
+ )
209
+
210
+ @classmethod
211
+ def text(
212
+ cls,
213
+ text: str,
214
+ status_code: int = 200,
215
+ headers: Mapping[str, str] | None = None,
216
+ ) -> "ResponseBase":
217
+ payload = text.encode("utf-8")
218
+ hdrs = [("content-type", "text/plain; charset=utf-8")]
219
+ for k, v in (headers or {}).items():
220
+ hdrs.append((k.lower(), v))
221
+ return cls(
222
+ status_code=status_code,
223
+ headers=hdrs,
224
+ body=payload,
225
+ media_type="text/plain",
226
+ )
227
+
228
+
229
+ @dataclass
230
+ class TemplateBase(TemplateSpec):
231
+ """Concrete template configuration used at runtime."""
@@ -0,0 +1,16 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ class SchemaBase:
5
+ """Shared schema helpers used by concrete schema wrappers."""
6
+
7
+ @classmethod
8
+ def collect(cls, model: type) -> dict[str, dict[str, type]]:
9
+ from tigrbl_canon.mapping.collect_decorated_schemas import (
10
+ collect_decorated_schemas,
11
+ )
12
+
13
+ return collect_decorated_schemas(model)
14
+
15
+
16
+ __all__ = ["SchemaBase"]
@@ -0,0 +1,69 @@
1
+ """Core OpenAPI security scheme primitives."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Mapping, Sequence
6
+
7
+ _VALID_SECURITY_SCHEME_TYPES = {
8
+ "http",
9
+ "apiKey",
10
+ "oauth2",
11
+ "openIdConnect",
12
+ "mutualTLS",
13
+ }
14
+
15
+
16
+ class OpenAPISecurityDependency:
17
+ """Base security dependency with OpenAPI document metadata."""
18
+
19
+ def __init__(
20
+ self,
21
+ *,
22
+ scheme_name: str,
23
+ scheme: Mapping[str, Any],
24
+ scopes: Sequence[str] | None = None,
25
+ auto_error: bool = True,
26
+ ) -> None:
27
+ self.scheme_name = scheme_name
28
+ self.auto_error = auto_error
29
+ self._scheme = dict(scheme)
30
+ self._scopes = list(scopes or [])
31
+ validate_openapi_security_scheme(self._scheme)
32
+
33
+ def openapi_security_scheme(self) -> dict[str, Any]:
34
+ return dict(self._scheme)
35
+
36
+ def openapi_security_requirement(self) -> dict[str, list[str]]:
37
+ return {self.scheme_name: list(self._scopes)}
38
+
39
+ def __call__(self, request: Any) -> Any | None:
40
+ return None
41
+
42
+
43
+ def validate_openapi_security_scheme(scheme: Mapping[str, Any]) -> None:
44
+ scheme_type = scheme.get("type")
45
+ if scheme_type not in _VALID_SECURITY_SCHEME_TYPES:
46
+ raise ValueError(
47
+ "OpenAPI security scheme 'type' must be one of: "
48
+ f"{sorted(_VALID_SECURITY_SCHEME_TYPES)}"
49
+ )
50
+
51
+ if "scheme" in scheme and scheme_type != "http":
52
+ raise ValueError("OpenAPI 'scheme' is only valid when type is 'http'.")
53
+
54
+ if scheme_type == "http" and not scheme.get("scheme"):
55
+ raise ValueError("OpenAPI type='http' requires a non-empty 'scheme'.")
56
+
57
+ if scheme_type == "apiKey":
58
+ if scheme.get("in") not in {"header", "query", "cookie"}:
59
+ raise ValueError(
60
+ "OpenAPI type='apiKey' requires 'in' to be header/query/cookie."
61
+ )
62
+ if not scheme.get("name"):
63
+ raise ValueError("OpenAPI type='apiKey' requires 'name'.")
64
+
65
+ if scheme_type == "oauth2" and not isinstance(scheme.get("flows"), Mapping):
66
+ raise ValueError("OpenAPI type='oauth2' requires a 'flows' object.")
67
+
68
+ if scheme_type == "openIdConnect" and not scheme.get("openIdConnectUrl"):
69
+ raise ValueError("OpenAPI type='openIdConnect' requires 'openIdConnectUrl'.")
@@ -0,0 +1,47 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import Any, Callable
5
+
6
+
7
+ class SessionABC(ABC):
8
+ """Authoritative Tigrbl session interface."""
9
+
10
+ @abstractmethod
11
+ async def begin(self) -> None: ...
12
+
13
+ @abstractmethod
14
+ async def commit(self) -> None: ...
15
+
16
+ @abstractmethod
17
+ async def rollback(self) -> None: ...
18
+
19
+ @abstractmethod
20
+ def in_transaction(self) -> bool: ...
21
+
22
+ @abstractmethod
23
+ async def get(self, model: type, ident: Any) -> Any | None: ...
24
+
25
+ @abstractmethod
26
+ def add(self, obj: Any) -> None: ...
27
+
28
+ @abstractmethod
29
+ async def delete(self, obj: Any) -> None: ...
30
+
31
+ @abstractmethod
32
+ async def flush(self) -> None: ...
33
+
34
+ @abstractmethod
35
+ async def refresh(self, obj: Any) -> None: ...
36
+
37
+ @abstractmethod
38
+ async def execute(self, stmt: Any) -> Any: ...
39
+
40
+ @abstractmethod
41
+ async def close(self) -> None: ...
42
+
43
+ @abstractmethod
44
+ async def run_sync(self, fn: Callable[[Any], Any]) -> Any: ...
45
+
46
+
47
+ __all__ = ["SessionABC"]
@@ -0,0 +1,132 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import inspect
5
+ from dataclasses import dataclass, field
6
+ from typing import Any, Callable, List, Optional
7
+
8
+ from ._session_abc import SessionABC
9
+ from tigrbl_core._spec.session_spec import SessionSpec
10
+
11
+
12
+ @dataclass
13
+ class TigrblSessionBase(SessionABC):
14
+ _spec: Optional[SessionSpec] = None
15
+ _open: bool = field(default=False, init=False)
16
+ _dirty: bool = field(default=False, init=False)
17
+ _pending: List[asyncio.Task] = field(default_factory=list, init=False)
18
+
19
+ def apply_spec(self, spec: SessionSpec | None) -> None:
20
+ self._spec = spec
21
+
22
+ async def run_sync(self, fn: Callable[[Any], Any]) -> Any:
23
+ rv = fn(self)
24
+ if inspect.isawaitable(rv):
25
+ return await rv
26
+ return rv
27
+
28
+ async def begin(self) -> None:
29
+ await self._tx_begin_impl()
30
+ self._open = True
31
+
32
+ async def commit(self) -> None:
33
+ if self._spec and self._spec.read_only and self._dirty:
34
+ raise RuntimeError("read-only session: writes detected before commit")
35
+ await self.flush()
36
+ await self._tx_commit_impl()
37
+ self._open = False
38
+ self._dirty = False
39
+
40
+ async def rollback(self) -> None:
41
+ for t in self._pending:
42
+ try:
43
+ t.cancel()
44
+ except Exception:
45
+ pass
46
+ self._pending.clear()
47
+ await self._tx_rollback_impl()
48
+ self._open = False
49
+ self._dirty = False
50
+
51
+ def in_transaction(self) -> bool:
52
+ return bool(self._open)
53
+
54
+ def add(self, obj: Any) -> None:
55
+ if self._spec and self._spec.read_only:
56
+ raise RuntimeError("write attempted in read-only session (add)")
57
+ self._dirty = True
58
+ rv = self._add_impl(obj)
59
+ if inspect.isawaitable(rv):
60
+ try:
61
+ loop = asyncio.get_running_loop()
62
+ except RuntimeError:
63
+ asyncio.run(rv)
64
+ else:
65
+ self._pending.append(loop.create_task(rv))
66
+
67
+ async def delete(self, obj: Any) -> None:
68
+ if self._spec and self._spec.read_only:
69
+ raise RuntimeError("write attempted in read-only session (delete)")
70
+ self._dirty = True
71
+ await self._delete_impl(obj)
72
+
73
+ async def flush(self) -> None:
74
+ if self._pending:
75
+ done, _ = await asyncio.wait(
76
+ self._pending, return_when=asyncio.ALL_COMPLETED
77
+ )
78
+ self._pending = []
79
+ for t in done:
80
+ _ = t.result()
81
+ await self._flush_impl()
82
+
83
+ async def refresh(self, obj: Any) -> None:
84
+ await self._refresh_impl(obj)
85
+
86
+ async def get(self, model: type, ident: Any) -> Any | None:
87
+ return await self._get_impl(model, ident)
88
+
89
+ async def execute(self, stmt: Any) -> Any:
90
+ return await self._execute_impl(stmt)
91
+
92
+ async def close(self) -> None:
93
+ for t in self._pending:
94
+ try:
95
+ t.cancel()
96
+ except Exception:
97
+ pass
98
+ self._pending = []
99
+ await self._close_impl()
100
+
101
+ async def _tx_begin_impl(self) -> None:
102
+ raise NotImplementedError
103
+
104
+ async def _tx_commit_impl(self) -> None:
105
+ raise NotImplementedError
106
+
107
+ async def _tx_rollback_impl(self) -> None:
108
+ raise NotImplementedError
109
+
110
+ def _add_impl(self, obj: Any) -> Any:
111
+ raise NotImplementedError
112
+
113
+ async def _delete_impl(self, obj: Any) -> None:
114
+ raise NotImplementedError
115
+
116
+ async def _flush_impl(self) -> None:
117
+ return
118
+
119
+ async def _refresh_impl(self, obj: Any) -> None:
120
+ return
121
+
122
+ async def _get_impl(self, model: type, ident: Any) -> Any | None:
123
+ raise NotImplementedError
124
+
125
+ async def _execute_impl(self, stmt: Any) -> Any:
126
+ raise NotImplementedError
127
+
128
+ async def _close_impl(self) -> None:
129
+ return
130
+
131
+
132
+ __all__ = ["TigrblSessionBase"]
@@ -0,0 +1,13 @@
1
+ """Base storage-layer primitives."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from tigrbl_core._spec.storage_spec import ForeignKeySpec
6
+
7
+
8
+ @dataclass(frozen=True)
9
+ class ForeignKeyBase(ForeignKeySpec):
10
+ """Base foreign-key configuration shared by concrete implementations."""
11
+
12
+
13
+ __all__ = ["ForeignKeyBase"]
@@ -0,0 +1,435 @@
1
+ # tigrbl/tigrbl/table/_base.py
2
+ from __future__ import annotations
3
+
4
+ from typing import Any, Optional, Union, get_args, get_origin
5
+ from enum import Enum as PyEnum
6
+
7
+ from sqlalchemy.orm import DeclarativeBase, declared_attr, mapped_column
8
+ from sqlalchemy import CheckConstraint, ForeignKey, MetaData
9
+ from sqlalchemy.types import Enum as SAEnum, String
10
+
11
+ # ──────────────────────────────────────────────────────────────────────────────
12
+ # Helpers – type inference & SA type instantiation
13
+ # ──────────────────────────────────────────────────────────────────────────────
14
+
15
+
16
+ def _unwrap_optional(t: Any) -> Any:
17
+ """Optional[T] / Union[T, None] → T"""
18
+ if get_origin(t) is Union:
19
+ args = [a for a in get_args(t) if a is not type(None)]
20
+ return args[0] if args else t
21
+ return t
22
+
23
+
24
+ def _infer_py_type(cls, name: str, spec: Any) -> Optional[type]:
25
+ """
26
+ Prefer FieldSpec.py_type if provided; otherwise unwrap Mapped[...] / Optional[...]
27
+ from the class' annotation to get the real Python type for the column.
28
+ """
29
+ fld = getattr(spec, "field", None)
30
+ py = getattr(fld, "py_type", None)
31
+ if isinstance(py, type):
32
+ return py
33
+
34
+ ann = getattr(cls, "__annotations__", {}).get(name)
35
+ if ann is None:
36
+ return None
37
+
38
+ # Mapped[T] → T (then unwrap Optional)
39
+ try:
40
+ from sqlalchemy.orm import Mapped
41
+
42
+ if get_origin(ann) is Mapped:
43
+ inner = get_args(ann)[0]
44
+ return _unwrap_optional(inner)
45
+ except Exception:
46
+ pass
47
+
48
+ # Optional[T]/Union[T, None] → T
49
+ return _unwrap_optional(ann)
50
+
51
+
52
+ def _instantiate_dtype(
53
+ dtype: Any, py_type: Any, spec: Any, cls_name: str, col_name: str
54
+ ):
55
+ """
56
+ Create a SQLAlchemy TypeEngine instance from either a type CLASS or an instance.
57
+ - SAEnum: instantiate from the actual Enum class with a stable name
58
+ - String: honor FieldSpec.constraints['max_length'] if present
59
+ - UUID (PG): prefer as_uuid=True when available
60
+ """
61
+ # Already an instance? keep it.
62
+ try:
63
+ from sqlalchemy.sql.type_api import TypeEngine
64
+
65
+ if isinstance(dtype, TypeEngine):
66
+ return dtype
67
+ except Exception:
68
+ pass
69
+
70
+ # SAEnum from a Python Enum class
71
+ if dtype is SAEnum and isinstance(py_type, type) and issubclass(py_type, PyEnum):
72
+ enum_name = f"{cls_name.lower()}_{col_name.lower()}"
73
+ return SAEnum(py_type, name=enum_name, native_enum=True, validate_strings=True)
74
+
75
+ # String – pick up max_length from FieldSpec
76
+ if dtype is String:
77
+ max_len = getattr(getattr(spec, "field", None), "constraints", {}).get(
78
+ "max_length"
79
+ )
80
+ return String(max_len) if max_len else String()
81
+
82
+ # PostgreSQL UUID (or similar) – try as_uuid=True first
83
+ try:
84
+ return dtype(as_uuid=True) # e.g., PG UUID
85
+ except TypeError:
86
+ try:
87
+ return dtype()
88
+ except TypeError:
89
+ # As a last resort, return the class; SQLA will raise clearly if unusable
90
+ return dtype
91
+
92
+
93
+ def _materialize_colspecs_to_sqla(cls, *, map_columns: bool = True) -> None:
94
+ """
95
+ Replace ColumnSpec attributes with sqlalchemy.orm.mapped_column(...) BEFORE mapping.
96
+ Keep the original specs in __tigrbl_cols__ for downstream builders.
97
+ """
98
+ try:
99
+ from tigrbl_core._spec.column_spec import ColumnSpec
100
+ except Exception:
101
+ return
102
+ try:
103
+ from sqlalchemy.orm import InstrumentedAttribute, MappedColumn
104
+ except Exception: # pragma: no cover - defensive for minimal SQLA envs
105
+ InstrumentedAttribute = None
106
+ MappedColumn = None
107
+
108
+ # Prefer explicit registry if present; otherwise collect specs from the
109
+ # entire MRO so mixins contribute their ColumnSpec definitions.
110
+ specs: dict[str, ColumnSpec] = {}
111
+ for base in reversed(cls.__mro__):
112
+ base_specs = getattr(base, "__tigrbl_cols__", None)
113
+ if isinstance(base_specs, dict) and base_specs:
114
+ specs.update(base_specs)
115
+ continue
116
+ for name, attr in getattr(base, "__dict__", {}).items():
117
+ if isinstance(attr, ColumnSpec):
118
+ specs.setdefault(name, attr)
119
+
120
+ if not specs:
121
+ return
122
+
123
+ if map_columns:
124
+ for name, spec in specs.items():
125
+ storage = getattr(spec, "storage", None)
126
+ if not storage:
127
+ # Virtual (wire-only) column – ensure SQLAlchemy ignores it.
128
+ if MappedColumn is not None and isinstance(spec, MappedColumn):
129
+ annotations = getattr(cls, "__annotations__", {}) or {}
130
+ if name not in annotations:
131
+ replacement = ColumnSpec(
132
+ storage=None,
133
+ field=getattr(spec, "field", None),
134
+ io=getattr(spec, "io", None),
135
+ default_factory=getattr(spec, "default_factory", None),
136
+ read_producer=getattr(spec, "read_producer", None),
137
+ )
138
+ setattr(cls, name, replacement)
139
+ specs[name] = replacement
140
+ continue
141
+ existing_attr = getattr(cls, name, None)
142
+ if InstrumentedAttribute is not None and isinstance(
143
+ existing_attr, InstrumentedAttribute
144
+ ):
145
+ # Column already mapped on a base class; avoid duplicating columns
146
+ # that trigger SQLAlchemy implicit combination warnings.
147
+ continue
148
+
149
+ dtype = getattr(storage, "type_", None)
150
+ if not dtype:
151
+ # No SA dtype specified – cannot materialize
152
+ continue
153
+
154
+ py_type = _infer_py_type(cls, name, spec)
155
+ dtype_inst = _instantiate_dtype(dtype, py_type, spec, cls.__name__, name)
156
+
157
+ # Foreign key (if any)
158
+ fk = getattr(storage, "fk", None)
159
+ fk_arg = None
160
+ if fk is not None:
161
+ # ForeignKeySpec: target="table(col)", on_delete/on_update: "CASCADE"/...
162
+ fk_arg = ForeignKey(
163
+ fk.target, ondelete=fk.on_delete, onupdate=fk.on_update
164
+ )
165
+
166
+ check = getattr(storage, "check", None)
167
+ args: list[Any] = []
168
+ if fk_arg is not None:
169
+ args.append(fk_arg)
170
+ if check is not None:
171
+ cname = f"ck_{cls.__name__.lower()}_{name}"
172
+ args.append(CheckConstraint(check, name=cname))
173
+
174
+ # Build mapped_column from StorageSpec flags
175
+ mc = mapped_column(
176
+ dtype_inst,
177
+ *args,
178
+ primary_key=getattr(storage, "primary_key", False),
179
+ nullable=getattr(storage, "nullable", True),
180
+ unique=getattr(storage, "unique", False),
181
+ index=getattr(storage, "index", False),
182
+ default=getattr(storage, "default", None),
183
+ onupdate=getattr(storage, "onupdate", None),
184
+ server_default=getattr(storage, "server_default", None),
185
+ comment=getattr(storage, "comment", None),
186
+ autoincrement=getattr(storage, "autoincrement", None),
187
+ )
188
+
189
+ setattr(cls, name, mc)
190
+
191
+ # Ensure downstream code can find the spec map
192
+ setattr(cls, "__tigrbl_cols__", dict(specs))
193
+
194
+
195
+ def _ensure_instrumented_attr_accessors() -> None:
196
+ """Expose ColumnSpec metadata on SQLAlchemy InstrumentedAttribute objects."""
197
+ try:
198
+ from sqlalchemy.orm.attributes import InstrumentedAttribute
199
+ except Exception: # pragma: no cover - defensive for minimal SQLA envs
200
+ return
201
+
202
+ if not hasattr(InstrumentedAttribute, "storage"):
203
+
204
+ def _storage(self): # type: ignore[no-untyped-def]
205
+ spec = getattr(self.class_, "__tigrbl_cols__", {}).get(self.key)
206
+ return getattr(spec, "storage", None)
207
+
208
+ InstrumentedAttribute.storage = property(_storage) # type: ignore[attr-defined]
209
+
210
+ if not hasattr(InstrumentedAttribute, "field"):
211
+
212
+ def _field(self): # type: ignore[no-untyped-def]
213
+ spec = getattr(self.class_, "__tigrbl_cols__", {}).get(self.key)
214
+ return getattr(spec, "field", None)
215
+
216
+ InstrumentedAttribute.field = property(_field) # type: ignore[attr-defined]
217
+
218
+ if not hasattr(InstrumentedAttribute, "io"):
219
+
220
+ def _io(self): # type: ignore[no-untyped-def]
221
+ spec = getattr(self.class_, "__tigrbl_cols__", {}).get(self.key)
222
+ return getattr(spec, "io", None)
223
+
224
+ InstrumentedAttribute.io = property(_io) # type: ignore[attr-defined]
225
+
226
+ if not hasattr(InstrumentedAttribute, "default_factory"):
227
+
228
+ def _default_factory(self): # type: ignore[no-untyped-def]
229
+ spec = getattr(self.class_, "__tigrbl_cols__", {}).get(self.key)
230
+ return getattr(spec, "default_factory", None)
231
+
232
+ InstrumentedAttribute.default_factory = property(_default_factory) # type: ignore[attr-defined]
233
+
234
+ if not hasattr(InstrumentedAttribute, "read_producer"):
235
+
236
+ def _read_producer(self): # type: ignore[no-untyped-def]
237
+ spec = getattr(self.class_, "__tigrbl_cols__", {}).get(self.key)
238
+ return getattr(spec, "read_producer", None)
239
+
240
+ InstrumentedAttribute.read_producer = property(_read_producer) # type: ignore[attr-defined]
241
+
242
+
243
+ # ──────────────────────────────────────────────────────────────────────────────
244
+ # Declarative Base
245
+ # ──────────────────────────────────────────────────────────────────────────────
246
+
247
+
248
+ class TableBase(DeclarativeBase):
249
+ __allow_unmapped__ = True
250
+
251
+ def __init_subclass__(cls, **kw):
252
+ # 0) Remove any previously registered class with the same module path.
253
+ try:
254
+ reg = TableBase.registry._class_registry
255
+ name = cls.__name__
256
+ existing = reg.get(name)
257
+ if existing is not None:
258
+ try:
259
+ TableBase.registry._dispose_cls(existing)
260
+ except Exception:
261
+ pass
262
+ reg.pop(name, None)
263
+ module_reg = reg.get("_sa_module_registry")
264
+ if module_reg is not None:
265
+ marker = module_reg
266
+ for part in cls.__module__.split("."):
267
+ contents = getattr(marker, "contents", None)
268
+ if not isinstance(contents, dict) or part not in contents:
269
+ marker = None
270
+ break
271
+ marker = contents.get(part)
272
+ if marker is not None and isinstance(
273
+ getattr(marker, "contents", None), dict
274
+ ):
275
+ marker.contents.pop(name, None)
276
+ except Exception:
277
+ pass
278
+
279
+ # 0.5) If a table with the same name already exists, allow this class
280
+ # to extend it instead of raising duplicate-table errors.
281
+ try:
282
+ table_name = getattr(cls, "__tablename__", None)
283
+ if table_name and table_name in TableBase.metadata.tables:
284
+ table_args = getattr(cls, "__table_args__", None)
285
+ if table_args is None:
286
+ cls.__table_args__ = {"extend_existing": True}
287
+ elif isinstance(table_args, dict):
288
+ table_args = dict(table_args)
289
+ table_args["extend_existing"] = True
290
+ cls.__table_args__ = table_args
291
+ elif isinstance(table_args, tuple):
292
+ if table_args and isinstance(table_args[-1], dict):
293
+ table_dict = dict(table_args[-1])
294
+ table_dict["extend_existing"] = True
295
+ cls.__table_args__ = (*table_args[:-1], table_dict)
296
+ else:
297
+ cls.__table_args__ = (*table_args, {"extend_existing": True})
298
+ except Exception:
299
+ pass
300
+
301
+ # 1) Determine whether this class should be mapped.
302
+ try:
303
+ from sqlalchemy import Column as _SAColumn
304
+ from sqlalchemy.orm import MappedColumn as _MappedColumn
305
+ except Exception: # pragma: no cover - defensive
306
+ _SAColumn = None
307
+ _MappedColumn = None
308
+
309
+ def _has_mappable_columns() -> bool:
310
+ for base in cls.__mro__:
311
+ for attr in getattr(base, "__dict__", {}).values():
312
+ if _SAColumn is not None and isinstance(attr, _SAColumn):
313
+ return True
314
+ if _MappedColumn is not None and isinstance(attr, _MappedColumn):
315
+ return True
316
+ storage = getattr(attr, "storage", None)
317
+ if storage is not None:
318
+ return True
319
+ mapping = getattr(base, "__tigrbl_cols__", None)
320
+ if isinstance(mapping, dict):
321
+ for spec in mapping.values():
322
+ if _MappedColumn is not None and isinstance(
323
+ spec, _MappedColumn
324
+ ):
325
+ return True
326
+ storage = getattr(spec, "storage", None)
327
+ if storage is not None:
328
+ return True
329
+ return False
330
+
331
+ def _has_primary_key() -> bool:
332
+ mapper_args = getattr(cls, "__mapper_args__", None)
333
+ if isinstance(mapper_args, dict) and mapper_args.get("primary_key"):
334
+ return True
335
+ for base in cls.__mro__:
336
+ for attr in getattr(base, "__dict__", {}).values():
337
+ if _SAColumn is not None and isinstance(attr, _SAColumn):
338
+ if getattr(attr, "primary_key", False):
339
+ return True
340
+ if _MappedColumn is not None and isinstance(attr, _MappedColumn):
341
+ if getattr(attr, "primary_key", False):
342
+ return True
343
+ storage = getattr(attr, "storage", None)
344
+ if storage is not None and getattr(storage, "primary_key", False):
345
+ return True
346
+ mapping = getattr(base, "__tigrbl_cols__", None)
347
+ if isinstance(mapping, dict):
348
+ for spec in mapping.values():
349
+ storage = getattr(spec, "storage", None)
350
+ if storage is not None and getattr(
351
+ storage, "primary_key", False
352
+ ):
353
+ return True
354
+ return False
355
+
356
+ explicit_abstract = "__abstract__" in cls.__dict__
357
+ if not explicit_abstract:
358
+ if not _has_mappable_columns() or not _has_primary_key():
359
+ cls.__abstract__ = True
360
+ else:
361
+ cls.__abstract__ = False
362
+
363
+ should_map = not getattr(cls, "__abstract__", False)
364
+
365
+ # 1.5) BEFORE SQLAlchemy maps: turn ColumnSpecs into real mapped_column(...)
366
+ _materialize_colspecs_to_sqla(cls, map_columns=should_map)
367
+ _ensure_instrumented_attr_accessors()
368
+
369
+ # 2) Let SQLAlchemy map the class (PK now exists)
370
+ super().__init_subclass__(**kw)
371
+
372
+ # 2.5) Surface ctx-only op declarations for lightweight introspection.
373
+ if not hasattr(cls, "__tigrbl_ops__"):
374
+ for attr in cls.__dict__.values():
375
+ target = getattr(attr, "__func__", attr)
376
+ if getattr(target, "__tigrbl_op_spec__", None) is not None:
377
+ cls.__tigrbl_ops__ = tuple()
378
+ break
379
+
380
+ # 2.6) Collect response specs declared via @response_ctx.
381
+ responses = {}
382
+ for name, obj in cls.__dict__.items():
383
+ spec = getattr(obj, "__tigrbl_response_spec__", None)
384
+ if spec is None:
385
+ continue
386
+ alias = getattr(obj, "__tigrbl_response_alias__", None) or name
387
+ responses[alias] = spec
388
+ if responses:
389
+ cls.responses = responses
390
+ cls.response = next(iter(responses.values()))
391
+
392
+ # 3) Seed model namespaces / index specs (ops/hooks/etc.) – idempotent
393
+ try:
394
+ from tigrbl_canon.mapping import model as _model_bind
395
+
396
+ _model_bind.bind(cls)
397
+ except Exception:
398
+ pass
399
+
400
+ # 3) AUTO-BUILD CRUD schemas from ColumnSpecs so /docs has them
401
+ try:
402
+ from tigrbl_canon.mapping import build_schemas as _build_schemas
403
+
404
+ _build_schemas(
405
+ cls
406
+ ) # attaches request/response models to the model/registry
407
+ except Exception:
408
+ # Surface during development if needed:
409
+ # raise
410
+ pass
411
+
412
+ metadata = MetaData(
413
+ naming_convention={
414
+ "pk": "pk_%(table_name)s",
415
+ "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
416
+ "ix": "ix_%(table_name)s_%(column_0_name)s",
417
+ "uq": "uq_%(table_name)s_%(column_0_name)s",
418
+ "ck": "ck_%(table_name)s_%(column_0_name)s_%(constraint_type)s",
419
+ }
420
+ )
421
+
422
+ @declared_attr.directive
423
+ def __tablename__(cls) -> str: # noqa: N805
424
+ return cls.__name__.lower()
425
+
426
+ @declared_attr.directive
427
+ def resource_name(cls): # noqa: N805
428
+ return getattr(cls, "__resource__", cls.__name__.lower())
429
+
430
+ def __getitem__(self, key: str) -> Any:
431
+ """Allow dict-style access to model attributes."""
432
+ return getattr(self, key)
433
+
434
+
435
+ __all__ = ["TableBase"]
@@ -0,0 +1,55 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Iterable
4
+
5
+
6
+ class TableRegistryBase(dict[str, Any]):
7
+ """Dict-like registry used for table/model lookups."""
8
+
9
+ def __init__(self, tables: Iterable[Any] = ()) -> None:
10
+ resolved_tables = tuple(tables or ())
11
+ dict.__init__(self)
12
+ self.tables = resolved_tables
13
+ self.register_many(resolved_tables)
14
+
15
+ def __getattr__(self, name: str) -> Any:
16
+ try:
17
+ value = self[name]
18
+ except KeyError as exc:
19
+ raise AttributeError(name) from exc
20
+
21
+ # Preserve historic table-registry attribute behavior where
22
+ # ``registry.Widget`` surfaced SQLAlchemy table metadata while key-based
23
+ # lookup remains model-centric for runtime mapping.
24
+ if isinstance(value, type):
25
+ table = getattr(value, "__table__", None)
26
+ if table is not None:
27
+ return table
28
+ return value
29
+
30
+ def __setattr__(self, name: str, value: Any) -> None:
31
+ if name == "tables" or name.startswith("_"):
32
+ super().__setattr__(name, value)
33
+ return
34
+ self[name] = value
35
+
36
+ def register(self, entry: Any) -> None:
37
+ if isinstance(entry, tuple) and len(entry) == 2 and isinstance(entry[0], str):
38
+ alias, model = entry
39
+ self[alias] = model
40
+ model_name = getattr(model, "__name__", alias)
41
+ self.setdefault(model_name, model)
42
+ return
43
+
44
+ model = entry
45
+ model_name = getattr(model, "__name__", None)
46
+ if model_name is None:
47
+ model_name = str(model)
48
+ self[model_name] = model
49
+
50
+ def register_many(self, tables: Iterable[Any]) -> None:
51
+ for entry in tables:
52
+ self.register(entry)
53
+
54
+
55
+ __all__ = ["TableRegistryBase"]
@@ -0,0 +1,55 @@
1
+ Metadata-Version: 2.4
2
+ Name: tigrbl-base
3
+ Version: 0.1.0.dev1
4
+ Summary: Abstract base interfaces for Tigrbl APIs and runtime components.
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-atoms
19
+ Requires-Dist: tigrbl-canon
20
+ Requires-Dist: tigrbl-concrete
21
+ Requires-Dist: tigrbl-core
22
+ Requires-Dist: tigrbl-runtime
23
+ Requires-Dist: tigrbl-typing
24
+ Description-Content-Type: text/markdown
25
+
26
+ ![Tigrbl branding](https://github.com/swarmauri/swarmauri-sdk/blob/a170683ecda8ca1c4f912c966d4499649ffb8224/assets/tigrbl.brand.theme.svg)
27
+
28
+ # tigrbl-base
29
+
30
+ ![PyPI - Downloads](https://img.shields.io/pypi/dm/tigrbl-base.svg) ![Hits](https://hits.sh/github.com/swarmauri/swarmauri-sdk.svg) ![Python Versions](https://img.shields.io/pypi/pyversions/tigrbl-base.svg) ![License](https://img.shields.io/pypi/l/tigrbl-base.svg) ![Version](https://img.shields.io/pypi/v/tigrbl-base.svg)
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-base
44
+ ```
45
+
46
+ ### pip
47
+
48
+ ```bash
49
+ pip install tigrbl-base
50
+ ```
51
+
52
+ ## Usage
53
+
54
+ Import from the shared package-specific module namespaces after installation in your environment.
55
+
@@ -0,0 +1,16 @@
1
+ tigrbl_base/_base/__init__.py,sha256=ozQWEKKarRB6CgjRAwuVADq9dxKPd8QUblW6Ptq1FQ4,590
2
+ tigrbl_base/_base/_hook_base.py,sha256=bFcG4ghm6oU3LoVGjgXxaD_ngyf-6GZSGBl9RJQvhXo,624
3
+ tigrbl_base/_base/_middleware_base.py,sha256=lv2W9kXUEb0k3-YuFkFEh4sYfNm2lu4xNzFTXTjaxu4,4598
4
+ tigrbl_base/_base/_op_base.py,sha256=4kejZkpj6tTcIfF2JAFNxh49z3lgIhvxn9NBw-pUyhc,230
5
+ tigrbl_base/_base/_request_base.py,sha256=pHHd9IiIG2SkJTTaJHm_66EjvYMPyCukBS8F9-ePM1E,892
6
+ tigrbl_base/_base/_response_base.py,sha256=1u1xla8l9BBTHitR0t9BypYQDpWJ82fLCzTezUk-Tt4,6818
7
+ tigrbl_base/_base/_schema_base.py,sha256=R1Y117j8ZtDmyrJ5GZHKtv2HjAxLvVByM8k_mUDblrk,398
8
+ tigrbl_base/_base/_security_base.py,sha256=LTv3TYeW6WX60KixLsEtIPta5ap4lmqDiSfBB1MP2YE,2284
9
+ tigrbl_base/_base/_session_abc.py,sha256=GxJBBcSXDgE7P_XGouZmMEnezD7fmd9MWU6KK8Qw24g,1028
10
+ tigrbl_base/_base/_session_base.py,sha256=D4HapOTe_mMIp7Z-O8ARWHEbmy1mFgzwhlFA6khBnMM,3879
11
+ tigrbl_base/_base/_storage.py,sha256=Pw8QYjZxL1Sg9IGRTIr6Hn2J9Caz4v-lsQAU0_MyFnw,303
12
+ tigrbl_base/_base/_table_base.py,sha256=zveQbRB1j-nRNGkZc3NOlnZ7u6hD5bzl3tJQLHTTwT0,18134
13
+ tigrbl_base/_base/_table_registry_base.py,sha256=0bkl0g6JFonzaaDiP5dVPxJ8g2LVqq25vyu9JniY1zM,1801
14
+ tigrbl_base-0.1.0.dev1.dist-info/METADATA,sha256=dfBp0JliSd6rJmMSocAm8MC82W6Nfm8QL_KNyqxcWdo,1755
15
+ tigrbl_base-0.1.0.dev1.dist-info/WHEEL,sha256=kJCRJT_g0adfAJzTx2GUMmS80rTJIVHRCfG0DQgLq3o,88
16
+ tigrbl_base-0.1.0.dev1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 2.3.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any