tigrbl 0.0.1.dev1__py3-none-any.whl → 0.3.0.dev3__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.dev3.dist-info/LICENSE +201 -0
  247. tigrbl-0.3.0.dev3.dist-info/METADATA +501 -0
  248. tigrbl-0.3.0.dev3.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.dev3.dist-info}/WHEEL +0 -0
@@ -0,0 +1,168 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict, Iterable, List, Mapping, Union
4
+
5
+ import builtins as _builtins
6
+ import logging
7
+
8
+ from .helpers import (
9
+ AsyncSession,
10
+ Session,
11
+ sa_delete,
12
+ _coerce_pk_value,
13
+ _immutable_columns,
14
+ _maybe_delete,
15
+ _maybe_execute,
16
+ _maybe_flush,
17
+ _maybe_get,
18
+ _set_attrs,
19
+ _single_pk_name,
20
+ _validate_enum_values,
21
+ )
22
+ from .ops import merge, read
23
+
24
+ logging.getLogger("uvicorn").setLevel(logging.DEBUG)
25
+ logger = logging.getLogger("uvicorn")
26
+
27
+
28
+ async def bulk_create(
29
+ model: type, rows: Iterable[Mapping[str, Any]], db: Union[Session, AsyncSession]
30
+ ) -> List[Any]:
31
+ """
32
+ Insert many rows. Returns the list of persisted instances.
33
+ Flush-only.
34
+ """
35
+ logger.debug("bulk_create called with model=%s rows=%s", model, rows)
36
+ items_data = [dict(r) for r in (rows or ())]
37
+ for r in items_data:
38
+ _validate_enum_values(model, r)
39
+ items = [model(**r) for r in items_data]
40
+ if not items:
41
+ logger.debug("bulk_create no items to create")
42
+ return []
43
+ if hasattr(db, "add_all"):
44
+ db.add_all(items) # type: ignore[attr-defined]
45
+ else:
46
+ for it in items:
47
+ db.add(it) # type: ignore[attr-defined]
48
+ await _maybe_flush(db)
49
+ logger.debug("bulk_create persisted %d items", len(items))
50
+ return items
51
+
52
+
53
+ async def bulk_update(
54
+ model: type, rows: Iterable[Mapping[str, Any]], db: Union[Session, AsyncSession]
55
+ ) -> List[Any]:
56
+ """
57
+ Update many rows by PK. Each row must include the PK field.
58
+ Returns the list of updated instances. Flush-only.
59
+ """
60
+ logger.debug("bulk_update called with model=%s rows=%s", model, rows)
61
+ pk = _single_pk_name(model)
62
+ skip = _immutable_columns(model, "update")
63
+ updated: List[Any] = []
64
+ for r in rows or ():
65
+ r = dict(r)
66
+ _validate_enum_values(model, r)
67
+ ident = r.get(pk)
68
+ if ident is None:
69
+ raise ValueError(f"bulk_update requires '{pk}' in each row")
70
+ obj = await read(model, ident, db)
71
+ data = {k: v for k, v in r.items() if k != pk}
72
+ _set_attrs(obj, data, allow_missing=True, skip=skip)
73
+ updated.append(obj)
74
+ if updated:
75
+ await _maybe_flush(db)
76
+ logger.debug("bulk_update updated %d items", len(updated))
77
+ return updated
78
+
79
+
80
+ async def bulk_replace(
81
+ model: type, rows: Iterable[Mapping[str, Any]], db: Union[Session, AsyncSession]
82
+ ) -> List[Any]:
83
+ """
84
+ Replace many rows by PK. Each row must include the PK field.
85
+ Missing attributes are nulled (except PK). Flush-only.
86
+ """
87
+ logger.debug("bulk_replace called with model=%s rows=%s", model, rows)
88
+ pk = _single_pk_name(model)
89
+ skip = _immutable_columns(model, "replace")
90
+ replaced: List[Any] = []
91
+ for r in rows or ():
92
+ r = dict(r)
93
+ _validate_enum_values(model, r)
94
+ ident = r.get(pk)
95
+ if ident is None:
96
+ raise ValueError(f"bulk_replace requires '{pk}' in each row")
97
+ obj = await read(model, ident, db)
98
+ data = {k: v for k, v in r.items() if k != pk}
99
+ _set_attrs(obj, data, allow_missing=False, skip=skip)
100
+ replaced.append(obj)
101
+ if replaced:
102
+ await _maybe_flush(db)
103
+ logger.debug("bulk_replace replaced %d items", len(replaced))
104
+ return replaced
105
+
106
+
107
+ async def bulk_merge(
108
+ model: type, rows: Iterable[Mapping[str, Any]], db: Union[Session, AsyncSession]
109
+ ) -> List[Any]:
110
+ """Merge many rows by primary key with upsert semantics."""
111
+ logger.debug("bulk_merge called with model=%s rows=%s", model, rows)
112
+ pk = _single_pk_name(model)
113
+ results: List[Any] = []
114
+ to_create: List[Mapping[str, Any]] = []
115
+ for r in rows or ():
116
+ r = dict(r)
117
+ ident = _coerce_pk_value(model, r.get(pk))
118
+ if ident is not None:
119
+ existing = await _maybe_get(db, model, ident)
120
+ if existing is not None:
121
+ data = {k: v for k, v in r.items() if k != pk}
122
+ merged = await merge(model, ident, data, db=db)
123
+ results.append(merged)
124
+ continue
125
+ r[pk] = ident
126
+ to_create.append(r)
127
+ if to_create:
128
+ created = await bulk_create(model, to_create, db)
129
+ results.extend(created)
130
+ logger.debug(
131
+ "bulk_merge returning %d results (%d created)",
132
+ len(results),
133
+ len(to_create),
134
+ )
135
+ return results
136
+
137
+
138
+ async def bulk_delete(
139
+ model: type, idents: Iterable[Any], db: Union[Session, AsyncSession]
140
+ ) -> Dict[str, int]:
141
+ """
142
+ Delete many rows by a sequence of PK values. Returns {"deleted": N}.
143
+ Flush-only.
144
+ """
145
+ logger.debug("bulk_delete called with model=%s idents=%s", model, idents)
146
+ pk_name = _single_pk_name(model)
147
+ id_seq = _builtins.list(idents or ())
148
+ if not id_seq:
149
+ logger.debug("bulk_delete no ids supplied")
150
+ return {"deleted": 0}
151
+
152
+ if sa_delete is not None:
153
+ col = getattr(model, pk_name)
154
+ stmt = sa_delete(model).where(col.in_(id_seq)) # type: ignore[attr-defined]
155
+ res = await _maybe_execute(db, stmt)
156
+ await _maybe_flush(db)
157
+ n = int(getattr(res, "rowcount", 0) or 0)
158
+ logger.debug("bulk_delete removed %d rows via stmt", n)
159
+ return {"deleted": n}
160
+
161
+ n = 0
162
+ for ident in id_seq:
163
+ obj = await read(model, ident, db)
164
+ await _maybe_delete(db, obj)
165
+ n += 1
166
+ await _maybe_flush(db)
167
+ logger.debug("bulk_delete removed %d rows individually", n)
168
+ return {"deleted": n}
@@ -0,0 +1,76 @@
1
+ from __future__ import annotations
2
+
3
+ try:
4
+ from sqlalchemy import select, delete as sa_delete, and_, asc, desc, Enum as SAEnum
5
+ from sqlalchemy.orm import Session
6
+ from sqlalchemy.ext.asyncio import AsyncSession
7
+ from sqlalchemy.orm.exc import NoResultFound # type: ignore
8
+ except Exception: # pragma: no cover
9
+ select = sa_delete = and_ = asc = desc = None # type: ignore
10
+ SAEnum = None # type: ignore
11
+ Session = object # type: ignore
12
+ AsyncSession = object # type: ignore
13
+
14
+ class NoResultFound(LookupError): # type: ignore
15
+ pass
16
+
17
+
18
+ from .model import (
19
+ _pk_columns,
20
+ _single_pk_name,
21
+ _coerce_pk_value,
22
+ _model_columns,
23
+ _colspecs,
24
+ _filter_in_values,
25
+ _immutable_columns,
26
+ )
27
+ from .filters import _CANON_OPS, _coerce_filters, _apply_filters, _apply_sort
28
+ from .db import (
29
+ _is_async_db,
30
+ _maybe_get,
31
+ _maybe_execute,
32
+ _maybe_flush,
33
+ _maybe_delete,
34
+ _set_attrs,
35
+ )
36
+ from .enum import _validate_enum_values
37
+ from .normalize import (
38
+ _normalize_list_call,
39
+ _pop_bound_self,
40
+ _extract_db,
41
+ _as_pos_int,
42
+ )
43
+
44
+ __all__ = [
45
+ "AsyncSession",
46
+ "Session",
47
+ "NoResultFound",
48
+ "select",
49
+ "sa_delete",
50
+ "_apply_filters",
51
+ "_apply_sort",
52
+ "_CANON_OPS",
53
+ "_coerce_filters",
54
+ "_coerce_pk_value",
55
+ "_colspecs",
56
+ "_filter_in_values",
57
+ "_immutable_columns",
58
+ "_is_async_db",
59
+ "_maybe_delete",
60
+ "_maybe_execute",
61
+ "_maybe_flush",
62
+ "_maybe_get",
63
+ "_model_columns",
64
+ "_normalize_list_call",
65
+ "_pop_bound_self",
66
+ "_extract_db",
67
+ "_as_pos_int",
68
+ "_pk_columns",
69
+ "_set_attrs",
70
+ "_single_pk_name",
71
+ "_validate_enum_values",
72
+ "SAEnum",
73
+ "asc",
74
+ "desc",
75
+ "and_",
76
+ ]
@@ -0,0 +1,92 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Mapping, Sequence, Union
4
+
5
+ import logging
6
+
7
+ from . import AsyncSession, Session
8
+ from .model import _model_columns, _single_pk_name
9
+
10
+ logging.getLogger("uvicorn").setLevel(logging.DEBUG)
11
+ logger = logging.getLogger("uvicorn")
12
+
13
+
14
+ def _is_async_db(db: Any) -> bool:
15
+ logger.debug("_is_async_db called with db=%s", db)
16
+ result = isinstance(db, AsyncSession) or hasattr(db, "run_sync")
17
+ logger.debug("_is_async_db returning %s", result)
18
+ return result
19
+
20
+
21
+ async def _maybe_get(db: Union[Session, AsyncSession], model: type, pk_value: Any):
22
+ logger.debug("_maybe_get model=%s pk_value=%s", model, pk_value)
23
+ if _is_async_db(db):
24
+ result = await db.get(model, pk_value) # type: ignore[attr-defined]
25
+ else:
26
+ result = db.get(model, pk_value) # type: ignore[attr-defined]
27
+ logger.debug("_maybe_get returning %s", result)
28
+ return result
29
+
30
+
31
+ async def _maybe_execute(db: Union[Session, AsyncSession], stmt: Any):
32
+ logger.debug("_maybe_execute stmt=%s", stmt)
33
+ if _is_async_db(db):
34
+ result = await db.execute(stmt) # type: ignore[attr-defined]
35
+ else:
36
+ result = db.execute(stmt) # type: ignore[attr-defined]
37
+ logger.debug("_maybe_execute returning %s", result)
38
+ return result
39
+
40
+
41
+ async def _maybe_flush(db: Union[Session, AsyncSession]) -> None:
42
+ logger.debug("_maybe_flush called")
43
+ if _is_async_db(db):
44
+ await db.flush() # type: ignore[attr-defined]
45
+ else:
46
+ db.flush() # type: ignore[attr-defined]
47
+ logger.debug("_maybe_flush completed")
48
+
49
+
50
+ async def _maybe_delete(db: Union[Session, AsyncSession], obj: Any) -> None:
51
+ logger.debug("_maybe_delete called with obj=%s", obj)
52
+ if not hasattr(db, "delete"):
53
+ logger.debug("_maybe_delete skipping delete; no attribute")
54
+ return
55
+ if _is_async_db(db):
56
+ await db.delete(obj) # type: ignore[attr-defined]
57
+ else:
58
+ db.delete(obj) # type: ignore[attr-defined]
59
+ logger.debug("_maybe_delete completed for obj=%s", obj)
60
+
61
+
62
+ def _set_attrs(
63
+ obj: Any,
64
+ values: Mapping[str, Any],
65
+ *,
66
+ allow_missing: bool = True,
67
+ skip: Sequence[str] = (),
68
+ ) -> None:
69
+ logger.debug(
70
+ "_set_attrs called on obj=%s values=%s allow_missing=%s skip=%s",
71
+ obj,
72
+ values,
73
+ allow_missing,
74
+ skip,
75
+ )
76
+ cols = set(_model_columns(type(obj)))
77
+ pk = _single_pk_name(type(obj))
78
+ skip_set = set(skip) | {pk}
79
+
80
+ if allow_missing:
81
+ for k, v in values.items():
82
+ if k in cols and k not in skip_set:
83
+ setattr(obj, k, v)
84
+ else:
85
+ for c in cols:
86
+ if c in skip_set:
87
+ continue
88
+ if c in values:
89
+ setattr(obj, c, values[c])
90
+ else:
91
+ setattr(obj, c, None)
92
+ logger.debug("_set_attrs completed for obj=%s", obj)
@@ -0,0 +1,86 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Mapping
4
+ import builtins as _builtins
5
+ import logging
6
+
7
+ from . import SAEnum
8
+
9
+ logging.getLogger("uvicorn").setLevel(logging.DEBUG)
10
+ logger = logging.getLogger("uvicorn")
11
+
12
+
13
+ def _validate_enum_values(model: type, values: Mapping[str, Any]) -> None:
14
+ logger.debug("_validate_enum_values called with model=%s values=%s", model, values)
15
+ if not values or SAEnum is None:
16
+ logger.debug("_validate_enum_values no validation needed")
17
+ return
18
+
19
+ table = getattr(model, "__table__", None)
20
+ if table is None:
21
+ return
22
+
23
+ get = getattr(table.c, "get", None)
24
+
25
+ for key, v in values.items():
26
+ col = get(key) if get else None
27
+ if col is None:
28
+ try:
29
+ col = table.c[key] # type: ignore[index]
30
+ except Exception:
31
+ col = None
32
+ if col is None:
33
+ continue
34
+
35
+ col_type = getattr(col, "type", None)
36
+ if col_type is None or not isinstance(col_type, SAEnum):
37
+ continue
38
+
39
+ if v is None:
40
+ continue
41
+
42
+ enum_cls = getattr(col_type, "enum_class", None)
43
+ if enum_cls is not None:
44
+ try:
45
+ import enum as _enum
46
+ except Exception: # pragma: no cover
47
+ _enum = None
48
+
49
+ if _enum is not None and isinstance(v, _enum.Enum):
50
+ if isinstance(v, enum_cls):
51
+ continue
52
+ logger.debug(
53
+ "_validate_enum_values invalid value %s for enum %s", v, enum_cls
54
+ )
55
+ raise LookupError(
56
+ f"{v!r} is not among the defined enum values. "
57
+ f"Enum name: {enum_cls.__name__}. "
58
+ f"Possible values: {', '.join([e.value for e in enum_cls])}"
59
+ )
60
+
61
+ allowed_values = [e.value for e in enum_cls]
62
+ allowed_names = [e.name for e in enum_cls]
63
+ if isinstance(v, str) and (v in allowed_values or v in allowed_names):
64
+ continue
65
+
66
+ logger.debug(
67
+ "_validate_enum_values invalid value %s for enum %s", v, enum_cls
68
+ )
69
+ raise LookupError(
70
+ f"{v!r} is not among the defined enum values. "
71
+ f"Enum name: {enum_cls.__name__}. "
72
+ f"Possible values: {', '.join(allowed_values)}"
73
+ )
74
+ else:
75
+ allowed = _builtins.list(getattr(col_type, "enums", []) or [])
76
+ if isinstance(v, str) and v in allowed:
77
+ continue
78
+ logger.debug(
79
+ "_validate_enum_values invalid value %s for enum %s", v, col_type
80
+ )
81
+ raise LookupError(
82
+ f"{v!r} is not among the defined enum values. "
83
+ f"Enum name: {getattr(col_type, 'name', 'Enum')}. "
84
+ f"Possible values: {', '.join(allowed) if allowed else '(none)'}"
85
+ )
86
+ logger.debug("_validate_enum_values completed")
@@ -0,0 +1,162 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict, Iterable, Mapping, Optional, Sequence
4
+
5
+ import logging
6
+
7
+ from . import select, and_, asc, desc
8
+ from .model import _model_columns, _colspecs
9
+
10
+ logging.getLogger("uvicorn").setLevel(logging.DEBUG)
11
+ logger = logging.getLogger("uvicorn")
12
+
13
+ _CANON_OPS = {
14
+ "eq": "eq",
15
+ "=": "eq",
16
+ "==": "eq",
17
+ "ne": "ne",
18
+ "!=": "ne",
19
+ "<>": "ne",
20
+ "lt": "lt",
21
+ "<": "lt",
22
+ "gt": "gt",
23
+ ">": "gt",
24
+ "lte": "lte",
25
+ "le": "lte",
26
+ "<=": "lte",
27
+ "gte": "gte",
28
+ "ge": "gte",
29
+ ">=": "gte",
30
+ "like": "like",
31
+ "not_like": "not_like",
32
+ "ilike": "ilike",
33
+ "not_ilike": "not_ilike",
34
+ "in": "in",
35
+ "not_in": "not_in",
36
+ "nin": "not_in",
37
+ }
38
+
39
+
40
+ def _coerce_filters(
41
+ model: type, filters: Optional[Mapping[str, Any]]
42
+ ) -> Dict[str, Any]:
43
+ logger.debug("_coerce_filters called with filters=%s", filters)
44
+ cols = set(_model_columns(model))
45
+ specs = _colspecs(model)
46
+ raw = dict(filters or {})
47
+ out: Dict[str, Any] = {}
48
+ for k, v in raw.items():
49
+ name, op = k.split("__", 1) if "__" in k else (k, "eq")
50
+ if name not in cols:
51
+ continue
52
+ canon = _CANON_OPS.get(op, op)
53
+ sp = specs.get(name)
54
+ if sp is not None:
55
+ io = getattr(sp, "io", None)
56
+ ops = set(getattr(io, "filter_ops", ()) or [])
57
+ ops = {_CANON_OPS.get(o, o) for o in ops}
58
+ if not ops or canon not in ops:
59
+ continue
60
+ key = name if canon == "eq" else f"{name}__{canon}"
61
+ out[key] = v
62
+ logger.debug("_coerce_filters returning %s", out)
63
+ return out
64
+
65
+
66
+ def _apply_filters(model: type, filters: Mapping[str, Any]) -> Any:
67
+ logger.debug("_apply_filters called with filters=%s", filters)
68
+ if select is None: # pragma: no cover
69
+ return None
70
+ clauses = []
71
+ for k, v in filters.items():
72
+ name, op = k.split("__", 1) if "__" in k else (k, "eq")
73
+ canon = _CANON_OPS.get(op, op)
74
+ col = getattr(model, name, None)
75
+ if col is None:
76
+ continue
77
+ if canon == "eq":
78
+ clauses.append(col == v)
79
+ elif canon == "ne":
80
+ clauses.append(col != v)
81
+ elif canon == "lt":
82
+ clauses.append(col < v)
83
+ elif canon == "gt":
84
+ clauses.append(col > v)
85
+ elif canon == "lte":
86
+ clauses.append(col <= v)
87
+ elif canon == "gte":
88
+ clauses.append(col >= v)
89
+ elif canon == "like":
90
+ clauses.append(col.like(v))
91
+ elif canon == "not_like":
92
+ clauses.append(~col.like(v))
93
+ elif canon == "ilike":
94
+ clauses.append(col.ilike(v))
95
+ elif canon == "not_ilike":
96
+ clauses.append(~col.ilike(v))
97
+ elif canon == "in":
98
+ seq = list(v) if isinstance(v, (list, tuple, set)) else [v]
99
+ clauses.append(col.in_(seq))
100
+ elif canon == "not_in":
101
+ seq = list(v) if isinstance(v, (list, tuple, set)) else [v]
102
+ clauses.append(~col.in_(seq))
103
+ if not clauses:
104
+ logger.debug("_apply_filters produced no clauses")
105
+ return None
106
+ result = clauses[0] if len(clauses) == 1 else and_(*clauses)
107
+ logger.debug("_apply_filters returning %s", result)
108
+ return result
109
+
110
+
111
+ def _apply_sort(model: type, sort: Any) -> Sequence[Any] | None:
112
+ logger.debug("_apply_sort called with sort=%s", sort)
113
+ if select is None or sort is None: # pragma: no cover
114
+ return None
115
+
116
+ def _tokenize(s: str) -> list[str]:
117
+ return [t.strip() for t in s.split(",") if t.strip()]
118
+
119
+ tokens: list[str] = []
120
+ if isinstance(sort, str):
121
+ tokens = _tokenize(sort)
122
+ elif isinstance(sort, Iterable):
123
+ for t in sort:
124
+ if isinstance(t, str):
125
+ tokens.extend(_tokenize(t))
126
+
127
+ if not tokens:
128
+ logger.debug("_apply_sort no tokens derived")
129
+ return None
130
+
131
+ specs = _colspecs(model)
132
+ order_by_exprs: list[Any] = []
133
+ for tok in tokens:
134
+ direction = "asc"
135
+ name = tok
136
+
137
+ if ":" in tok:
138
+ name, dirpart = tok.split(":", 1)
139
+ name = name.strip()
140
+ dirpart = dirpart.strip().lower()
141
+ if dirpart in ("desc", "descending"):
142
+ direction = "desc"
143
+ elif tok.startswith("-"):
144
+ name = tok[1:]
145
+ direction = "desc"
146
+
147
+ col = getattr(model, name, None)
148
+ if col is None:
149
+ continue
150
+ sp = specs.get(name)
151
+ if sp is not None:
152
+ io = getattr(sp, "io", None)
153
+ if io is not None and not getattr(io, "sortable", False):
154
+ continue
155
+ if direction == "desc":
156
+ order_by_exprs.append(desc(col))
157
+ else:
158
+ order_by_exprs.append(asc(col))
159
+
160
+ result = order_by_exprs or None
161
+ logger.debug("_apply_sort returning %s", result)
162
+ return result