tigrbl 0.0.1.dev1__py3-none-any.whl → 0.3.0.dev2__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.
Files changed (252) hide show
  1. tigrbl/README.md +94 -0
  2. tigrbl/__init__.py +139 -14
  3. tigrbl/api/__init__.py +6 -0
  4. tigrbl/api/_api.py +72 -0
  5. tigrbl/api/api_spec.py +30 -0
  6. tigrbl/api/mro_collect.py +43 -0
  7. tigrbl/api/shortcuts.py +56 -0
  8. tigrbl/api/tigrbl_api.py +286 -0
  9. tigrbl/app/__init__.py +0 -0
  10. tigrbl/app/_app.py +61 -0
  11. tigrbl/app/app_spec.py +42 -0
  12. tigrbl/app/mro_collect.py +67 -0
  13. tigrbl/app/shortcuts.py +65 -0
  14. tigrbl/app/tigrbl_app.py +314 -0
  15. tigrbl/bindings/__init__.py +73 -0
  16. tigrbl/bindings/api/__init__.py +12 -0
  17. tigrbl/bindings/api/common.py +109 -0
  18. tigrbl/bindings/api/include.py +256 -0
  19. tigrbl/bindings/api/resource_proxy.py +149 -0
  20. tigrbl/bindings/api/rpc.py +111 -0
  21. tigrbl/bindings/columns.py +49 -0
  22. tigrbl/bindings/handlers/__init__.py +11 -0
  23. tigrbl/bindings/handlers/builder.py +119 -0
  24. tigrbl/bindings/handlers/ctx.py +74 -0
  25. tigrbl/bindings/handlers/identifiers.py +228 -0
  26. tigrbl/bindings/handlers/namespaces.py +51 -0
  27. tigrbl/bindings/handlers/steps.py +276 -0
  28. tigrbl/bindings/hooks.py +311 -0
  29. tigrbl/bindings/model.py +194 -0
  30. tigrbl/bindings/model_helpers.py +139 -0
  31. tigrbl/bindings/model_registry.py +77 -0
  32. tigrbl/bindings/rest/__init__.py +7 -0
  33. tigrbl/bindings/rest/attach.py +34 -0
  34. tigrbl/bindings/rest/collection.py +265 -0
  35. tigrbl/bindings/rest/common.py +116 -0
  36. tigrbl/bindings/rest/fastapi.py +76 -0
  37. tigrbl/bindings/rest/helpers.py +119 -0
  38. tigrbl/bindings/rest/io.py +317 -0
  39. tigrbl/bindings/rest/member.py +367 -0
  40. tigrbl/bindings/rest/router.py +292 -0
  41. tigrbl/bindings/rest/routing.py +133 -0
  42. tigrbl/bindings/rpc.py +364 -0
  43. tigrbl/bindings/schemas/__init__.py +11 -0
  44. tigrbl/bindings/schemas/builder.py +348 -0
  45. tigrbl/bindings/schemas/defaults.py +260 -0
  46. tigrbl/bindings/schemas/utils.py +193 -0
  47. tigrbl/column/README.md +62 -0
  48. tigrbl/column/__init__.py +72 -0
  49. tigrbl/column/_column.py +96 -0
  50. tigrbl/column/column_spec.py +40 -0
  51. tigrbl/column/field_spec.py +31 -0
  52. tigrbl/column/infer/__init__.py +25 -0
  53. tigrbl/column/infer/core.py +92 -0
  54. tigrbl/column/infer/jsonhints.py +44 -0
  55. tigrbl/column/infer/planning.py +133 -0
  56. tigrbl/column/infer/types.py +102 -0
  57. tigrbl/column/infer/utils.py +59 -0
  58. tigrbl/column/io_spec.py +133 -0
  59. tigrbl/column/mro_collect.py +59 -0
  60. tigrbl/column/shortcuts.py +89 -0
  61. tigrbl/column/storage_spec.py +65 -0
  62. tigrbl/config/__init__.py +19 -0
  63. tigrbl/config/constants.py +224 -0
  64. tigrbl/config/defaults.py +29 -0
  65. tigrbl/config/resolver.py +295 -0
  66. tigrbl/core/__init__.py +47 -0
  67. tigrbl/core/crud/__init__.py +36 -0
  68. tigrbl/core/crud/bulk.py +168 -0
  69. tigrbl/core/crud/helpers/__init__.py +76 -0
  70. tigrbl/core/crud/helpers/db.py +92 -0
  71. tigrbl/core/crud/helpers/enum.py +86 -0
  72. tigrbl/core/crud/helpers/filters.py +162 -0
  73. tigrbl/core/crud/helpers/model.py +123 -0
  74. tigrbl/core/crud/helpers/normalize.py +99 -0
  75. tigrbl/core/crud/ops.py +235 -0
  76. tigrbl/ddl/__init__.py +344 -0
  77. tigrbl/decorators.py +17 -0
  78. tigrbl/deps/__init__.py +20 -0
  79. tigrbl/deps/fastapi.py +45 -0
  80. tigrbl/deps/favicon.svg +4 -0
  81. tigrbl/deps/jinja.py +27 -0
  82. tigrbl/deps/pydantic.py +10 -0
  83. tigrbl/deps/sqlalchemy.py +94 -0
  84. tigrbl/deps/starlette.py +36 -0
  85. tigrbl/engine/__init__.py +26 -0
  86. tigrbl/engine/_engine.py +130 -0
  87. tigrbl/engine/bind.py +33 -0
  88. tigrbl/engine/builders.py +236 -0
  89. tigrbl/engine/collect.py +111 -0
  90. tigrbl/engine/decorators.py +108 -0
  91. tigrbl/engine/engine_spec.py +261 -0
  92. tigrbl/engine/resolver.py +224 -0
  93. tigrbl/engine/shortcuts.py +216 -0
  94. tigrbl/hook/__init__.py +21 -0
  95. tigrbl/hook/_hook.py +22 -0
  96. tigrbl/hook/decorators.py +28 -0
  97. tigrbl/hook/hook_spec.py +24 -0
  98. tigrbl/hook/mro_collect.py +98 -0
  99. tigrbl/hook/shortcuts.py +44 -0
  100. tigrbl/hook/types.py +76 -0
  101. tigrbl/op/__init__.py +50 -0
  102. tigrbl/op/_op.py +31 -0
  103. tigrbl/op/canonical.py +31 -0
  104. tigrbl/op/collect.py +11 -0
  105. tigrbl/op/decorators.py +238 -0
  106. tigrbl/op/model_registry.py +301 -0
  107. tigrbl/op/mro_collect.py +99 -0
  108. tigrbl/op/resolver.py +216 -0
  109. tigrbl/op/types.py +136 -0
  110. tigrbl/orm/__init__.py +1 -0
  111. tigrbl/orm/mixins/_RowBound.py +83 -0
  112. tigrbl/orm/mixins/__init__.py +95 -0
  113. tigrbl/orm/mixins/bootstrappable.py +113 -0
  114. tigrbl/orm/mixins/bound.py +47 -0
  115. tigrbl/orm/mixins/edges.py +40 -0
  116. tigrbl/orm/mixins/fields.py +165 -0
  117. tigrbl/orm/mixins/hierarchy.py +54 -0
  118. tigrbl/orm/mixins/key_digest.py +44 -0
  119. tigrbl/orm/mixins/lifecycle.py +115 -0
  120. tigrbl/orm/mixins/locks.py +51 -0
  121. tigrbl/orm/mixins/markers.py +16 -0
  122. tigrbl/orm/mixins/operations.py +57 -0
  123. tigrbl/orm/mixins/ownable.py +337 -0
  124. tigrbl/orm/mixins/principals.py +98 -0
  125. tigrbl/orm/mixins/tenant_bound.py +301 -0
  126. tigrbl/orm/mixins/upsertable.py +111 -0
  127. tigrbl/orm/mixins/utils.py +49 -0
  128. tigrbl/orm/tables/__init__.py +72 -0
  129. tigrbl/orm/tables/_base.py +8 -0
  130. tigrbl/orm/tables/audit.py +56 -0
  131. tigrbl/orm/tables/client.py +25 -0
  132. tigrbl/orm/tables/group.py +29 -0
  133. tigrbl/orm/tables/org.py +30 -0
  134. tigrbl/orm/tables/rbac.py +76 -0
  135. tigrbl/orm/tables/status.py +106 -0
  136. tigrbl/orm/tables/tenant.py +22 -0
  137. tigrbl/orm/tables/user.py +39 -0
  138. tigrbl/response/README.md +34 -0
  139. tigrbl/response/__init__.py +33 -0
  140. tigrbl/response/bind.py +12 -0
  141. tigrbl/response/decorators.py +37 -0
  142. tigrbl/response/resolver.py +83 -0
  143. tigrbl/response/shortcuts.py +144 -0
  144. tigrbl/response/types.py +49 -0
  145. tigrbl/rest/__init__.py +27 -0
  146. tigrbl/runtime/README.md +129 -0
  147. tigrbl/runtime/__init__.py +20 -0
  148. tigrbl/runtime/atoms/__init__.py +102 -0
  149. tigrbl/runtime/atoms/emit/__init__.py +42 -0
  150. tigrbl/runtime/atoms/emit/paired_post.py +158 -0
  151. tigrbl/runtime/atoms/emit/paired_pre.py +106 -0
  152. tigrbl/runtime/atoms/emit/readtime_alias.py +120 -0
  153. tigrbl/runtime/atoms/out/__init__.py +38 -0
  154. tigrbl/runtime/atoms/out/masking.py +135 -0
  155. tigrbl/runtime/atoms/refresh/__init__.py +38 -0
  156. tigrbl/runtime/atoms/refresh/demand.py +130 -0
  157. tigrbl/runtime/atoms/resolve/__init__.py +40 -0
  158. tigrbl/runtime/atoms/resolve/assemble.py +167 -0
  159. tigrbl/runtime/atoms/resolve/paired_gen.py +147 -0
  160. tigrbl/runtime/atoms/response/__init__.py +17 -0
  161. tigrbl/runtime/atoms/response/negotiate.py +30 -0
  162. tigrbl/runtime/atoms/response/negotiation.py +43 -0
  163. tigrbl/runtime/atoms/response/render.py +36 -0
  164. tigrbl/runtime/atoms/response/renderer.py +116 -0
  165. tigrbl/runtime/atoms/response/template.py +44 -0
  166. tigrbl/runtime/atoms/response/templates.py +88 -0
  167. tigrbl/runtime/atoms/schema/__init__.py +40 -0
  168. tigrbl/runtime/atoms/schema/collect_in.py +21 -0
  169. tigrbl/runtime/atoms/schema/collect_out.py +21 -0
  170. tigrbl/runtime/atoms/storage/__init__.py +38 -0
  171. tigrbl/runtime/atoms/storage/to_stored.py +167 -0
  172. tigrbl/runtime/atoms/wire/__init__.py +45 -0
  173. tigrbl/runtime/atoms/wire/build_in.py +166 -0
  174. tigrbl/runtime/atoms/wire/build_out.py +87 -0
  175. tigrbl/runtime/atoms/wire/dump.py +206 -0
  176. tigrbl/runtime/atoms/wire/validate_in.py +227 -0
  177. tigrbl/runtime/context.py +206 -0
  178. tigrbl/runtime/errors/__init__.py +61 -0
  179. tigrbl/runtime/errors/converters.py +214 -0
  180. tigrbl/runtime/errors/exceptions.py +124 -0
  181. tigrbl/runtime/errors/mappings.py +71 -0
  182. tigrbl/runtime/errors/utils.py +150 -0
  183. tigrbl/runtime/events.py +209 -0
  184. tigrbl/runtime/executor/__init__.py +6 -0
  185. tigrbl/runtime/executor/guards.py +132 -0
  186. tigrbl/runtime/executor/helpers.py +88 -0
  187. tigrbl/runtime/executor/invoke.py +150 -0
  188. tigrbl/runtime/executor/types.py +84 -0
  189. tigrbl/runtime/kernel.py +628 -0
  190. tigrbl/runtime/labels.py +353 -0
  191. tigrbl/runtime/opview.py +87 -0
  192. tigrbl/runtime/ordering.py +256 -0
  193. tigrbl/runtime/system.py +279 -0
  194. tigrbl/runtime/trace.py +330 -0
  195. tigrbl/schema/__init__.py +38 -0
  196. tigrbl/schema/_schema.py +27 -0
  197. tigrbl/schema/builder/__init__.py +17 -0
  198. tigrbl/schema/builder/build_schema.py +209 -0
  199. tigrbl/schema/builder/cache.py +24 -0
  200. tigrbl/schema/builder/compat.py +16 -0
  201. tigrbl/schema/builder/extras.py +85 -0
  202. tigrbl/schema/builder/helpers.py +51 -0
  203. tigrbl/schema/builder/list_params.py +117 -0
  204. tigrbl/schema/builder/strip_parent_fields.py +70 -0
  205. tigrbl/schema/collect.py +55 -0
  206. tigrbl/schema/decorators.py +68 -0
  207. tigrbl/schema/get_schema.py +86 -0
  208. tigrbl/schema/schema_spec.py +20 -0
  209. tigrbl/schema/shortcuts.py +42 -0
  210. tigrbl/schema/types.py +34 -0
  211. tigrbl/schema/utils.py +143 -0
  212. tigrbl/shortcuts.py +22 -0
  213. tigrbl/specs.py +44 -0
  214. tigrbl/system/__init__.py +12 -0
  215. tigrbl/system/diagnostics/__init__.py +24 -0
  216. tigrbl/system/diagnostics/compat.py +31 -0
  217. tigrbl/system/diagnostics/healthz.py +41 -0
  218. tigrbl/system/diagnostics/hookz.py +51 -0
  219. tigrbl/system/diagnostics/kernelz.py +20 -0
  220. tigrbl/system/diagnostics/methodz.py +43 -0
  221. tigrbl/system/diagnostics/router.py +73 -0
  222. tigrbl/system/diagnostics/utils.py +43 -0
  223. tigrbl/table/__init__.py +9 -0
  224. tigrbl/table/_base.py +237 -0
  225. tigrbl/table/_table.py +54 -0
  226. tigrbl/table/mro_collect.py +69 -0
  227. tigrbl/table/shortcuts.py +57 -0
  228. tigrbl/table/table_spec.py +28 -0
  229. tigrbl/transport/__init__.py +74 -0
  230. tigrbl/transport/jsonrpc/__init__.py +19 -0
  231. tigrbl/transport/jsonrpc/dispatcher.py +352 -0
  232. tigrbl/transport/jsonrpc/helpers.py +115 -0
  233. tigrbl/transport/jsonrpc/models.py +41 -0
  234. tigrbl/transport/rest/__init__.py +25 -0
  235. tigrbl/transport/rest/aggregator.py +132 -0
  236. tigrbl/types/__init__.py +174 -0
  237. tigrbl/types/allow_anon_provider.py +19 -0
  238. tigrbl/types/authn_abc.py +30 -0
  239. tigrbl/types/nested_path_provider.py +22 -0
  240. tigrbl/types/op.py +35 -0
  241. tigrbl/types/op_config_provider.py +17 -0
  242. tigrbl/types/op_verb_alias_provider.py +33 -0
  243. tigrbl/types/request_extras_provider.py +22 -0
  244. tigrbl/types/response_extras_provider.py +22 -0
  245. tigrbl/types/table_config_provider.py +13 -0
  246. tigrbl-0.3.0.dev2.dist-info/LICENSE +201 -0
  247. tigrbl-0.3.0.dev2.dist-info/METADATA +501 -0
  248. tigrbl-0.3.0.dev2.dist-info/RECORD +249 -0
  249. tigrbl/ExampleAgent.py +0 -1
  250. tigrbl-0.0.1.dev1.dist-info/METADATA +0 -18
  251. tigrbl-0.0.1.dev1.dist-info/RECORD +0 -5
  252. {tigrbl-0.0.1.dev1.dist-info → tigrbl-0.3.0.dev2.dist-info}/WHEEL +0 -0
@@ -0,0 +1,209 @@
1
+ """Core schema builder logic for Tigrbl v3."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from typing import Any, Dict, Iterable, Set, Tuple, Type, Union
7
+
8
+ from pydantic import AliasChoices, BaseModel, ConfigDict, Field, create_model
9
+
10
+ from ..utils import namely_model
11
+ from ...column.mro_collect import mro_collect_columns
12
+ from .cache import _SchemaCache
13
+ from .extras import _merge_request_extras, _merge_response_extras
14
+ from .helpers import _add_field, _is_required, _python_type
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ def _build_schema(
20
+ orm_cls: type,
21
+ *,
22
+ name: str | None = None,
23
+ include: Set[str] | None = None,
24
+ exclude: Set[str] | None = None,
25
+ verb: str = "create",
26
+ ) -> Type[BaseModel]:
27
+ """Build (and cache) a verb-specific Pydantic schema for *orm_cls*."""
28
+ cache_key = (
29
+ orm_cls,
30
+ verb,
31
+ frozenset(include or ()),
32
+ frozenset(exclude or ()),
33
+ name,
34
+ )
35
+ cached = _SchemaCache.get(cache_key)
36
+ if cached is not None:
37
+ logger.debug("schema: cache hit %s verb=%s", orm_cls.__name__, verb)
38
+ return cached
39
+
40
+ logger.debug(
41
+ "schema: building %s verb=%s include=%s exclude=%s",
42
+ orm_cls.__name__,
43
+ verb,
44
+ include,
45
+ exclude,
46
+ )
47
+ fields: Dict[str, Tuple[type, Field]] = {}
48
+
49
+ # ── PASS 1: table-backed columns only (avoid mapper relationships)
50
+ table = getattr(orm_cls, "__table__", None)
51
+ table_cols: Iterable[Any] = tuple(table.columns) if table is not None else ()
52
+ specs: Dict[str, Any] = mro_collect_columns(orm_cls)
53
+
54
+ for col in table_cols:
55
+ attr_name = col.key or col.name
56
+
57
+ if include and attr_name not in include:
58
+ continue
59
+ if exclude and attr_name in exclude:
60
+ continue
61
+
62
+ spec = specs.get(attr_name)
63
+ io = getattr(spec, "io", None) if spec is not None else None
64
+ if verb in {"create", "update", "replace"}:
65
+ """Determine if the column participates in inbound verbs.
66
+
67
+ When a ColumnSpec is present it may explicitly restrict inbound
68
+ verbs via ``io.in_verbs``. Columns that only declare outbound verbs
69
+ are treated as read-only and omitted from request schemas. If no
70
+ ColumnSpec or ``in_verbs`` is provided we allow all verbs.
71
+ """
72
+
73
+ if getattr(col, "primary_key", False) and verb in {
74
+ "update",
75
+ "replace",
76
+ "delete",
77
+ }:
78
+ # Always expose the PK for mutating operations even when the
79
+ # ColumnSpec omits inbound verbs. The identifier is required so
80
+ # consumers can target the correct row.
81
+ pass
82
+ else:
83
+ if io is not None:
84
+ in_verbs = set(getattr(io, "in_verbs", ()) or ())
85
+ out_verbs = set(getattr(io, "out_verbs", ()) or ())
86
+ if not in_verbs:
87
+ if out_verbs:
88
+ continue
89
+ elif verb not in in_verbs:
90
+ continue
91
+
92
+ logger.debug("schema: processing column %s (verb=%s)", attr_name, verb)
93
+
94
+ # Determine type and requiredness
95
+ py_t = _python_type(col)
96
+ required = _is_required(col, verb)
97
+
98
+ # Field construction (collect kwargs then create Field once)
99
+ fs = getattr(spec, "field", None)
100
+ field_kwargs: Dict[str, Any] = dict(getattr(fs, "constraints", {}) or {})
101
+
102
+ default_factory = getattr(spec, "default_factory", None)
103
+ if default_factory and verb in set(getattr(io, "in_verbs", []) or []):
104
+ field_kwargs["default_factory"] = default_factory
105
+ required = False
106
+ else:
107
+ field_kwargs["default"] = None if not required else ...
108
+
109
+ # IOSpec aliases → Pydantic validation/serialization aliases
110
+ alias_in = getattr(io, "alias_in", None) if io is not None else None
111
+ alias_out = getattr(io, "alias_out", None) if io is not None else None
112
+ if alias_in:
113
+ field_kwargs["validation_alias"] = AliasChoices(alias_in, attr_name)
114
+ if alias_out:
115
+ field_kwargs["serialization_alias"] = alias_out
116
+
117
+ fld = Field(**field_kwargs)
118
+
119
+ # Optional typing if nullable
120
+ is_nullable = bool(getattr(col, "nullable", True))
121
+ if is_nullable and py_t is not Any:
122
+ py_t = Union[py_t, None]
123
+
124
+ # Apply alias mappings for IO specs so that generated Pydantic models
125
+ # accept both the canonical field name and any configured alias. This
126
+ # ensures request payloads can use ``alias_in`` and response models use
127
+ # ``alias_out`` while still normalizing to the canonical attribute name
128
+ # internally.
129
+ if io is not None:
130
+ if verb in {"read", "list"}:
131
+ alias = getattr(io, "alias_out", None)
132
+ else:
133
+ alias = getattr(io, "alias_in", None)
134
+ if alias:
135
+ fld.alias = alias
136
+ fld.serialization_alias = alias
137
+ fld.validation_alias = AliasChoices(attr_name, alias)
138
+
139
+ _add_field(fields, name=attr_name, py_t=py_t, field=fld)
140
+ logger.debug(
141
+ "schema: added field %s required=%s type=%r", attr_name, required, py_t
142
+ )
143
+
144
+ # ── PASS 1b: virtual columns declared via ColumnSpec --------------------
145
+ for attr_name, spec in specs.items():
146
+ if getattr(spec, "storage", None) is not None:
147
+ continue # real columns handled above
148
+ if include and attr_name not in include:
149
+ continue
150
+ if exclude and attr_name in exclude:
151
+ continue
152
+
153
+ io = getattr(spec, "io", None)
154
+ allowed_verbs = set(getattr(io, "in_verbs", ()) or ()) | set(
155
+ getattr(io, "out_verbs", ()) or ()
156
+ )
157
+ if allowed_verbs and verb not in allowed_verbs:
158
+ continue
159
+
160
+ fs = getattr(spec, "field", None)
161
+ py_t = getattr(fs, "py_type", Any) if fs is not None else Any
162
+ required = bool(fs and verb in getattr(fs, "required_in", ()))
163
+ allow_null = bool(fs and verb in getattr(fs, "allow_null_in", ()))
164
+ nullable = bool(getattr(spec, "nullable", True))
165
+ field_kwargs: Dict[str, Any] = dict(getattr(fs, "constraints", {}) or {})
166
+
167
+ default_factory = getattr(spec, "default_factory", None)
168
+ if default_factory and verb in set(getattr(spec.io, "in_verbs", []) or []):
169
+ field_kwargs["default_factory"] = default_factory
170
+ required = False
171
+ else:
172
+ field_kwargs["default"] = None if not required else ...
173
+
174
+ fld = Field(**field_kwargs)
175
+
176
+ if (allow_null or nullable) and py_t is not Any:
177
+ py_t = Union[py_t, None]
178
+
179
+ _add_field(fields, name=attr_name, py_t=py_t, field=fld)
180
+ logger.debug(
181
+ "schema: added virtual field %s required=%s type=%r",
182
+ attr_name,
183
+ required,
184
+ py_t,
185
+ )
186
+
187
+ # ── PASS 2: request/response extras
188
+ _merge_request_extras(orm_cls, verb, fields, include=include, exclude=exclude)
189
+ _merge_response_extras(orm_cls, verb, fields, include=include, exclude=exclude)
190
+
191
+ model_name = name or f"{orm_cls.__name__}{verb.capitalize()}"
192
+ cfg_kwargs = {"from_attributes": True}
193
+ if verb in {"create", "update", "replace"}:
194
+ cfg_kwargs["extra"] = "forbid"
195
+ cfg = ConfigDict(**cfg_kwargs)
196
+
197
+ schema_cls = create_model(model_name, __config__=cfg, **fields) # type: ignore[arg-type]
198
+ schema_cls.model_rebuild(force=True)
199
+ schema_cls = namely_model(
200
+ schema_cls,
201
+ name=model_name,
202
+ doc=f"Tigrbl v3 {orm_cls.__name__} {verb} schema",
203
+ )
204
+ _SchemaCache[cache_key] = schema_cls
205
+ logger.debug("schema: created %s with %d fields", model_name, len(fields))
206
+ return schema_cls
207
+
208
+
209
+ __all__ = ["_build_schema"]
@@ -0,0 +1,24 @@
1
+ """Cache and type definitions for schema builder."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Dict, Tuple, Type, Union, Literal
6
+
7
+ from pydantic import BaseModel
8
+
9
+ _SchemaVerb = Union[
10
+ Literal["create"],
11
+ Literal["read"],
12
+ Literal["update"],
13
+ Literal["replace"],
14
+ Literal["merge"],
15
+ Literal["delete"],
16
+ Literal["list"],
17
+ Literal["clear"],
18
+ ]
19
+
20
+ _SchemaCache: Dict[
21
+ Tuple[type, str, frozenset, frozenset, str | None], Type[BaseModel]
22
+ ] = {}
23
+
24
+ __all__ = ["_SchemaVerb", "_SchemaCache"]
@@ -0,0 +1,16 @@
1
+ """Compatibility utilities for schema builder."""
2
+
3
+ from __future__ import annotations
4
+
5
+ try:
6
+ # Pydantic v2 sentinel for "no default"
7
+ from pydantic_core import PydanticUndefined # type: ignore
8
+ except Exception: # pragma: no cover
9
+
10
+ class PydanticUndefinedClass: # type: ignore
11
+ pass
12
+
13
+ PydanticUndefined = PydanticUndefinedClass() # type: ignore
14
+
15
+
16
+ __all__ = ["PydanticUndefined"]
@@ -0,0 +1,85 @@
1
+ """Support for request/response extras in schema building."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from typing import Any, Dict, Set, Tuple
7
+
8
+ from pydantic import Field
9
+
10
+ from ...config.constants import (
11
+ TIGRBL_REQUEST_EXTRAS_ATTR,
12
+ TIGRBL_RESPONSE_EXTRAS_ATTR,
13
+ )
14
+ from .helpers import _add_field
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ def _merge_request_extras(
20
+ orm_cls: type,
21
+ verb: str,
22
+ fields: Dict[str, Tuple[type, Field]],
23
+ *,
24
+ include: Set[str] | None,
25
+ exclude: Set[str] | None,
26
+ ) -> None:
27
+ """Merge request-only virtual fields into the schema."""
28
+ buckets = getattr(orm_cls, TIGRBL_REQUEST_EXTRAS_ATTR, None)
29
+ if not buckets:
30
+ return
31
+ if verb not in {"create", "update", "replace", "delete"}:
32
+ return
33
+
34
+ for bucket in (buckets.get("*", {}), buckets.get(verb, {})):
35
+ for name, spec in (bucket or {}).items():
36
+ if include and name not in include:
37
+ continue
38
+ if exclude and name in exclude:
39
+ continue
40
+ if isinstance(spec, tuple) and len(spec) == 2:
41
+ py_t, fld = spec
42
+ else:
43
+ py_t, fld = (spec or Any), Field(None)
44
+ _add_field(fields, name=name, py_t=py_t, field=fld)
45
+ logger.debug(
46
+ "schema: added request-extra field %s (verb=%s, type=%r)",
47
+ name,
48
+ verb,
49
+ py_t,
50
+ )
51
+
52
+
53
+ def _merge_response_extras(
54
+ orm_cls: type,
55
+ verb: str,
56
+ fields: Dict[str, Tuple[type, Field]],
57
+ *,
58
+ include: Set[str] | None,
59
+ exclude: Set[str] | None,
60
+ ) -> None:
61
+ """Merge response-only virtual fields into the schema."""
62
+ buckets = getattr(orm_cls, TIGRBL_RESPONSE_EXTRAS_ATTR, None)
63
+ if not buckets:
64
+ return
65
+
66
+ for bucket in (buckets.get("*", {}), buckets.get(verb, {})):
67
+ for name, spec in (bucket or {}).items():
68
+ if include and name not in include:
69
+ continue
70
+ if exclude and name in exclude:
71
+ continue
72
+ if isinstance(spec, tuple) and len(spec) == 2:
73
+ py_t, fld = spec
74
+ else:
75
+ py_t, fld = (spec or Any), Field(None)
76
+ _add_field(fields, name=name, py_t=py_t, field=fld)
77
+ logger.debug(
78
+ "schema: added response-extra field %s (verb=%s, type=%r)",
79
+ name,
80
+ verb,
81
+ py_t,
82
+ )
83
+
84
+
85
+ __all__ = ["_merge_request_extras", "_merge_response_extras"]
@@ -0,0 +1,51 @@
1
+ """Internal helper utilities for schema building."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Dict, Tuple
6
+
7
+ from pydantic import Field
8
+
9
+
10
+ def _bool(x: Any) -> bool:
11
+ try:
12
+ return bool(x)
13
+ except Exception: # pragma: no cover
14
+ return False
15
+
16
+
17
+ def _add_field(
18
+ sink: Dict[str, Tuple[type, Field]],
19
+ *,
20
+ name: str,
21
+ py_t: type | Any,
22
+ field: Field | None = None,
23
+ ) -> None:
24
+ sink[name] = (py_t, field if field is not None else Field(None))
25
+
26
+
27
+ def _python_type(col: Any) -> type | Any:
28
+ try:
29
+ return col.type.python_type
30
+ except Exception: # pragma: no cover
31
+ return Any
32
+
33
+
34
+ def _is_required(col: Any, verb: str) -> bool:
35
+ """Decide if a column should be required for the given verb."""
36
+ if getattr(col, "primary_key", False):
37
+ if verb in {"update", "replace", "delete"}:
38
+ return True
39
+ auto = getattr(col, "autoincrement", False)
40
+ if auto not in (False, None) or getattr(col, "identity", None) is not None:
41
+ return False
42
+ if verb == "update":
43
+ return False
44
+ is_nullable = bool(getattr(col, "nullable", True))
45
+ has_default = (getattr(col, "default", None) is not None) or (
46
+ getattr(col, "server_default", None) is not None
47
+ )
48
+ return not is_nullable and not has_default
49
+
50
+
51
+ __all__ = ["_bool", "_add_field", "_python_type", "_is_required"]
@@ -0,0 +1,117 @@
1
+ """Schema builders for list parameter models."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import uuid
7
+ from typing import Any, Type
8
+
9
+ from pydantic import BaseModel, ConfigDict, Field, create_model
10
+
11
+ from ..utils import namely_model
12
+ from ...column.mro_collect import mro_collect_columns
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ def _build_list_params(model: type) -> Type[BaseModel]:
18
+ """Create a list/filter schema for the given model."""
19
+ tab = model.__name__
20
+ logger.debug("schema: build_list_params for %s", tab)
21
+
22
+ base = dict(
23
+ skip=(int | None, Field(None, ge=0)),
24
+ limit=(int | None, Field(None, ge=10)),
25
+ sort=(str | list[str] | None, Field(None)),
26
+ )
27
+ _scalars = {str, int, float, bool, bytes, uuid.UUID}
28
+ cols: dict[str, tuple[type, Field]] = {}
29
+
30
+ table = getattr(model, "__table__", None)
31
+ if table is None or not getattr(table, "columns", None):
32
+ # No table info; return a minimal pager schema
33
+ schema = create_model(
34
+ f"{tab}ListParams", __config__=ConfigDict(extra="forbid"), **base
35
+ ) # type: ignore[arg-type]
36
+ schema = namely_model(
37
+ schema,
38
+ name=f"{tab}ListParams",
39
+ doc=f"List parameters for {tab}",
40
+ )
41
+ logger.debug(
42
+ "schema: build_list_params generated %s (no columns)", schema.__name__
43
+ )
44
+ return schema
45
+
46
+ pk_name = None
47
+ for c in table.columns:
48
+ if getattr(c, "primary_key", False):
49
+ pk_name = c.name
50
+ break
51
+
52
+ _canon = {
53
+ "eq": "eq",
54
+ "=": "eq",
55
+ "==": "eq",
56
+ "ne": "ne",
57
+ "!=": "ne",
58
+ "<>": "ne",
59
+ "lt": "lt",
60
+ "<": "lt",
61
+ "gt": "gt",
62
+ ">": "gt",
63
+ "lte": "lte",
64
+ "le": "lte",
65
+ "<=": "lte",
66
+ "gte": "gte",
67
+ "ge": "gte",
68
+ ">=": "gte",
69
+ "like": "like",
70
+ "not_like": "not_like",
71
+ "ilike": "ilike",
72
+ "not_ilike": "not_ilike",
73
+ "in": "in",
74
+ "not_in": "not_in",
75
+ }
76
+
77
+ for c in table.columns:
78
+ if pk_name and c.name == pk_name:
79
+ continue
80
+ py_t = getattr(c.type, "python_type", Any)
81
+ if py_t in _scalars:
82
+ spec_map = mro_collect_columns(model)
83
+ spec = spec_map.get(c.name)
84
+ io = getattr(spec, "io", None)
85
+ ops_raw = set(getattr(io, "filter_ops", ()) or [])
86
+ if not ops_raw:
87
+ # Allow basic equality filtering by default on scalar columns
88
+ ops_raw = {"eq"}
89
+ ops = {_canon.get(op, op) for op in ops_raw}
90
+ if "eq" in ops:
91
+ cols[c.name] = (py_t | None, Field(None))
92
+ logger.debug("schema: list filter add %s type=%r", c.name, py_t)
93
+ for op in ops:
94
+ if op == "eq":
95
+ continue
96
+ fname = f"{c.name}__{op}"
97
+ cols[fname] = (py_t | None, Field(None))
98
+ logger.debug(
99
+ "schema: list filter add %s op=%s type=%r", c.name, op, py_t
100
+ )
101
+
102
+ schema = create_model(
103
+ f"{tab}ListParams",
104
+ __config__=ConfigDict(extra="forbid"),
105
+ **base, # type: ignore[arg-type]
106
+ **cols, # type: ignore[arg-type]
107
+ )
108
+ schema = namely_model(
109
+ schema,
110
+ name=f"{tab}ListParams",
111
+ doc=f"List parameters for {tab}",
112
+ )
113
+ logger.debug("schema: build_list_params generated %s", schema.__name__)
114
+ return schema
115
+
116
+
117
+ __all__ = ["_build_list_params"]
@@ -0,0 +1,70 @@
1
+ """Utilities for removing fields from parent schemas."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import inspect
6
+ from typing import (
7
+ Any,
8
+ Dict,
9
+ List,
10
+ Set,
11
+ Tuple,
12
+ Type,
13
+ Union,
14
+ get_args,
15
+ get_origin,
16
+ get_type_hints,
17
+ )
18
+
19
+ from pydantic import BaseModel, create_model
20
+
21
+ from .compat import PydanticUndefined
22
+
23
+
24
+ def _strip_parent_fields(base: Type[BaseModel], *, drop: Set[str]) -> Type[BaseModel]:
25
+ """Return a shallow clone of *base* with selected fields removed."""
26
+ assert issubclass(base, BaseModel), "base must be a Pydantic BaseModel subclass"
27
+
28
+ # RootModel[Union[Model, List[Model]]] – unwrap inner model so we can strip
29
+ # identifiers and return the cleaned schema directly.
30
+ if len(getattr(base, "model_fields", {})) == 1 and "root" in base.model_fields:
31
+ root_ann = base.model_fields["root"].annotation
32
+ if get_origin(root_ann) is Union:
33
+ item_type = None
34
+ for arg in get_args(root_ann):
35
+ origin = get_origin(arg)
36
+ if inspect.isclass(arg) and issubclass(arg, BaseModel):
37
+ item_type = arg
38
+ break
39
+ if origin in (list, List):
40
+ sub = get_args(arg)[0]
41
+ if inspect.isclass(sub) and issubclass(sub, BaseModel):
42
+ item_type = sub
43
+ break
44
+ if item_type is not None:
45
+ return _strip_parent_fields(item_type, drop=drop)
46
+
47
+ hints = get_type_hints(base, include_extras=True)
48
+ new_fields: Dict[str, Tuple[type, Any]] = {}
49
+
50
+ for name, finfo in base.model_fields.items(): # type: ignore[attr-defined]
51
+ if name in (drop or ()): # pragma: no branch
52
+ continue
53
+ typ = hints.get(name, Any)
54
+ default = (
55
+ finfo.default
56
+ if getattr(finfo, "default", PydanticUndefined) is not PydanticUndefined
57
+ else ...
58
+ )
59
+ new_fields[name] = (typ, default)
60
+
61
+ clone = create_model(
62
+ f"{base.__name__}Pruned",
63
+ __config__=getattr(base, "model_config", None),
64
+ **new_fields,
65
+ ) # type: ignore[arg-type]
66
+ clone.model_rebuild(force=True)
67
+ return clone
68
+
69
+
70
+ __all__ = ["_strip_parent_fields"]
@@ -0,0 +1,55 @@
1
+ # tigrbl/v3/schema/collect.py
2
+ from __future__ import annotations
3
+
4
+ import inspect
5
+ import logging
6
+ from functools import lru_cache
7
+ from typing import Dict
8
+
9
+ from ..config.constants import TIGRBL_SCHEMA_DECLS_ATTR
10
+
11
+ from .decorators import _SchemaDecl
12
+
13
+ logging.getLogger("uvicorn").setLevel(logging.DEBUG)
14
+ logger = logging.getLogger("uvicorn")
15
+
16
+
17
+ @lru_cache(maxsize=None)
18
+ def collect_decorated_schemas(model: type) -> Dict[str, Dict[str, type]]:
19
+ """Gather schema declarations for ``model`` across its MRO."""
20
+ logger.info("Collecting decorated schemas for %s", model.__name__)
21
+ out: Dict[str, Dict[str, type]] = {}
22
+
23
+ # Explicit registrations (MRO-merged)
24
+ for base in reversed(model.__mro__):
25
+ mapping: Dict[str, Dict[str, type]] = (
26
+ getattr(base, TIGRBL_SCHEMA_DECLS_ATTR, {}) or {}
27
+ )
28
+ if mapping:
29
+ logger.debug(
30
+ "Found explicit schema mapping on %s: %s", base.__name__, mapping
31
+ )
32
+ for alias, kinds in mapping.items():
33
+ bucket = out.setdefault(alias, {})
34
+ bucket.update(kinds or {})
35
+
36
+ # Nested classes with __tigrbl_schema_decl__
37
+ for base in reversed(model.__mro__):
38
+ for name, obj in base.__dict__.items():
39
+ if not inspect.isclass(obj):
40
+ logger.debug("Skipping non-class attribute %s.%s", base.__name__, name)
41
+ continue
42
+ decl: _SchemaDecl | None = getattr(obj, "__tigrbl_schema_decl__", None)
43
+ if not decl:
44
+ logger.debug(
45
+ "Class %s.%s has no schema declaration", base.__name__, name
46
+ )
47
+ continue
48
+ bucket = out.setdefault(decl.alias, {})
49
+ bucket[decl.kind] = obj
50
+
51
+ logger.debug("Collected schema aliases: %s", list(out.keys()))
52
+ return out
53
+
54
+
55
+ __all__ = ["collect_decorated_schemas"]