tigrbl 0.3.2.dev1__py3-none-any.whl → 0.3.3__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/api/_api.py +5 -0
- tigrbl/api/tigrbl_api.py +6 -0
- tigrbl/app/_app.py +24 -0
- tigrbl/app/mro_collect.py +60 -21
- tigrbl/app/tigrbl_app.py +93 -2
- tigrbl/bindings/model_helpers.py +10 -10
- tigrbl/bindings/rest/collection.py +77 -3
- tigrbl/bindings/rest/member.py +3 -3
- tigrbl/column/field_spec.py +5 -4
- tigrbl/column/shortcuts.py +14 -3
- tigrbl/response/__init__.py +6 -1
- tigrbl/response/decorators.py +8 -0
- tigrbl/schema/builder/build_schema.py +6 -0
- tigrbl/system/diagnostics/hookz.py +1 -2
- tigrbl/system/diagnostics/methodz.py +17 -17
- tigrbl/table/_base.py +226 -58
- tigrbl/table/mro_collect.py +22 -5
- tigrbl/types/__init__.py +1 -0
- {tigrbl-0.3.2.dev1.dist-info → tigrbl-0.3.3.dist-info}/METADATA +1 -1
- {tigrbl-0.3.2.dev1.dist-info → tigrbl-0.3.3.dist-info}/RECORD +22 -22
- {tigrbl-0.3.2.dev1.dist-info → tigrbl-0.3.3.dist-info}/WHEEL +0 -0
- {tigrbl-0.3.2.dev1.dist-info → tigrbl-0.3.3.dist-info}/licenses/LICENSE +0 -0
tigrbl/api/_api.py
CHANGED
|
@@ -26,6 +26,11 @@ class Api(APISpec, ApiRouter):
|
|
|
26
26
|
def __eq__(self, other: object) -> bool: # pragma: no cover - identity compare
|
|
27
27
|
return self is other
|
|
28
28
|
|
|
29
|
+
@property
|
|
30
|
+
def router(self) -> "Api": # pragma: no cover - simple alias
|
|
31
|
+
"""Mirror FastAPI-style router access for API instances."""
|
|
32
|
+
return self
|
|
33
|
+
|
|
29
34
|
def __init__(
|
|
30
35
|
self, *, engine: EngineCfg | None = None, **router_kwargs: Any
|
|
31
36
|
) -> None:
|
tigrbl/api/tigrbl_api.py
CHANGED
|
@@ -62,6 +62,8 @@ class TigrblApi(_Api):
|
|
|
62
62
|
self,
|
|
63
63
|
*,
|
|
64
64
|
engine: EngineCfg | None = None,
|
|
65
|
+
models: Sequence[type] | None = None,
|
|
66
|
+
prefix: str | None = None,
|
|
65
67
|
jsonrpc_prefix: str = "/rpc",
|
|
66
68
|
system_prefix: str = "/system",
|
|
67
69
|
api_hooks: Mapping[str, Iterable[Callable]]
|
|
@@ -69,6 +71,8 @@ class TigrblApi(_Api):
|
|
|
69
71
|
| None = None,
|
|
70
72
|
**router_kwargs: Any,
|
|
71
73
|
) -> None:
|
|
74
|
+
if prefix is not None:
|
|
75
|
+
self.PREFIX = prefix
|
|
72
76
|
_Api.__init__(self, engine=engine, **router_kwargs)
|
|
73
77
|
self.jsonrpc_prefix = jsonrpc_prefix
|
|
74
78
|
self.system_prefix = system_prefix
|
|
@@ -89,6 +93,8 @@ class TigrblApi(_Api):
|
|
|
89
93
|
|
|
90
94
|
# API-level hooks map (merged into each model at include-time; precedence handled in bindings.hooks)
|
|
91
95
|
self._api_hooks_map = copy.deepcopy(api_hooks) if api_hooks else None
|
|
96
|
+
if models:
|
|
97
|
+
self.include_models(list(models))
|
|
92
98
|
|
|
93
99
|
# ------------------------- internal helpers -------------------------
|
|
94
100
|
|
tigrbl/app/_app.py
CHANGED
|
@@ -12,12 +12,36 @@ from .app_spec import AppSpec
|
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class App(AppSpec, FastAPI):
|
|
15
|
+
TITLE = "Tigrbl"
|
|
16
|
+
VERSION = "0.1.0"
|
|
17
|
+
LIFESPAN = None
|
|
18
|
+
MIDDLEWARES = ()
|
|
19
|
+
APIS = ()
|
|
20
|
+
OPS = ()
|
|
21
|
+
MODELS = ()
|
|
22
|
+
SCHEMAS = ()
|
|
23
|
+
HOOKS = ()
|
|
24
|
+
SECURITY_DEPS = ()
|
|
25
|
+
DEPS = ()
|
|
26
|
+
RESPONSE = None
|
|
27
|
+
JSONRPC_PREFIX = "/rpc"
|
|
28
|
+
SYSTEM_PREFIX = "/system"
|
|
29
|
+
|
|
15
30
|
def __init__(
|
|
16
31
|
self, *, engine: EngineCfg | None = None, **fastapi_kwargs: Any
|
|
17
32
|
) -> None:
|
|
18
33
|
# Manually mirror ``AppSpec`` fields so the dataclass-generated ``repr``
|
|
19
34
|
# and friends have expected attributes while runtime structures remain
|
|
20
35
|
# mutable dictionaries or lists as needed.
|
|
36
|
+
title = fastapi_kwargs.pop("title", None)
|
|
37
|
+
if title is not None:
|
|
38
|
+
self.TITLE = title
|
|
39
|
+
version = fastapi_kwargs.pop("version", None)
|
|
40
|
+
if version is not None:
|
|
41
|
+
self.VERSION = version
|
|
42
|
+
lifespan = fastapi_kwargs.pop("lifespan", None)
|
|
43
|
+
if lifespan is not None:
|
|
44
|
+
self.LIFESPAN = lifespan
|
|
21
45
|
self.title = self.TITLE
|
|
22
46
|
self.version = self.VERSION
|
|
23
47
|
self.engine = engine if engine is not None else getattr(self, "ENGINE", None)
|
tigrbl/app/mro_collect.py
CHANGED
|
@@ -9,10 +9,22 @@ from .app_spec import AppSpec
|
|
|
9
9
|
logger = logging.getLogger("uvicorn")
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
def _merge_seq_attr(
|
|
12
|
+
def _merge_seq_attr(
|
|
13
|
+
app: type,
|
|
14
|
+
attr: str,
|
|
15
|
+
*,
|
|
16
|
+
include_inherited: bool = False,
|
|
17
|
+
reverse: bool = False,
|
|
18
|
+
) -> Tuple[Any, ...]:
|
|
13
19
|
values: list[Any] = []
|
|
14
|
-
|
|
15
|
-
|
|
20
|
+
mro = reversed(app.__mro__) if reverse else app.__mro__
|
|
21
|
+
for base in mro:
|
|
22
|
+
if include_inherited:
|
|
23
|
+
if not hasattr(base, attr):
|
|
24
|
+
continue
|
|
25
|
+
seq = getattr(base, attr) or ()
|
|
26
|
+
else:
|
|
27
|
+
seq = base.__dict__.get(attr, ()) or ()
|
|
16
28
|
try:
|
|
17
29
|
values.extend(seq)
|
|
18
30
|
except TypeError: # non-iterable
|
|
@@ -25,30 +37,57 @@ def mro_collect_app_spec(app: type) -> AppSpec:
|
|
|
25
37
|
"""Collect AppSpec-like declarations across the app's MRO."""
|
|
26
38
|
logger.info("Collecting app spec for %s", app.__name__)
|
|
27
39
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
40
|
+
sentinel = object()
|
|
41
|
+
title: Any = sentinel
|
|
42
|
+
version: Any = sentinel
|
|
43
|
+
engine: Any | None = sentinel # type: ignore[assignment]
|
|
44
|
+
response: Any = sentinel
|
|
45
|
+
jsonrpc_prefix: Any = sentinel
|
|
46
|
+
system_prefix: Any = sentinel
|
|
47
|
+
lifespan: Any = sentinel
|
|
35
48
|
|
|
36
|
-
for base in
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
49
|
+
for base in app.__mro__:
|
|
50
|
+
if "TITLE" in base.__dict__ and title is sentinel:
|
|
51
|
+
title = base.__dict__["TITLE"]
|
|
52
|
+
if "VERSION" in base.__dict__ and version is sentinel:
|
|
53
|
+
version = base.__dict__["VERSION"]
|
|
54
|
+
if "ENGINE" in base.__dict__ and engine is sentinel:
|
|
55
|
+
engine = base.__dict__["ENGINE"]
|
|
56
|
+
if "RESPONSE" in base.__dict__ and response is sentinel:
|
|
57
|
+
response = base.__dict__["RESPONSE"]
|
|
58
|
+
if "JSONRPC_PREFIX" in base.__dict__ and jsonrpc_prefix is sentinel:
|
|
59
|
+
jsonrpc_prefix = base.__dict__["JSONRPC_PREFIX"]
|
|
60
|
+
if "SYSTEM_PREFIX" in base.__dict__ and system_prefix is sentinel:
|
|
61
|
+
system_prefix = base.__dict__["SYSTEM_PREFIX"]
|
|
62
|
+
if "LIFESPAN" in base.__dict__ and lifespan is sentinel:
|
|
63
|
+
lifespan = base.__dict__["LIFESPAN"]
|
|
46
64
|
|
|
65
|
+
if title is sentinel:
|
|
66
|
+
title = "Tigrbl"
|
|
67
|
+
if version is sentinel:
|
|
68
|
+
version = "0.1.0"
|
|
69
|
+
if engine is sentinel:
|
|
70
|
+
engine = None
|
|
71
|
+
if response is sentinel:
|
|
72
|
+
response = None
|
|
73
|
+
if jsonrpc_prefix is sentinel:
|
|
74
|
+
jsonrpc_prefix = "/rpc"
|
|
75
|
+
if system_prefix is sentinel:
|
|
76
|
+
system_prefix = "/system"
|
|
77
|
+
if lifespan is sentinel:
|
|
78
|
+
lifespan = None
|
|
79
|
+
|
|
80
|
+
include_inherited_apis = "APIS" not in app.__dict__
|
|
47
81
|
spec = AppSpec(
|
|
48
82
|
title=title,
|
|
49
83
|
version=version,
|
|
50
84
|
engine=engine,
|
|
51
|
-
apis=_merge_seq_attr(
|
|
85
|
+
apis=_merge_seq_attr(
|
|
86
|
+
app,
|
|
87
|
+
"APIS",
|
|
88
|
+
include_inherited=include_inherited_apis,
|
|
89
|
+
reverse=include_inherited_apis,
|
|
90
|
+
),
|
|
52
91
|
ops=_merge_seq_attr(app, "OPS"),
|
|
53
92
|
models=_merge_seq_attr(app, "MODELS"),
|
|
54
93
|
schemas=_merge_seq_attr(app, "SCHEMAS"),
|
tigrbl/app/tigrbl_app.py
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
# tigrbl/v3/app/tigrbl_app.py
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
|
+
import asyncio
|
|
4
5
|
import copy
|
|
6
|
+
import inspect
|
|
5
7
|
from types import SimpleNamespace
|
|
6
8
|
from typing import (
|
|
7
9
|
Any,
|
|
@@ -71,6 +73,7 @@ class TigrblApp(_App):
|
|
|
71
73
|
self,
|
|
72
74
|
*,
|
|
73
75
|
engine: EngineCfg | None = None,
|
|
76
|
+
apis: Sequence[Any] | None = None,
|
|
74
77
|
jsonrpc_prefix: str = "/rpc",
|
|
75
78
|
system_prefix: str = "/system",
|
|
76
79
|
api_hooks: Mapping[str, Iterable[Callable]]
|
|
@@ -106,9 +109,13 @@ class TigrblApp(_App):
|
|
|
106
109
|
self.table_config: Dict[str, Dict[str, Any]] = {}
|
|
107
110
|
self.core = SimpleNamespace()
|
|
108
111
|
self.core_raw = SimpleNamespace()
|
|
112
|
+
self.apis = list(getattr(self, "APIS", ()))
|
|
109
113
|
|
|
110
114
|
# API-level hooks map (merged into each model at include-time; precedence handled in bindings.hooks)
|
|
111
115
|
self._api_hooks_map = copy.deepcopy(api_hooks) if api_hooks else None
|
|
116
|
+
if apis:
|
|
117
|
+
self.apis.extend(list(apis))
|
|
118
|
+
self.include_apis(self.apis)
|
|
112
119
|
|
|
113
120
|
# ------------------------- internal helpers -------------------------
|
|
114
121
|
|
|
@@ -175,6 +182,92 @@ class TigrblApp(_App):
|
|
|
175
182
|
mount_router=mount_router,
|
|
176
183
|
)
|
|
177
184
|
|
|
185
|
+
def include_api(
|
|
186
|
+
self,
|
|
187
|
+
api: Any,
|
|
188
|
+
*,
|
|
189
|
+
prefix: str | None = None,
|
|
190
|
+
mount_router: bool = True,
|
|
191
|
+
) -> Any:
|
|
192
|
+
"""Mount a Tigrbl API router onto this app and track it."""
|
|
193
|
+
if api not in self.apis:
|
|
194
|
+
self.apis.append(api)
|
|
195
|
+
if not mount_router:
|
|
196
|
+
return api
|
|
197
|
+
router = getattr(api, "router", api)
|
|
198
|
+
if hasattr(self, "include_router"):
|
|
199
|
+
self.include_router(router, prefix=prefix or "")
|
|
200
|
+
return api
|
|
201
|
+
|
|
202
|
+
def include_router(self, router: Any, *args: Any, **kwargs: Any) -> None:
|
|
203
|
+
"""Extend FastAPI include_router to track Tigrbl APIs."""
|
|
204
|
+
if hasattr(router, "models") and hasattr(router, "initialize"):
|
|
205
|
+
self.include_api(
|
|
206
|
+
router,
|
|
207
|
+
prefix=kwargs.get("prefix"),
|
|
208
|
+
mount_router=False,
|
|
209
|
+
)
|
|
210
|
+
super().include_router(router, *args, **kwargs)
|
|
211
|
+
|
|
212
|
+
def include_apis(self, apis: Sequence[Any]) -> None:
|
|
213
|
+
"""Mount multiple APIs, supporting optional per-item prefixes."""
|
|
214
|
+
for entry in apis:
|
|
215
|
+
prefix = None
|
|
216
|
+
api = entry
|
|
217
|
+
if isinstance(entry, tuple) and entry:
|
|
218
|
+
api = entry[0]
|
|
219
|
+
if len(entry) > 1:
|
|
220
|
+
value = entry[1]
|
|
221
|
+
if isinstance(value, dict):
|
|
222
|
+
prefix = value.get("prefix")
|
|
223
|
+
elif isinstance(value, str):
|
|
224
|
+
prefix = value
|
|
225
|
+
self.include_api(api, prefix=prefix)
|
|
226
|
+
|
|
227
|
+
def initialize(
|
|
228
|
+
self,
|
|
229
|
+
*,
|
|
230
|
+
schemas: Iterable[str] | None = None,
|
|
231
|
+
sqlite_attachments: Mapping[str, str] | None = None,
|
|
232
|
+
tables: Iterable[Any] | None = None,
|
|
233
|
+
):
|
|
234
|
+
"""Initialize DDL for the app and any attached APIs."""
|
|
235
|
+
result = _ddl_initialize(
|
|
236
|
+
self,
|
|
237
|
+
schemas=schemas,
|
|
238
|
+
sqlite_attachments=sqlite_attachments,
|
|
239
|
+
tables=tables,
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
api_results = []
|
|
243
|
+
for api in self.apis:
|
|
244
|
+
init = getattr(api, "initialize", None)
|
|
245
|
+
if callable(init):
|
|
246
|
+
api_results.append(
|
|
247
|
+
init(
|
|
248
|
+
schemas=schemas,
|
|
249
|
+
sqlite_attachments=sqlite_attachments,
|
|
250
|
+
tables=tables,
|
|
251
|
+
)
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
awaitables = [r for r in [result, *api_results] if inspect.isawaitable(r)]
|
|
255
|
+
if not awaitables:
|
|
256
|
+
return None
|
|
257
|
+
|
|
258
|
+
async def _inner():
|
|
259
|
+
for item in [result, *api_results]:
|
|
260
|
+
if inspect.isawaitable(item):
|
|
261
|
+
await item
|
|
262
|
+
|
|
263
|
+
try:
|
|
264
|
+
loop = asyncio.get_running_loop()
|
|
265
|
+
except RuntimeError:
|
|
266
|
+
asyncio.run(_inner())
|
|
267
|
+
return None
|
|
268
|
+
|
|
269
|
+
return loop.create_task(_inner())
|
|
270
|
+
|
|
178
271
|
async def rpc_call(
|
|
179
272
|
self,
|
|
180
273
|
model_or_name: type | str,
|
|
@@ -311,8 +404,6 @@ class TigrblApp(_App):
|
|
|
311
404
|
tables.append(t)
|
|
312
405
|
return tables
|
|
313
406
|
|
|
314
|
-
initialize = _ddl_initialize
|
|
315
|
-
|
|
316
407
|
# ------------------------- repr -------------------------
|
|
317
408
|
|
|
318
409
|
def __repr__(self) -> str: # pragma: no cover
|
tigrbl/bindings/model_helpers.py
CHANGED
|
@@ -24,40 +24,40 @@ def _ensure_model_namespaces(model: type) -> None:
|
|
|
24
24
|
"""Create top-level namespaces on the model class if missing."""
|
|
25
25
|
|
|
26
26
|
# op indexes & metadata
|
|
27
|
-
if not
|
|
28
|
-
if
|
|
27
|
+
if "ops" not in model.__dict__:
|
|
28
|
+
if "opspecs" in model.__dict__:
|
|
29
29
|
model.ops = model.opspecs
|
|
30
30
|
else:
|
|
31
31
|
model.ops = SimpleNamespace(all=(), by_key={}, by_alias={})
|
|
32
32
|
# Backwards compatibility: older code may still expect `model.opspecs`
|
|
33
33
|
model.opspecs = model.ops
|
|
34
34
|
# pydantic schemas: .<alias>.in_ / .<alias>.out
|
|
35
|
-
if not
|
|
35
|
+
if "schemas" not in model.__dict__:
|
|
36
36
|
model.schemas = SimpleNamespace()
|
|
37
37
|
# hooks: phase chains & raw hook descriptors if you want to expose them
|
|
38
|
-
if not
|
|
38
|
+
if "hooks" not in model.__dict__:
|
|
39
39
|
model.hooks = SimpleNamespace()
|
|
40
40
|
# handlers: .<alias>.raw (core/custom), .<alias>.handler (HANDLER chain entry point)
|
|
41
|
-
if not
|
|
41
|
+
if "handlers" not in model.__dict__:
|
|
42
42
|
model.handlers = SimpleNamespace()
|
|
43
43
|
# rpc: callables to be registered/mounted elsewhere as JSON-RPC methods
|
|
44
|
-
if not
|
|
44
|
+
if "rpc" not in model.__dict__:
|
|
45
45
|
model.rpc = SimpleNamespace()
|
|
46
46
|
# rest: .router (FastAPI Router or compatible) – built in rest binding
|
|
47
|
-
if not
|
|
47
|
+
if "rest" not in model.__dict__:
|
|
48
48
|
model.rest = SimpleNamespace(router=None)
|
|
49
49
|
# basic table metadata for convenience (introspective only; NEVER used for HTTP paths)
|
|
50
|
-
if not
|
|
50
|
+
if "columns" not in model.__dict__:
|
|
51
51
|
table = getattr(model, "__table__", None)
|
|
52
52
|
cols = tuple(getattr(table, "columns", ()) or ())
|
|
53
53
|
model.columns = tuple(
|
|
54
54
|
getattr(c, "name", None) for c in cols if getattr(c, "name", None)
|
|
55
55
|
)
|
|
56
|
-
if not
|
|
56
|
+
if "table_config" not in model.__dict__:
|
|
57
57
|
table = getattr(model, "__table__", None)
|
|
58
58
|
model.table_config = dict(getattr(table, "kwargs", {}) or {})
|
|
59
59
|
# ensure raw hook store exists for decorator merges
|
|
60
|
-
if not
|
|
60
|
+
if "__tigrbl_hooks__" not in model.__dict__:
|
|
61
61
|
setattr(model, "__tigrbl_hooks__", {})
|
|
62
62
|
|
|
63
63
|
|
|
@@ -178,7 +178,79 @@ def _make_collection_endpoint(
|
|
|
178
178
|
_endpoint.__signature__ = inspect.Signature(params)
|
|
179
179
|
else:
|
|
180
180
|
body_model = _request_model_for(sp, model)
|
|
181
|
+
if body_model is None and sp.request_model is None and target == "custom":
|
|
182
|
+
|
|
183
|
+
async def _endpoint(
|
|
184
|
+
request: Request,
|
|
185
|
+
db: Any = Depends(db_dep),
|
|
186
|
+
h: Mapping[str, Any] = Depends(hdr_dep),
|
|
187
|
+
**kw: Any,
|
|
188
|
+
):
|
|
189
|
+
parent_kw = {k: kw[k] for k in nested_vars if k in kw}
|
|
190
|
+
_coerce_parent_kw(model, parent_kw)
|
|
191
|
+
payload: Mapping[str, Any] = dict(parent_kw)
|
|
192
|
+
if isinstance(h, Mapping):
|
|
193
|
+
payload = {**payload, **dict(h)}
|
|
194
|
+
ctx = _ctx(model, alias, target, request, db, payload, parent_kw, api)
|
|
195
|
+
|
|
196
|
+
def _serializer(r, _ctx=ctx):
|
|
197
|
+
out = _serialize_output(model, alias, target, sp, r)
|
|
198
|
+
temp = (
|
|
199
|
+
getattr(_ctx, "temp", {}) if isinstance(_ctx, Mapping) else {}
|
|
200
|
+
)
|
|
201
|
+
extras = (
|
|
202
|
+
temp.get("response_extras", {})
|
|
203
|
+
if isinstance(temp, Mapping)
|
|
204
|
+
else {}
|
|
205
|
+
)
|
|
206
|
+
if isinstance(out, dict) and isinstance(extras, dict):
|
|
207
|
+
out.update(extras)
|
|
208
|
+
return out
|
|
209
|
+
|
|
210
|
+
ctx["response_serializer"] = _serializer
|
|
211
|
+
phases = _get_phase_chains(model, alias)
|
|
212
|
+
result = await _executor._invoke(
|
|
213
|
+
request=request,
|
|
214
|
+
db=db,
|
|
215
|
+
phases=phases,
|
|
216
|
+
ctx=ctx,
|
|
217
|
+
)
|
|
218
|
+
return result
|
|
219
|
+
|
|
220
|
+
_endpoint.__signature__ = _sig(
|
|
221
|
+
nested_vars,
|
|
222
|
+
[
|
|
223
|
+
inspect.Parameter(
|
|
224
|
+
"request",
|
|
225
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
226
|
+
annotation=Request,
|
|
227
|
+
),
|
|
228
|
+
inspect.Parameter(
|
|
229
|
+
"db",
|
|
230
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
231
|
+
annotation=Annotated[Any, Depends(db_dep)],
|
|
232
|
+
),
|
|
233
|
+
inspect.Parameter(
|
|
234
|
+
"h",
|
|
235
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
236
|
+
annotation=Annotated[Mapping[str, Any], Depends(hdr_dep)],
|
|
237
|
+
),
|
|
238
|
+
],
|
|
239
|
+
)
|
|
240
|
+
_endpoint.__name__ = f"rest_{model.__name__}_{alias}_collection"
|
|
241
|
+
_endpoint.__qualname__ = _endpoint.__name__
|
|
242
|
+
_endpoint.__doc__ = (
|
|
243
|
+
f"REST collection endpoint for {model.__name__}.{alias} ({target})"
|
|
244
|
+
)
|
|
245
|
+
return _endpoint
|
|
246
|
+
|
|
181
247
|
base = body_model or Mapping[str, Any]
|
|
248
|
+
body_required = target in {
|
|
249
|
+
"create",
|
|
250
|
+
"update",
|
|
251
|
+
"replace",
|
|
252
|
+
"merge",
|
|
253
|
+
} or target.startswith("bulk_")
|
|
182
254
|
if target.startswith("bulk_"):
|
|
183
255
|
alias_ns = getattr(
|
|
184
256
|
getattr(model, "schemas", None) or SimpleNamespace(), alias, None
|
|
@@ -197,13 +269,13 @@ def _make_collection_endpoint(
|
|
|
197
269
|
_list_ann(Mapping[str, Any]),
|
|
198
270
|
)
|
|
199
271
|
else:
|
|
200
|
-
body_annotation = base
|
|
272
|
+
body_annotation = _union(base, type(None)) if not body_required else base
|
|
201
273
|
|
|
202
274
|
async def _endpoint(
|
|
203
275
|
request: Request,
|
|
204
276
|
db: Any = Depends(db_dep),
|
|
205
277
|
h: Mapping[str, Any] = Depends(hdr_dep),
|
|
206
|
-
body=
|
|
278
|
+
body=None,
|
|
207
279
|
**kw: Any,
|
|
208
280
|
):
|
|
209
281
|
parent_kw = {k: kw[k] for k in nested_vars if k in kw}
|
|
@@ -251,6 +323,7 @@ def _make_collection_endpoint(
|
|
|
251
323
|
return result
|
|
252
324
|
return result
|
|
253
325
|
|
|
326
|
+
body_default = ... if body_required else None
|
|
254
327
|
_endpoint.__signature__ = _sig(
|
|
255
328
|
nested_vars,
|
|
256
329
|
[
|
|
@@ -272,7 +345,8 @@ def _make_collection_endpoint(
|
|
|
272
345
|
inspect.Parameter(
|
|
273
346
|
"body",
|
|
274
347
|
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
275
|
-
annotation=Annotated[body_annotation, Body(
|
|
348
|
+
annotation=Annotated[body_annotation, Body()],
|
|
349
|
+
default=body_default,
|
|
276
350
|
),
|
|
277
351
|
],
|
|
278
352
|
)
|
tigrbl/bindings/rest/member.py
CHANGED
|
@@ -134,7 +134,7 @@ def _make_member_endpoint(
|
|
|
134
134
|
params.extend(
|
|
135
135
|
[
|
|
136
136
|
inspect.Parameter(
|
|
137
|
-
|
|
137
|
+
pk_param,
|
|
138
138
|
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
139
139
|
annotation=Annotated[Any, Path(...)],
|
|
140
140
|
),
|
|
@@ -230,7 +230,7 @@ def _make_member_endpoint(
|
|
|
230
230
|
params.extend(
|
|
231
231
|
[
|
|
232
232
|
inspect.Parameter(
|
|
233
|
-
|
|
233
|
+
pk_param,
|
|
234
234
|
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
235
235
|
annotation=Annotated[Any, Path(...)],
|
|
236
236
|
),
|
|
@@ -350,7 +350,7 @@ def _make_member_endpoint(
|
|
|
350
350
|
params.extend(
|
|
351
351
|
[
|
|
352
352
|
inspect.Parameter(
|
|
353
|
-
|
|
353
|
+
pk_param,
|
|
354
354
|
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
355
355
|
annotation=Annotated[Any, Path(...)],
|
|
356
356
|
),
|
tigrbl/column/field_spec.py
CHANGED
|
@@ -15,16 +15,17 @@ class FieldSpec:
|
|
|
15
15
|
``py_type`` denotes the expected Python type and may be omitted when the
|
|
16
16
|
model attribute is annotated; the type will then be inferred. ``constraints``
|
|
17
17
|
mirrors arguments accepted by :func:`pydantic.Field` and participates in
|
|
18
|
-
schema generation. ``
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
declared type.
|
|
18
|
+
schema generation. ``description`` provides a convenience field for schema
|
|
19
|
+
metadata. ``required_in`` and ``allow_null_in`` govern which API verbs must
|
|
20
|
+
supply the value or may explicitly send ``null`` in requests. Responses rely
|
|
21
|
+
on Pydantic's built-in encoders based solely on the declared type.
|
|
22
22
|
"""
|
|
23
23
|
|
|
24
24
|
py_type: Any = Any
|
|
25
25
|
|
|
26
26
|
# For request/response schema generation (+ pydantic.Field)
|
|
27
27
|
constraints: Dict[str, Any] = dc_field(default_factory=dict)
|
|
28
|
+
description: str | None = None
|
|
28
29
|
|
|
29
30
|
# Request policy (DB nullability lives in StorageSpec.nullable)
|
|
30
31
|
required_in: Tuple[str, ...] = ()
|
tigrbl/column/shortcuts.py
CHANGED
|
@@ -64,10 +64,21 @@ def makeVirtualColumn(
|
|
|
64
64
|
) -> Column:
|
|
65
65
|
"""Convenience for wire-only virtual columns."""
|
|
66
66
|
if spec is not None:
|
|
67
|
-
if any(
|
|
68
|
-
x is not None for x in (field, io, default_factory, producer, read_producer)
|
|
69
|
-
):
|
|
67
|
+
if any(x is not None for x in (field, io, default_factory)):
|
|
70
68
|
raise ValueError("Provide either spec or individual components, not both.")
|
|
69
|
+
if producer is not None and read_producer is not None:
|
|
70
|
+
raise ValueError(
|
|
71
|
+
"Provide only one of producer= or read_producer=, not both."
|
|
72
|
+
)
|
|
73
|
+
rp = read_producer or producer
|
|
74
|
+
if rp is not None:
|
|
75
|
+
spec = ColumnSpec(
|
|
76
|
+
storage=spec.storage,
|
|
77
|
+
field=spec.field,
|
|
78
|
+
io=spec.io,
|
|
79
|
+
default_factory=spec.default_factory,
|
|
80
|
+
read_producer=rp,
|
|
81
|
+
)
|
|
71
82
|
return Column(spec=spec, **kw)
|
|
72
83
|
if producer is not None and read_producer is not None:
|
|
73
84
|
raise ValueError("Provide only one of producer= or read_producer=, not both.")
|
tigrbl/response/__init__.py
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
from .decorators import
|
|
1
|
+
from .decorators import (
|
|
2
|
+
response_ctx,
|
|
3
|
+
get_attached_response_spec,
|
|
4
|
+
get_attached_response_alias,
|
|
5
|
+
)
|
|
2
6
|
from .types import (
|
|
3
7
|
Response,
|
|
4
8
|
ResponseKind,
|
|
@@ -14,6 +18,7 @@ from ..runtime.atoms.response.templates import render_template
|
|
|
14
18
|
__all__ = [
|
|
15
19
|
"response_ctx",
|
|
16
20
|
"get_attached_response_spec",
|
|
21
|
+
"get_attached_response_alias",
|
|
17
22
|
"ResponseSpec",
|
|
18
23
|
"ResponseKind",
|
|
19
24
|
"TemplateSpec",
|
tigrbl/response/decorators.py
CHANGED
|
@@ -5,6 +5,7 @@ from .types import ResponseSpec
|
|
|
5
5
|
|
|
6
6
|
T = TypeVar("T")
|
|
7
7
|
_ATTR = "__tigrbl_response_spec__"
|
|
8
|
+
_ALIAS_ATTR = "__tigrbl_response_alias__"
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
def _to_spec(spec: Optional[ResponseSpec] = None, **kwargs: Any) -> ResponseSpec:
|
|
@@ -24,10 +25,13 @@ def response_ctx(**kwargs: Any) -> Callable[[T], T]: ...
|
|
|
24
25
|
|
|
25
26
|
|
|
26
27
|
def response_ctx(*args: Any, **kwargs: Any) -> Callable[[T], T]:
|
|
28
|
+
alias = kwargs.pop("alias", None)
|
|
27
29
|
spec = _to_spec(*args, **kwargs)
|
|
28
30
|
|
|
29
31
|
def decorator(target: T) -> T:
|
|
30
32
|
setattr(target, _ATTR, spec)
|
|
33
|
+
if alias is not None:
|
|
34
|
+
setattr(target, _ALIAS_ATTR, alias)
|
|
31
35
|
return target
|
|
32
36
|
|
|
33
37
|
return decorator
|
|
@@ -35,3 +39,7 @@ def response_ctx(*args: Any, **kwargs: Any) -> Callable[[T], T]:
|
|
|
35
39
|
|
|
36
40
|
def get_attached_response_spec(obj: Any) -> Optional[ResponseSpec]:
|
|
37
41
|
return getattr(obj, _ATTR, None)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def get_attached_response_alias(obj: Any) -> Optional[str]:
|
|
45
|
+
return getattr(obj, _ALIAS_ATTR, None)
|
|
@@ -98,6 +98,9 @@ def _build_schema(
|
|
|
98
98
|
# Field construction (collect kwargs then create Field once)
|
|
99
99
|
fs = getattr(spec, "field", None)
|
|
100
100
|
field_kwargs: Dict[str, Any] = dict(getattr(fs, "constraints", {}) or {})
|
|
101
|
+
description = getattr(fs, "description", None)
|
|
102
|
+
if description and "description" not in field_kwargs:
|
|
103
|
+
field_kwargs["description"] = description
|
|
101
104
|
|
|
102
105
|
default_factory = getattr(spec, "default_factory", None)
|
|
103
106
|
if default_factory and verb in set(getattr(io, "in_verbs", []) or []):
|
|
@@ -163,6 +166,9 @@ def _build_schema(
|
|
|
163
166
|
allow_null = bool(fs and verb in getattr(fs, "allow_null_in", ()))
|
|
164
167
|
nullable = bool(getattr(spec, "nullable", True))
|
|
165
168
|
field_kwargs: Dict[str, Any] = dict(getattr(fs, "constraints", {}) or {})
|
|
169
|
+
description = getattr(fs, "description", None)
|
|
170
|
+
if description and "description" not in field_kwargs:
|
|
171
|
+
field_kwargs["description"] = description
|
|
166
172
|
|
|
167
173
|
default_factory = getattr(spec, "default_factory", None)
|
|
168
174
|
if default_factory and verb in set(getattr(spec.io, "in_verbs", []) or []):
|
|
@@ -15,29 +15,29 @@ def build_methodz_endpoint(api: Any):
|
|
|
15
15
|
from . import _model_iter, _opspecs
|
|
16
16
|
|
|
17
17
|
methods: List[Dict[str, Any]] = []
|
|
18
|
+
per_model: Dict[str, List[Dict[str, Any]]] = {}
|
|
18
19
|
for model in _model_iter(api):
|
|
19
20
|
mname = getattr(model, "__name__", "Model")
|
|
20
21
|
for sp in _opspecs(model):
|
|
21
22
|
if not getattr(sp, "expose_rpc", True):
|
|
22
23
|
continue
|
|
23
|
-
|
|
24
|
-
{
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
)
|
|
24
|
+
entry = {
|
|
25
|
+
"method": f"{mname}.{sp.alias}",
|
|
26
|
+
"model": mname,
|
|
27
|
+
"alias": sp.alias,
|
|
28
|
+
"target": sp.target,
|
|
29
|
+
"arity": sp.arity,
|
|
30
|
+
"persist": sp.persist,
|
|
31
|
+
"request_model": getattr(sp, "request_model", None) is not None,
|
|
32
|
+
"response_model": getattr(sp, "response_model", None) is not None,
|
|
33
|
+
"routes": bool(getattr(sp, "expose_routes", True)),
|
|
34
|
+
"rpc": bool(getattr(sp, "expose_rpc", True)),
|
|
35
|
+
"tags": list(getattr(sp, "tags", ()) or (mname,)),
|
|
36
|
+
}
|
|
37
|
+
methods.append(entry)
|
|
38
|
+
per_model.setdefault(mname, []).append(entry)
|
|
39
39
|
methods.sort(key=lambda x: (x["model"], x["alias"]))
|
|
40
|
-
cache = {"methods": methods}
|
|
40
|
+
cache = {"methods": methods, **per_model}
|
|
41
41
|
return cache
|
|
42
42
|
|
|
43
43
|
return _methodz
|
tigrbl/table/_base.py
CHANGED
|
@@ -90,7 +90,7 @@ def _instantiate_dtype(
|
|
|
90
90
|
return dtype
|
|
91
91
|
|
|
92
92
|
|
|
93
|
-
def _materialize_colspecs_to_sqla(cls) -> None:
|
|
93
|
+
def _materialize_colspecs_to_sqla(cls, *, map_columns: bool = True) -> None:
|
|
94
94
|
"""
|
|
95
95
|
Replace ColumnSpec attributes with sqlalchemy.orm.mapped_column(...) BEFORE mapping.
|
|
96
96
|
Keep the original specs in __tigrbl_cols__ for downstream builders.
|
|
@@ -100,9 +100,10 @@ def _materialize_colspecs_to_sqla(cls) -> None:
|
|
|
100
100
|
except Exception:
|
|
101
101
|
return
|
|
102
102
|
try:
|
|
103
|
-
from sqlalchemy.orm import InstrumentedAttribute
|
|
103
|
+
from sqlalchemy.orm import InstrumentedAttribute, MappedColumn
|
|
104
104
|
except Exception: # pragma: no cover - defensive for minimal SQLA envs
|
|
105
105
|
InstrumentedAttribute = None
|
|
106
|
+
MappedColumn = None
|
|
106
107
|
|
|
107
108
|
# Prefer explicit registry if present; otherwise collect specs from the
|
|
108
109
|
# entire MRO so mixins contribute their ColumnSpec definitions.
|
|
@@ -119,61 +120,124 @@ def _materialize_colspecs_to_sqla(cls) -> None:
|
|
|
119
120
|
if not specs:
|
|
120
121
|
return
|
|
121
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
|
+
|
|
122
191
|
# Ensure downstream code can find the spec map
|
|
123
192
|
setattr(cls, "__tigrbl_cols__", dict(specs))
|
|
124
193
|
|
|
125
|
-
for name, spec in specs.items():
|
|
126
|
-
storage = getattr(spec, "storage", None)
|
|
127
|
-
if not storage:
|
|
128
|
-
# Virtual (wire-only) column – no DB column
|
|
129
|
-
continue
|
|
130
|
-
existing_attr = getattr(cls, name, None)
|
|
131
|
-
if InstrumentedAttribute is not None and isinstance(
|
|
132
|
-
existing_attr, InstrumentedAttribute
|
|
133
|
-
):
|
|
134
|
-
# Column already mapped on a base class; avoid duplicating columns
|
|
135
|
-
# that trigger SQLAlchemy implicit combination warnings.
|
|
136
|
-
continue
|
|
137
194
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
|
142
201
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
# Build mapped_column from StorageSpec flags
|
|
162
|
-
mc = mapped_column(
|
|
163
|
-
dtype_inst,
|
|
164
|
-
*args,
|
|
165
|
-
primary_key=getattr(storage, "primary_key", False),
|
|
166
|
-
nullable=getattr(storage, "nullable", True),
|
|
167
|
-
unique=getattr(storage, "unique", False),
|
|
168
|
-
index=getattr(storage, "index", False),
|
|
169
|
-
default=getattr(storage, "default", None),
|
|
170
|
-
onupdate=getattr(storage, "onupdate", None),
|
|
171
|
-
server_default=getattr(storage, "server_default", None),
|
|
172
|
-
comment=getattr(storage, "comment", None),
|
|
173
|
-
autoincrement=getattr(storage, "autoincrement", None),
|
|
174
|
-
)
|
|
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"):
|
|
175
219
|
|
|
176
|
-
|
|
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]
|
|
177
241
|
|
|
178
242
|
|
|
179
243
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
@@ -188,16 +252,27 @@ class Base(DeclarativeBase):
|
|
|
188
252
|
# 0) Remove any previously registered class with the same module path.
|
|
189
253
|
try:
|
|
190
254
|
reg = Base.registry._class_registry
|
|
191
|
-
|
|
192
|
-
existing = reg.get(
|
|
255
|
+
name = cls.__name__
|
|
256
|
+
existing = reg.get(name)
|
|
193
257
|
if existing is not None:
|
|
194
258
|
try:
|
|
195
259
|
Base.registry._dispose_cls(existing)
|
|
196
260
|
except Exception:
|
|
197
261
|
pass
|
|
198
|
-
reg.pop(
|
|
199
|
-
|
|
200
|
-
|
|
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)
|
|
201
276
|
except Exception:
|
|
202
277
|
pass
|
|
203
278
|
|
|
@@ -223,12 +298,105 @@ class Base(DeclarativeBase):
|
|
|
223
298
|
except Exception:
|
|
224
299
|
pass
|
|
225
300
|
|
|
226
|
-
# 1)
|
|
227
|
-
|
|
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()
|
|
228
368
|
|
|
229
369
|
# 2) Let SQLAlchemy map the class (PK now exists)
|
|
230
370
|
super().__init_subclass__(**kw)
|
|
231
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_decl__", None) is not None:
|
|
377
|
+
cls.__tigrbl_ops__ = tuple()
|
|
378
|
+
break
|
|
379
|
+
|
|
380
|
+
# 2.6) Collect response specs declared via @response_ctx
|
|
381
|
+
try:
|
|
382
|
+
from tigrbl.response import (
|
|
383
|
+
get_attached_response_spec,
|
|
384
|
+
get_attached_response_alias,
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
responses = {}
|
|
388
|
+
for name, obj in cls.__dict__.items():
|
|
389
|
+
spec = get_attached_response_spec(obj)
|
|
390
|
+
if spec is None:
|
|
391
|
+
continue
|
|
392
|
+
alias = get_attached_response_alias(obj) or name
|
|
393
|
+
responses[alias] = spec
|
|
394
|
+
if responses:
|
|
395
|
+
cls.responses = responses
|
|
396
|
+
cls.response = next(iter(responses.values()))
|
|
397
|
+
except Exception:
|
|
398
|
+
pass
|
|
399
|
+
|
|
232
400
|
# 3) Seed model namespaces / index specs (ops/hooks/etc.) – idempotent
|
|
233
401
|
try:
|
|
234
402
|
from tigrbl.bindings import model as _model_bind
|
tigrbl/table/mro_collect.py
CHANGED
|
@@ -26,15 +26,30 @@ def mro_collect_table_spec(model: type) -> TableSpec:
|
|
|
26
26
|
|
|
27
27
|
Merges common spec attributes (OPS, COLUMNS, SCHEMAS, HOOKS, SECURITY_DEPS,
|
|
28
28
|
DEPS) declared on the class or any mixins. Engine bindings declared via
|
|
29
|
-
``table_config``
|
|
30
|
-
|
|
29
|
+
``table_config`` prefer the last inherited binding in the MRO (from
|
|
30
|
+
wrapper classes) and otherwise fall back to the first direct binding.
|
|
31
31
|
"""
|
|
32
32
|
|
|
33
33
|
logger.info("Collecting table spec for %s", model.__name__)
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
direct_engine: Any | None = None
|
|
36
|
+
inherited_engine: Any | None = None
|
|
36
37
|
for base in model.__mro__:
|
|
37
|
-
|
|
38
|
+
if "table_config" in base.__dict__:
|
|
39
|
+
cfg = base.__dict__.get("table_config")
|
|
40
|
+
if isinstance(cfg, Mapping):
|
|
41
|
+
eng = (
|
|
42
|
+
cfg.get("engine")
|
|
43
|
+
or cfg.get("db")
|
|
44
|
+
or cfg.get("database")
|
|
45
|
+
or cfg.get("engine_provider")
|
|
46
|
+
or cfg.get("db_provider")
|
|
47
|
+
)
|
|
48
|
+
if eng is not None and direct_engine is None:
|
|
49
|
+
direct_engine = eng
|
|
50
|
+
continue
|
|
51
|
+
|
|
52
|
+
cfg = getattr(base, "table_config", None)
|
|
38
53
|
if isinstance(cfg, Mapping):
|
|
39
54
|
eng = (
|
|
40
55
|
cfg.get("engine")
|
|
@@ -44,7 +59,9 @@ def mro_collect_table_spec(model: type) -> TableSpec:
|
|
|
44
59
|
or cfg.get("db_provider")
|
|
45
60
|
)
|
|
46
61
|
if eng is not None:
|
|
47
|
-
|
|
62
|
+
inherited_engine = eng
|
|
63
|
+
|
|
64
|
+
engine = inherited_engine if inherited_engine is not None else direct_engine
|
|
48
65
|
|
|
49
66
|
spec = TableSpec(
|
|
50
67
|
model=model,
|
tigrbl/types/__init__.py
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
tigrbl/README.md,sha256=ed4kzQZJ44MkqbWEcFDFVQC6wbuMY7KkFlWmm4YCEgg,3753
|
|
2
2
|
tigrbl/__init__.py,sha256=9Sx1PgBOHRcu5mo001oENxKhCuJEEZjpnyISYJjJOrg,4286
|
|
3
3
|
tigrbl/api/__init__.py,sha256=iU8usWi_Xa5brbyYsFqLV06Ad9vTXFuVsyN7NFyyuY4,137
|
|
4
|
-
tigrbl/api/_api.py,sha256=
|
|
4
|
+
tigrbl/api/_api.py,sha256=tyDVhhfw6J8gkNcjffA9YZh12w42xwmn5rz2O0NNMvI,4009
|
|
5
5
|
tigrbl/api/api_spec.py,sha256=ESSts5TW2pb4a9wU9H6drmKeU7YbZwx_xPaQ54RFuR4,991
|
|
6
6
|
tigrbl/api/mro_collect.py,sha256=V9U62GsBKDJ0R0N-8f2lVAEpjaO7i74LUcNliwnW-Co,1598
|
|
7
7
|
tigrbl/api/shortcuts.py,sha256=_Ha6yQilo8siXQrSd3ALiK6IzFmgL_OePqs5gy9bFRs,1454
|
|
8
|
-
tigrbl/api/tigrbl_api.py,sha256=
|
|
8
|
+
tigrbl/api/tigrbl_api.py,sha256=ZEAj8vTtMU8JA2Ea1sQYC8vQbAYRfZk_xM8Y-3L_Yh0,10914
|
|
9
9
|
tigrbl/app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
-
tigrbl/app/_app.py,sha256=
|
|
10
|
+
tigrbl/app/_app.py,sha256=BXEMETNBK6Wj3wB7U2LhsYhUNftenGcyGMJQLAtLjDE,4229
|
|
11
11
|
tigrbl/app/_model_registry.py,sha256=v6UoDd7hmv5J4DpNEV13BOgIKIcKou10w3zyZVPd-po,1506
|
|
12
12
|
tigrbl/app/app_spec.py,sha256=RzVHibgoIyQQtlQzRtrWDa4pSD7PpWsysgfztJ1tSRY,1474
|
|
13
|
-
tigrbl/app/mro_collect.py,sha256=
|
|
13
|
+
tigrbl/app/mro_collect.py,sha256=W-kOoKGUSW9dtsKaPuMkN5sLBw_0-H8MTqps1KPwPyI,3351
|
|
14
14
|
tigrbl/app/shortcuts.py,sha256=PEMbmwaqW8a3jkcHM5AXsYA2mmuXGFpOTnqlJnUX9Lo,1775
|
|
15
|
-
tigrbl/app/tigrbl_app.py,sha256=
|
|
15
|
+
tigrbl/app/tigrbl_app.py,sha256=xM4tCdW43A-7C7-ne1okp3q2CsQynBWIgydxnZmlDlY,14836
|
|
16
16
|
tigrbl/bindings/__init__.py,sha256=GcM5PTKNLS1Jx__OiAcjLI5yybL40RrA8OzMVPno4_U,2643
|
|
17
17
|
tigrbl/bindings/api/__init__.py,sha256=ayw4ZBCvvUThQm9PXS6TyVkV1MEYY8YwGcyT4HwWZ7c,395
|
|
18
18
|
tigrbl/bindings/api/common.py,sha256=ivTUwC35QKj0-5op_dksHx4vTsf-fWLYJjwHGuFMKcA,3599
|
|
@@ -28,17 +28,17 @@ tigrbl/bindings/handlers/namespaces.py,sha256=zA_QDEVvzX7NPQ7pN1UJwVWmjJPlxeWJYd
|
|
|
28
28
|
tigrbl/bindings/handlers/steps.py,sha256=-Ypdjg_aFxo6vD814B6tfqZL1Dh4bG_Zmd5G6MXDDfE,9617
|
|
29
29
|
tigrbl/bindings/hooks.py,sha256=tr5hI5ByfAHYQB8HClQ7RO1dg4wCFnfD0I1Jyse2AfU,12306
|
|
30
30
|
tigrbl/bindings/model.py,sha256=ZgS-_NAnTJ55XyMJ9YkL2NxDFIm8LCao2XXyEO2-qoM,7553
|
|
31
|
-
tigrbl/bindings/model_helpers.py,sha256=
|
|
31
|
+
tigrbl/bindings/model_helpers.py,sha256=yI8fmHBjiHwwmnPmavZ2r0eNmCk5PpNnj1YKVKdcXvg,4513
|
|
32
32
|
tigrbl/bindings/model_registry.py,sha256=ACMOIiQbyLs-89BwSNjY6iAS6xR8_vUdl0OcuT1p7ro,2424
|
|
33
33
|
tigrbl/bindings/rest/__init__.py,sha256=u3vD4v9bhD8wotjt7e2Wt46pmGOC5BWHo9co9KOYLxQ,193
|
|
34
34
|
tigrbl/bindings/rest/attach.py,sha256=7fGb1El6ZdCBVn3LvEcyFTNYPPeaEaIGLbyAGzM7ebE,1036
|
|
35
|
-
tigrbl/bindings/rest/collection.py,sha256=
|
|
35
|
+
tigrbl/bindings/rest/collection.py,sha256=bRsj5bAVL7iTIgGyEhdOaHVXN_ajaP9uD4iyKvcCEFU,12675
|
|
36
36
|
tigrbl/bindings/rest/common.py,sha256=K9Kpo3fxj90lcrk8lhGZp-NiInDSv4EqO8Td8VeoimU,2545
|
|
37
37
|
tigrbl/bindings/rest/fastapi.py,sha256=eUg2Aj5JCBlsMSBsf0GJF9KueZnGYhUeGHQ9N7-UB6Y,2072
|
|
38
38
|
tigrbl/bindings/rest/helpers.py,sha256=LrYhqokKAqgyXbxDGKIvXkW14xGoBmcSBurr9eyrPHY,3768
|
|
39
39
|
tigrbl/bindings/rest/io.py,sha256=Sl1nFpAuqv9jqw835YxhdEQ43CJ2XPPA29zeUsBxDsM,11107
|
|
40
40
|
tigrbl/bindings/rest/io_headers.py,sha256=ZUqhXfVlQrosnjnIFr5BP1W9fbpGdxafdYB6Dnots8g,1646
|
|
41
|
-
tigrbl/bindings/rest/member.py,sha256=
|
|
41
|
+
tigrbl/bindings/rest/member.py,sha256=DKheZ14A7SGbk8BSNIV41p9Jmf4peMu6K6pOSus6sWs,13078
|
|
42
42
|
tigrbl/bindings/rest/router.py,sha256=ofkC_uLot5L5wfhyr3Vclzxog-5TjAn4Gd5D5haezRM,10877
|
|
43
43
|
tigrbl/bindings/rest/routing.py,sha256=5Wi3fhgLysEcaHc2dzWk1523NcRZFJpcBiMKr4aaxlo,4440
|
|
44
44
|
tigrbl/bindings/rpc.py,sha256=TsRNQ2q6LH7Dn8j_fguKimUOXRKUB8gmdOek7bp8Yjo,13196
|
|
@@ -50,7 +50,7 @@ tigrbl/column/README.md,sha256=TP7kbwSg0t5ZX68iAL0_62yC2cDOmwznIM23jXb4H5E,2169
|
|
|
50
50
|
tigrbl/column/__init__.py,sha256=84Aa9AQbmAGnJrFcH5dF92WBqFllgbZjvoPaFHijOW4,1764
|
|
51
51
|
tigrbl/column/_column.py,sha256=WUDeHCglwrX0lugixzsNvTk34iUrMLPCyIcnBieA0UQ,3219
|
|
52
52
|
tigrbl/column/column_spec.py,sha256=y4GLFGnpvnCbMAOJser2oKYBlN8Ifc0ILF3Rv5V47jY,1405
|
|
53
|
-
tigrbl/column/field_spec.py,sha256=
|
|
53
|
+
tigrbl/column/field_spec.py,sha256=vULVbaqLcM2KjsLAq-jBEo0E7b9jJzieqfrRfgDMEZE,1324
|
|
54
54
|
tigrbl/column/infer/__init__.py,sha256=EBrWe1J50A-xKPE6CIN1SbJ4d27aqwjOQGyNqWOi_Ic,367
|
|
55
55
|
tigrbl/column/infer/core.py,sha256=zd0tlVffsUVYynuY-J5OXQu1OLEk8M3iILhcubyls-0,2559
|
|
56
56
|
tigrbl/column/infer/jsonhints.py,sha256=IBoNQJUKIIpZdwrYfsKQUA1djHC37XPm8mE-crn1qCM,1659
|
|
@@ -59,7 +59,7 @@ tigrbl/column/infer/types.py,sha256=EOyPYKQgpmwVxCapiDlxPwVvEmqsXN8AMOpoRyAzpFQ,
|
|
|
59
59
|
tigrbl/column/infer/utils.py,sha256=J8VcLGA-KXjy3IJ2ROQ1alyfYP9qj8ukb5Z3IMvasi4,1532
|
|
60
60
|
tigrbl/column/io_spec.py,sha256=GkMTkzk_2fOIo8XK_0rbYRW-Re9rCuIfPMpR4dvUyJE,4223
|
|
61
61
|
tigrbl/column/mro_collect.py,sha256=jAmt-trA95xxcs5SOjwA3sxh9J_MAXDlVFSbpTcyla0,2084
|
|
62
|
-
tigrbl/column/shortcuts.py,sha256=
|
|
62
|
+
tigrbl/column/shortcuts.py,sha256=Cr8NlWTCpJxzdHdAo0hZNfTdMR_Tu2RWeu5s7rHZMXI,3042
|
|
63
63
|
tigrbl/column/storage_spec.py,sha256=U19K6QlELQ9tTyxEA5xRWEcjCcIHcb9-iRfDwluPl_8,2396
|
|
64
64
|
tigrbl/config/__init__.py,sha256=DwpSnn0f0I2dAhIMwm1u8MOrvd04syKIc96OFCk-v-o,455
|
|
65
65
|
tigrbl/config/constants.py,sha256=QHFWpMfp8iqK0PZNhFDCatQZjv4kJDbHHGsPb5MqnMw,9856
|
|
@@ -143,9 +143,9 @@ tigrbl/orm/tables/status.py,sha256=Eb73GsibghSqqY_lzhsO6OxQSYvlfRTc0PnO38nDtYw,3
|
|
|
143
143
|
tigrbl/orm/tables/tenant.py,sha256=bTNkg36A1EeRX2hQuRCUr6J78RkFdm0BgUGHNWCnOHI,450
|
|
144
144
|
tigrbl/orm/tables/user.py,sha256=J0ix0hb8UgcCjoAQhSLn-XN8HR8TTt3mmlwH0bAjTJs,789
|
|
145
145
|
tigrbl/response/README.md,sha256=SVfrSPT7RuZMy2JmyxL9S3bOvhGdDsZQANH-grH37FI,1758
|
|
146
|
-
tigrbl/response/__init__.py,sha256=
|
|
146
|
+
tigrbl/response/__init__.py,sha256=P_N0Fr6LNywdzh0sgG9-wbU51Au5soUP2_9HdAtRE3s,884
|
|
147
147
|
tigrbl/response/bind.py,sha256=fj5deXrOBn-jauvW0j2yNdVziOBvPOvbOklFQFeoyYk,293
|
|
148
|
-
tigrbl/response/decorators.py,sha256=
|
|
148
|
+
tigrbl/response/decorators.py,sha256=x48FaZkjZ_xZq_v6tS9fJqVXuVvVB1wg_j95MqnejUY,1203
|
|
149
149
|
tigrbl/response/resolver.py,sha256=ZCcQiBxzZuC_iteOQVcQdLBQmY8wSSJ2HYryb2YzzUA,2532
|
|
150
150
|
tigrbl/response/shortcuts.py,sha256=nihXsZna0wzAH8ML9xxWyvzDM77ge2pR-JD7k8S8byM,4690
|
|
151
151
|
tigrbl/response/types.py,sha256=110etKRHl0jnnhwFPCWwl6DawQ7cOel1yq4lo3r9KIk,1329
|
|
@@ -203,7 +203,7 @@ tigrbl/runtime/trace.py,sha256=67XPisJg7t5jpXghfYRdJK7hES89Jk_TQUYb2u2cDVk,9865
|
|
|
203
203
|
tigrbl/schema/__init__.py,sha256=PM7xsbgM9L98WnVFek8_dJ_Z3V1m6JometlRLjrgu2w,967
|
|
204
204
|
tigrbl/schema/_schema.py,sha256=Xf_50TOtldOa4pYM6pPBYotNqNbpq91xQJP5p8S4DwI,557
|
|
205
205
|
tigrbl/schema/builder/__init__.py,sha256=9RfxmNhGIIm4zqM-iKks_J8fo6SHRBeZznHz_1lbgUk,482
|
|
206
|
-
tigrbl/schema/builder/build_schema.py,sha256=
|
|
206
|
+
tigrbl/schema/builder/build_schema.py,sha256=rD13_mT3oQPlHZElgO4rirtxPKL8EGpyae-1rj5Dbz4,8266
|
|
207
207
|
tigrbl/schema/builder/cache.py,sha256=1OwjjURIpQWRy01JCdzRUzyIUeWWCjLYtmy5XHW8wKE,521
|
|
208
208
|
tigrbl/schema/builder/compat.py,sha256=KdvpV_lYWrWd9qDoGUBRrl5uXAHXMnXrF9cJW8N86pI,402
|
|
209
209
|
tigrbl/schema/builder/extras.py,sha256=t-94VjVULlTDFsRDJ3IBkBUFYkwZbWajjS1JIyTExUM,2526
|
|
@@ -231,16 +231,16 @@ tigrbl/system/__init__.py,sha256=KzuvZppotGl-tcrwxKkD-mjBRHQyaeZ7AzcBTkI34cs,310
|
|
|
231
231
|
tigrbl/system/diagnostics/__init__.py,sha256=QtQfmaE7LreWKH2Zggmh8kIAilzhF1gQYdhNOVAGuCE,717
|
|
232
232
|
tigrbl/system/diagnostics/compat.py,sha256=g3rzIWKPvspdaDRlwnFzcbsmORfbo1Bjwa1VTrf0Bco,990
|
|
233
233
|
tigrbl/system/diagnostics/healthz.py,sha256=hB-FX6DUJYOlQGdF-5Skb0bspO8Hi4IEOFg7AxcsrV0,1319
|
|
234
|
-
tigrbl/system/diagnostics/hookz.py,sha256=
|
|
234
|
+
tigrbl/system/diagnostics/hookz.py,sha256=q8Fl7yPPdcEw2JkZNAEMhxKwd7EezAfdPNmAZt7E7C0,1781
|
|
235
235
|
tigrbl/system/diagnostics/kernelz.py,sha256=MuM7-95yaIu28nZ3KOWYhfbYeb489Ep6ucmRTqL3JMA,433
|
|
236
|
-
tigrbl/system/diagnostics/methodz.py,sha256=
|
|
236
|
+
tigrbl/system/diagnostics/methodz.py,sha256=kYOpKGuhPjJV5bW13-e4jHM4GQelsoc3Vf2L_s1Ado4,1628
|
|
237
237
|
tigrbl/system/diagnostics/router.py,sha256=5RAv6LPoN4luGwIPnYGak66uT9FslYPc-d_GKqc4S8c,1854
|
|
238
238
|
tigrbl/system/diagnostics/utils.py,sha256=qxC8pUNK2lQKh5cGlF-PSFA-cfJFLlAHW0-iEosvPgg,1172
|
|
239
239
|
tigrbl/system/uvicorn.py,sha256=ogvIqfv-1CxAPZ8BADucaNAy_ePsLA3IVIZxmhdfL3A,1526
|
|
240
240
|
tigrbl/table/__init__.py,sha256=yyP9iZBUJh-D7TCy9WQIvMXKL3ztyX-EXdTK4RTE7iw,207
|
|
241
|
-
tigrbl/table/_base.py,sha256=
|
|
241
|
+
tigrbl/table/_base.py,sha256=k2ZEaMCygpoCMFEwOC2L02oe4cR8W3zwn2NInZcIcHI,18170
|
|
242
242
|
tigrbl/table/_table.py,sha256=B7ft2SMnbp3aTWKO44M4EWTHmzFKyQlpdj-3QULRaGk,1740
|
|
243
|
-
tigrbl/table/mro_collect.py,sha256=
|
|
243
|
+
tigrbl/table/mro_collect.py,sha256=PbuSZnUvVbs3NCe2otie7pvcjxeF54hSiVp_VywclSA,2733
|
|
244
244
|
tigrbl/table/shortcuts.py,sha256=-IZAZyMTsiCdKV0w7nq1C2YBsB6iE_uNGJb-PatlO8I,1716
|
|
245
245
|
tigrbl/table/table_spec.py,sha256=dvilrGWX7fVc6ThTbAqJKxxl3r6_MKNFY0cs_wuyvC8,1001
|
|
246
246
|
tigrbl/transport/__init__.py,sha256=Hq2yob_mvMOQdd8Ts04-rzL282rRpIXo2Prortk0fL4,1896
|
|
@@ -250,7 +250,7 @@ tigrbl/transport/jsonrpc/helpers.py,sha256=oyqx36m8n7EofciPVvTEM9Pz1l51zJwsI224A
|
|
|
250
250
|
tigrbl/transport/jsonrpc/models.py,sha256=omtjb-NN8HyWgIZ5tHafEsbwC7f1XlttAFHFA41Xn2k,973
|
|
251
251
|
tigrbl/transport/rest/__init__.py,sha256=AU_twrP0A958FtXvLSf1i60Jn-UZSRUkAZ1Gd2TeYaw,764
|
|
252
252
|
tigrbl/transport/rest/aggregator.py,sha256=V1zDvv1bwpNyt6rUPmEUEV5nORjb5sHU5LJ00m1ybYY,4454
|
|
253
|
-
tigrbl/types/__init__.py,sha256=
|
|
253
|
+
tigrbl/types/__init__.py,sha256=TSmKjMPDqn_AdZEfYKuGPRkBFsT2P7fqC-BSUXRwZlA,4120
|
|
254
254
|
tigrbl/types/allow_anon_provider.py,sha256=5mWvSfk_eCY_o6oMm1gSEqz6cKyJyoZ1-DcVYm0KmpA,565
|
|
255
255
|
tigrbl/types/authn_abc.py,sha256=GtlXkMb59GEEXNEfeRX_ZfNzu-S4hcLsESzBAaPT2Fg,769
|
|
256
256
|
tigrbl/types/nested_path_provider.py,sha256=1z-4Skz_X_hy-XGEAQnNv6vyrfFNsPIvlhBqf457Sjc,609
|
|
@@ -261,7 +261,7 @@ tigrbl/types/request_extras_provider.py,sha256=JOIpzx1PYA2AYYvkMiXrxlwpBLOPD2cQa
|
|
|
261
261
|
tigrbl/types/response_extras_provider.py,sha256=sFB0R3vyUqmpT-o8983hH9FAlOq6-wwNVK6vfuCPHCg,653
|
|
262
262
|
tigrbl/types/table_config_provider.py,sha256=EkfOhy9UDfy7EgiZddw9KIl5tRujRjXJlr4cSk1Rm5k,361
|
|
263
263
|
tigrbl/types/uuid.py,sha256=pD-JrhS0L2GXeJ0Hv_oKzRuiXmxHDTVoMqExO48iqZE,1993
|
|
264
|
-
tigrbl-0.3.
|
|
265
|
-
tigrbl-0.3.
|
|
266
|
-
tigrbl-0.3.
|
|
267
|
-
tigrbl-0.3.
|
|
264
|
+
tigrbl-0.3.3.dist-info/METADATA,sha256=jzA4dOJopmMt5CDrysJvNm2BrJDlnqLGQttYTIHp600,17848
|
|
265
|
+
tigrbl-0.3.3.dist-info/WHEEL,sha256=3ny-bZhpXrU6vSQ1UPG34FoxZBp3lVcvK0LkgUz6VLk,88
|
|
266
|
+
tigrbl-0.3.3.dist-info/licenses/LICENSE,sha256=djUXOlCxLVszShEpZXshZ7v33G-2qIC_j9KXpWKZSzQ,11359
|
|
267
|
+
tigrbl-0.3.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|