tigrbl 0.3.2.dev1__py3-none-any.whl → 0.3.3.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.
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(app: type, attr: str) -> Tuple[Any, ...]:
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
- for base in reversed(app.__mro__):
15
- seq = getattr(base, attr, ()) or ()
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
- title = "Tigrbl"
29
- version = "0.1.0"
30
- engine: Any | None = None
31
- response = None
32
- jsonrpc_prefix = "/rpc"
33
- system_prefix = "/system"
34
- lifespan = None
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 reversed(app.__mro__):
37
- title = getattr(base, "TITLE", title)
38
- version = getattr(base, "VERSION", version)
39
- eng = getattr(base, "ENGINE", None)
40
- if eng is not None:
41
- engine = eng
42
- response = getattr(base, "RESPONSE", response)
43
- jsonrpc_prefix = getattr(base, "JSONRPC_PREFIX", jsonrpc_prefix)
44
- system_prefix = getattr(base, "SYSTEM_PREFIX", system_prefix)
45
- lifespan = getattr(base, "LIFESPAN", lifespan)
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(app, "APIS"),
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
@@ -2,6 +2,7 @@
2
2
  from __future__ import annotations
3
3
 
4
4
  import copy
5
+ import inspect
5
6
  from types import SimpleNamespace
6
7
  from typing import (
7
8
  Any,
@@ -71,6 +72,7 @@ class TigrblApp(_App):
71
72
  self,
72
73
  *,
73
74
  engine: EngineCfg | None = None,
75
+ apis: Sequence[Any] | None = None,
74
76
  jsonrpc_prefix: str = "/rpc",
75
77
  system_prefix: str = "/system",
76
78
  api_hooks: Mapping[str, Iterable[Callable]]
@@ -106,9 +108,13 @@ class TigrblApp(_App):
106
108
  self.table_config: Dict[str, Dict[str, Any]] = {}
107
109
  self.core = SimpleNamespace()
108
110
  self.core_raw = SimpleNamespace()
111
+ self.apis = list(getattr(self, "APIS", ()))
109
112
 
110
113
  # API-level hooks map (merged into each model at include-time; precedence handled in bindings.hooks)
111
114
  self._api_hooks_map = copy.deepcopy(api_hooks) if api_hooks else None
115
+ if apis:
116
+ self.apis.extend(list(apis))
117
+ self.include_apis(self.apis)
112
118
 
113
119
  # ------------------------- internal helpers -------------------------
114
120
 
@@ -175,6 +181,86 @@ class TigrblApp(_App):
175
181
  mount_router=mount_router,
176
182
  )
177
183
 
184
+ def include_api(
185
+ self,
186
+ api: Any,
187
+ *,
188
+ prefix: str | None = None,
189
+ mount_router: bool = True,
190
+ ) -> Any:
191
+ """Mount a Tigrbl API router onto this app and track it."""
192
+ if api not in self.apis:
193
+ self.apis.append(api)
194
+ if not mount_router:
195
+ return api
196
+ router = getattr(api, "router", api)
197
+ if hasattr(self, "include_router"):
198
+ self.include_router(router, prefix=prefix or "")
199
+ return api
200
+
201
+ def include_router(self, router: Any, *args: Any, **kwargs: Any) -> None:
202
+ """Extend FastAPI include_router to track Tigrbl APIs."""
203
+ if hasattr(router, "models") and hasattr(router, "initialize"):
204
+ self.include_api(
205
+ router,
206
+ prefix=kwargs.get("prefix"),
207
+ mount_router=False,
208
+ )
209
+ super().include_router(router, *args, **kwargs)
210
+
211
+ def include_apis(self, apis: Sequence[Any]) -> None:
212
+ """Mount multiple APIs, supporting optional per-item prefixes."""
213
+ for entry in apis:
214
+ prefix = None
215
+ api = entry
216
+ if isinstance(entry, tuple) and entry:
217
+ api = entry[0]
218
+ if len(entry) > 1:
219
+ value = entry[1]
220
+ if isinstance(value, dict):
221
+ prefix = value.get("prefix")
222
+ elif isinstance(value, str):
223
+ prefix = value
224
+ self.include_api(api, prefix=prefix)
225
+
226
+ def initialize(
227
+ self,
228
+ *,
229
+ schemas: Iterable[str] | None = None,
230
+ sqlite_attachments: Mapping[str, str] | None = None,
231
+ tables: Iterable[Any] | None = None,
232
+ ):
233
+ """Initialize DDL for the app and any attached APIs."""
234
+ result = _ddl_initialize(
235
+ self,
236
+ schemas=schemas,
237
+ sqlite_attachments=sqlite_attachments,
238
+ tables=tables,
239
+ )
240
+
241
+ api_results = []
242
+ for api in self.apis:
243
+ init = getattr(api, "initialize", None)
244
+ if callable(init):
245
+ api_results.append(
246
+ init(
247
+ schemas=schemas,
248
+ sqlite_attachments=sqlite_attachments,
249
+ tables=tables,
250
+ )
251
+ )
252
+
253
+ awaitables = [r for r in [result, *api_results] if inspect.isawaitable(r)]
254
+ if not awaitables:
255
+ return None
256
+
257
+ async def _inner():
258
+ for item in [result, *api_results]:
259
+ if inspect.isawaitable(item):
260
+ await item
261
+
262
+ return _inner()
263
+
178
264
  async def rpc_call(
179
265
  self,
180
266
  model_or_name: type | str,
@@ -311,8 +397,6 @@ class TigrblApp(_App):
311
397
  tables.append(t)
312
398
  return tables
313
399
 
314
- initialize = _ddl_initialize
315
-
316
400
  # ------------------------- repr -------------------------
317
401
 
318
402
  def __repr__(self) -> str: # pragma: no cover
@@ -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 hasattr(model, "ops"):
28
- if hasattr(model, "opspecs"):
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 hasattr(model, "schemas"):
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 hasattr(model, "hooks"):
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 hasattr(model, "handlers"):
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 hasattr(model, "rpc"):
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 hasattr(model, "rest"):
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 hasattr(model, "columns"):
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 hasattr(model, "table_config"):
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 hasattr(model, "__tigrbl_hooks__"):
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=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
  )
@@ -134,7 +134,7 @@ def _make_member_endpoint(
134
134
  params.extend(
135
135
  [
136
136
  inspect.Parameter(
137
- "item_id",
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
- "item_id",
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
- "item_id",
353
+ pk_param,
354
354
  inspect.Parameter.POSITIONAL_OR_KEYWORD,
355
355
  annotation=Annotated[Any, Path(...)],
356
356
  ),
@@ -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. ``required_in`` and ``allow_null_in`` govern which API
19
- verbs must supply the value or may explicitly send ``null`` in requests.
20
- Responses rely on Pydantic's built-in encoders based solely on the
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, ...] = ()
@@ -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.")
@@ -1,4 +1,8 @@
1
- from .decorators import response_ctx, get_attached_response_spec
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",
@@ -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 []):
@@ -43,8 +43,7 @@ def build_hookz_endpoint(api: Any):
43
43
  phase_map[ph] = [_label_callable(fn) for fn in steps]
44
44
  if phase_map:
45
45
  model_map[alias] = phase_map
46
- if model_map:
47
- out[mname] = model_map
46
+ out[mname] = model_map
48
47
  cache = out
49
48
  return cache
50
49
 
@@ -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
- methods.append(
24
- {
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)
33
- is not None,
34
- "routes": bool(getattr(sp, "expose_routes", True)),
35
- "rpc": bool(getattr(sp, "expose_rpc", True)),
36
- "tags": list(getattr(sp, "tags", ()) or (mname,)),
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
- dtype = getattr(storage, "type_", None)
139
- if not dtype:
140
- # No SA dtype specified – cannot materialize
141
- continue
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
- py_type = _infer_py_type(cls, name, spec)
144
- dtype_inst = _instantiate_dtype(dtype, py_type, spec, cls.__name__, name)
145
-
146
- # Foreign key (if any)
147
- fk = getattr(storage, "fk", None)
148
- fk_arg = None
149
- if fk is not None:
150
- # ForeignKeySpec: target="table(col)", on_delete/on_update: "CASCADE"/...
151
- fk_arg = ForeignKey(fk.target, ondelete=fk.on_delete, onupdate=fk.on_update)
152
-
153
- check = getattr(storage, "check", None)
154
- args: list[Any] = []
155
- if fk_arg is not None:
156
- args.append(fk_arg)
157
- if check is not None:
158
- cname = f"ck_{cls.__name__.lower()}_{name}"
159
- args.append(CheckConstraint(check, name=cname))
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
- setattr(cls, name, mc)
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
  # ──────────────────────────────────────────────────────────────────────────────
@@ -223,12 +287,105 @@ class Base(DeclarativeBase):
223
287
  except Exception:
224
288
  pass
225
289
 
226
- # 1) BEFORE SQLAlchemy maps: turn ColumnSpecs into real mapped_column(...)
227
- _materialize_colspecs_to_sqla(cls)
290
+ # 1) Determine whether this class should be mapped.
291
+ try:
292
+ from sqlalchemy import Column as _SAColumn
293
+ from sqlalchemy.orm import MappedColumn as _MappedColumn
294
+ except Exception: # pragma: no cover - defensive
295
+ _SAColumn = None
296
+ _MappedColumn = None
297
+
298
+ def _has_mappable_columns() -> bool:
299
+ for base in cls.__mro__:
300
+ for attr in getattr(base, "__dict__", {}).values():
301
+ if _SAColumn is not None and isinstance(attr, _SAColumn):
302
+ return True
303
+ if _MappedColumn is not None and isinstance(attr, _MappedColumn):
304
+ return True
305
+ storage = getattr(attr, "storage", None)
306
+ if storage is not None:
307
+ return True
308
+ mapping = getattr(base, "__tigrbl_cols__", None)
309
+ if isinstance(mapping, dict):
310
+ for spec in mapping.values():
311
+ if _MappedColumn is not None and isinstance(
312
+ spec, _MappedColumn
313
+ ):
314
+ return True
315
+ storage = getattr(spec, "storage", None)
316
+ if storage is not None:
317
+ return True
318
+ return False
319
+
320
+ def _has_primary_key() -> bool:
321
+ mapper_args = getattr(cls, "__mapper_args__", None)
322
+ if isinstance(mapper_args, dict) and mapper_args.get("primary_key"):
323
+ return True
324
+ for base in cls.__mro__:
325
+ for attr in getattr(base, "__dict__", {}).values():
326
+ if _SAColumn is not None and isinstance(attr, _SAColumn):
327
+ if getattr(attr, "primary_key", False):
328
+ return True
329
+ if _MappedColumn is not None and isinstance(attr, _MappedColumn):
330
+ if getattr(attr, "primary_key", False):
331
+ return True
332
+ storage = getattr(attr, "storage", None)
333
+ if storage is not None and getattr(storage, "primary_key", False):
334
+ return True
335
+ mapping = getattr(base, "__tigrbl_cols__", None)
336
+ if isinstance(mapping, dict):
337
+ for spec in mapping.values():
338
+ storage = getattr(spec, "storage", None)
339
+ if storage is not None and getattr(
340
+ storage, "primary_key", False
341
+ ):
342
+ return True
343
+ return False
344
+
345
+ explicit_abstract = "__abstract__" in cls.__dict__
346
+ if not explicit_abstract:
347
+ if not _has_mappable_columns() or not _has_primary_key():
348
+ cls.__abstract__ = True
349
+ else:
350
+ cls.__abstract__ = False
351
+
352
+ should_map = not getattr(cls, "__abstract__", False)
353
+
354
+ # 1.5) BEFORE SQLAlchemy maps: turn ColumnSpecs into real mapped_column(...)
355
+ _materialize_colspecs_to_sqla(cls, map_columns=should_map)
356
+ _ensure_instrumented_attr_accessors()
228
357
 
229
358
  # 2) Let SQLAlchemy map the class (PK now exists)
230
359
  super().__init_subclass__(**kw)
231
360
 
361
+ # 2.5) Surface ctx-only op declarations for lightweight introspection.
362
+ if not hasattr(cls, "__tigrbl_ops__"):
363
+ for attr in cls.__dict__.values():
364
+ target = getattr(attr, "__func__", attr)
365
+ if getattr(target, "__tigrbl_op_decl__", None) is not None:
366
+ cls.__tigrbl_ops__ = tuple()
367
+ break
368
+
369
+ # 2.6) Collect response specs declared via @response_ctx
370
+ try:
371
+ from tigrbl.response import (
372
+ get_attached_response_spec,
373
+ get_attached_response_alias,
374
+ )
375
+
376
+ responses = {}
377
+ for name, obj in cls.__dict__.items():
378
+ spec = get_attached_response_spec(obj)
379
+ if spec is None:
380
+ continue
381
+ alias = get_attached_response_alias(obj) or name
382
+ responses[alias] = spec
383
+ if responses:
384
+ cls.responses = responses
385
+ cls.response = next(iter(responses.values()))
386
+ except Exception:
387
+ pass
388
+
232
389
  # 3) Seed model namespaces / index specs (ops/hooks/etc.) – idempotent
233
390
  try:
234
391
  from tigrbl.bindings import model as _model_bind
@@ -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`` use the same precedence: later classes in the MRO override
30
- earlier ones.
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
- engine: Any | None = None
35
+ direct_engine: Any | None = None
36
+ inherited_engine: Any | None = None
36
37
  for base in model.__mro__:
37
- cfg = base.__dict__.get("table_config")
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
- engine = eng
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
@@ -45,6 +45,7 @@ from ..deps.sqlalchemy import (
45
45
  StaticPool,
46
46
  )
47
47
 
48
+
48
49
  from ..deps.pydantic import (
49
50
  BaseModel,
50
51
  Field,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tigrbl
3
- Version: 0.3.2.dev1
3
+ Version: 0.3.3.dev1
4
4
  Summary: Automatic API generation tools by Swarmauri.
5
5
  License-Expression: Apache-2.0
6
6
  License-File: LICENSE
@@ -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=dMWJfCC8YVrzY7pzGetm0xxBqC7FGuw_QPFeiH1nKP8,3840
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=UKSvSLaG7vzmM5r39iyNWUslUMiN6eWZlJtXyW5K4Ak,10704
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=bsg58JEbnLpjBJvvjLayHKGZvrYL7434tTiLZhx1jyY,3596
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=JfmkAnUGLgVVaevAjVU9O_8JMyJAYiTXTmrxW4EdPx8,2025
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=4WqA509WekXx1BCW7Ph2tqdFEYkzcrHCYJz8MIVjG4w,11817
15
+ tigrbl/app/tigrbl_app.py,sha256=vN1mlMoUy7ENWKBbVGuY_OFjYjvlf3nv5ybELvfGqgI,14656
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=3rbt7YFT2ohBGNsd7nfKnnSnHz1nOmeE_xIyAjubBQQ,4493
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=GK2xD19SXkDSDeVlDiFmgsmT0zj8ssIYFFf91grfSmE,9680
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=aSuvfBWMpGMx-rYK5Zr3Y2ATTBbBkj4IxaZyN0Yh91Q,13081
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=bXG2oioTEF3G5UyXL9MqWo_SGpHz1EJui5HfIoo0FBs,1223
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=a6ns1Y5zdbgzzKeUDZq86ejV2la_HZVHmLSgWucPxKc,2606
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=Y26CVXUlk7zvcP7oorZHxbJ5D0-KC_LcDIxQjw0QlB4,803
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=iOBBME2jSvuS_KV9zDCQKBQDNg80QLFtYboJehwqvRM,940
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=2paI62EZ5_Q00Axnz67R63nSI7HRCqG-BQ5ncosVcDI,7924
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=by1yszQqiktf3LXb_QEEbVO6u64l_8E4bk0_Z7_mSTw,1811
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=rZ9QpNJMxH_gH-zkSv6vK-qw3ZLRLEImP32Ki-JmyC4,1577
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=2PCazG2fUuMCqTOghPsa2W7f2de-e31tX78ckfcVbHw,10456
241
+ tigrbl/table/_base.py,sha256=JKZ6Q_2bxDyedw43HkBtinfJFGzWuH3ISRXWu_cnb4c,17660
242
242
  tigrbl/table/_table.py,sha256=B7ft2SMnbp3aTWKO44M4EWTHmzFKyQlpdj-3QULRaGk,1740
243
- tigrbl/table/mro_collect.py,sha256=JwL0zAeLNUgJcgd2JuLKcbFc806zBguhFqpCkeZyzRw,2027
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=JwQCoSrk3-aPtlfd9dLXO1YAPuLc_CPUa6Uye8frFBg,4119
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.2.dev1.dist-info/METADATA,sha256=_XdyS757AmpJyg3k2bEkaD0p5aY-_DHbCcye2UPscs8,17853
265
- tigrbl-0.3.2.dev1.dist-info/WHEEL,sha256=3ny-bZhpXrU6vSQ1UPG34FoxZBp3lVcvK0LkgUz6VLk,88
266
- tigrbl-0.3.2.dev1.dist-info/licenses/LICENSE,sha256=djUXOlCxLVszShEpZXshZ7v33G-2qIC_j9KXpWKZSzQ,11359
267
- tigrbl-0.3.2.dev1.dist-info/RECORD,,
264
+ tigrbl-0.3.3.dev1.dist-info/METADATA,sha256=CExkdEPpGN2jQMDVYfYSoPO8m1ev6hPqUoTi8IdA6do,17853
265
+ tigrbl-0.3.3.dev1.dist-info/WHEEL,sha256=3ny-bZhpXrU6vSQ1UPG34FoxZBp3lVcvK0LkgUz6VLk,88
266
+ tigrbl-0.3.3.dev1.dist-info/licenses/LICENSE,sha256=djUXOlCxLVszShEpZXshZ7v33G-2qIC_j9KXpWKZSzQ,11359
267
+ tigrbl-0.3.3.dev1.dist-info/RECORD,,