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,123 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict, Mapping, Tuple
4
+
5
+ import logging
6
+
7
+ from ....column.mro_collect import mro_collect_columns
8
+
9
+ logging.getLogger("uvicorn").setLevel(logging.DEBUG)
10
+ logger = logging.getLogger("uvicorn")
11
+
12
+
13
+ def _pk_columns(model: type) -> Tuple[Any, ...]:
14
+ logger.debug("_pk_columns called with model=%s", model)
15
+ table = getattr(model, "__table__", None)
16
+ if table is None:
17
+ raise ValueError(f"{model.__name__} has no __table__")
18
+ pks = tuple(table.primary_key.columns) # type: ignore[attr-defined]
19
+ if not pks:
20
+ raise ValueError(f"{model.__name__} has no primary key")
21
+ logger.debug("_pk_columns returning %s", pks)
22
+ return pks
23
+
24
+
25
+ def _single_pk_name(model: type) -> str:
26
+ logger.debug("_single_pk_name called with model=%s", model)
27
+ pks = _pk_columns(model)
28
+ if len(pks) != 1:
29
+ raise NotImplementedError(
30
+ f"{model.__name__} has composite PK; not supported by default core"
31
+ )
32
+ name = pks[0].name
33
+ logger.debug("_single_pk_name returning %s", name)
34
+ return name
35
+
36
+
37
+ def _coerce_pk_value(model: type, value: Any) -> Any:
38
+ logger.debug("_coerce_pk_value called with model=%s value=%s", model, value)
39
+ if value is None:
40
+ return None
41
+ try:
42
+ col = _pk_columns(model)[0]
43
+ py_type = col.type.python_type # type: ignore[attr-defined]
44
+ except Exception: # pragma: no cover - best effort
45
+ logger.debug("_coerce_pk_value returning original value %s", value)
46
+ return value
47
+ if isinstance(value, py_type):
48
+ return value
49
+ try:
50
+ coerced = py_type(value)
51
+ logger.debug("_coerce_pk_value coerced %s to %s", value, coerced)
52
+ return coerced
53
+ except Exception: # pragma: no cover - fallback to original
54
+ logger.debug("_coerce_pk_value failed to coerce %s", value)
55
+ return value
56
+
57
+
58
+ def _model_columns(model: type) -> Tuple[str, ...]:
59
+ logger.debug("_model_columns called with model=%s", model)
60
+ table = getattr(model, "__table__", None)
61
+ if table is None:
62
+ return ()
63
+ cols = tuple(c.name for c in table.columns)
64
+ logger.debug("_model_columns returning %s", cols)
65
+ return cols
66
+
67
+
68
+ def _colspecs(model: type) -> Mapping[str, Any]:
69
+ logger.info("_colspecs called with model=%s", model)
70
+ cache_bust = hash(
71
+ (
72
+ id(getattr(model, "__tigrbl_colspecs__", None)),
73
+ id(getattr(model, "__tigrbl_cols__", None)),
74
+ )
75
+ )
76
+ specs = mro_collect_columns(model, _cache_bust=cache_bust)
77
+ logger.info("_colspecs returning %s", specs)
78
+ return specs
79
+
80
+
81
+ def _filter_in_values(
82
+ model: type, data: Mapping[str, Any], verb: str
83
+ ) -> Dict[str, Any]:
84
+ logger.info("_filter_in_values called with data=%s verb=%s", data, verb)
85
+ specs = _colspecs(model)
86
+ if not specs:
87
+ result = dict(data)
88
+ logger.debug("_filter_in_values returning %s", result)
89
+ return result
90
+ out: Dict[str, Any] = {}
91
+ for k, v in data.items():
92
+ sp = specs.get(k)
93
+ if sp is None:
94
+ out[k] = v
95
+ continue
96
+ io = getattr(sp, "io", None)
97
+ allowed = True
98
+ if io is not None:
99
+ in_verbs = getattr(io, "in_verbs", ())
100
+ mutable = getattr(io, "mutable_verbs", ())
101
+ if in_verbs and verb not in in_verbs:
102
+ allowed = False
103
+ if mutable and verb not in mutable:
104
+ allowed = False
105
+ if allowed:
106
+ out[k] = v
107
+ logger.info("_filter_in_values returning %s", out)
108
+ return out
109
+
110
+
111
+ def _immutable_columns(model: type, verb: str) -> set[str]:
112
+ logger.info("_immutable_columns called with model=%s verb=%s", model, verb)
113
+ specs = _colspecs(model)
114
+ if not specs:
115
+ return set()
116
+ imm: set[str] = set()
117
+ for name, sp in specs.items():
118
+ io = getattr(sp, "io", None)
119
+ mutable = getattr(io, "mutable_verbs", ()) if io else ()
120
+ if mutable and verb not in mutable:
121
+ imm.add(name)
122
+ logger.info("_immutable_columns returning %s", imm)
123
+ return imm
@@ -0,0 +1,99 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict, Mapping, Optional, Union
4
+ import builtins as _builtins
5
+ import logging
6
+
7
+ from . import AsyncSession, Session
8
+
9
+ logging.getLogger("uvicorn").setLevel(logging.DEBUG)
10
+ logger = logging.getLogger("uvicorn")
11
+
12
+
13
+ def _pop_bound_self(args: list[Any]) -> None:
14
+ logger.debug("_pop_bound_self called with args=%s", args)
15
+ if args and not isinstance(args[0], type):
16
+ args.pop(0)
17
+ logger.debug("_pop_bound_self result args=%s", args)
18
+
19
+
20
+ def _extract_db(
21
+ args: list[Any], kwargs: dict[str, Any]
22
+ ) -> Union[Session, AsyncSession]:
23
+ logger.debug("_extract_db called with args=%s kwargs=%s", args, kwargs)
24
+ db = kwargs.pop("db", None)
25
+ if db is not None:
26
+ logger.debug("_extract_db found db in kwargs=%s", db)
27
+ return db
28
+ for i, a in enumerate(args):
29
+ if isinstance(a, (Session, AsyncSession)) or hasattr(a, "execute"):
30
+ args.pop(i)
31
+ logger.debug("_extract_db using positional db=%s", a)
32
+ return a # type: ignore[return-value]
33
+ logger.debug("_extract_db failed to find db")
34
+ raise TypeError("db session is required")
35
+
36
+
37
+ def _as_pos_int(x: Any) -> Optional[int]:
38
+ logger.debug("_as_pos_int called with x=%s", x)
39
+ if x is None:
40
+ return None
41
+ try:
42
+ v = int(x)
43
+ result = v if v >= 0 else 0
44
+ logger.debug("_as_pos_int returning %s", result)
45
+ return result
46
+ except Exception:
47
+ logger.debug("_as_pos_int returning None for x=%s", x)
48
+ return None
49
+
50
+
51
+ def _normalize_list_call(
52
+ _args: tuple[Any, ...], _kwargs: dict[str, Any]
53
+ ) -> tuple[type, Dict[str, Any]]:
54
+ logger.debug("_normalize_list_call called with _args=%s _kwargs=%s", _args, _kwargs)
55
+ args = _builtins.list(_args)
56
+ kwargs = dict(_kwargs)
57
+
58
+ _pop_bound_self(args)
59
+
60
+ if args and isinstance(args[0], type):
61
+ model = args.pop(0)
62
+ else:
63
+ model = kwargs.pop("model", None)
64
+ if not isinstance(model, type):
65
+ raise TypeError("list(model, ...) requires a model class")
66
+
67
+ filters = kwargs.pop("filters", None)
68
+ if filters is None and args:
69
+ maybe = args[0]
70
+ if isinstance(maybe, Mapping):
71
+ filters = args.pop(0)
72
+
73
+ skip = _as_pos_int(kwargs.pop("skip", None))
74
+ limit = _as_pos_int(kwargs.pop("limit", None))
75
+ sort = kwargs.pop("sort", None)
76
+
77
+ if skip is None and args:
78
+ skip = _as_pos_int(args[0])
79
+ if skip is not None:
80
+ args.pop(0)
81
+ if limit is None and args:
82
+ limit = _as_pos_int(args[0])
83
+ if limit is not None:
84
+ args.pop(0)
85
+
86
+ db = _extract_db(args, kwargs)
87
+
88
+ if filters is None:
89
+ filters = {}
90
+
91
+ result = {
92
+ "filters": filters,
93
+ "skip": skip,
94
+ "limit": limit,
95
+ "db": db,
96
+ "sort": sort,
97
+ }
98
+ logger.debug("_normalize_list_call returning model=%s params=%s", model, result)
99
+ return model, result
@@ -0,0 +1,235 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict, List, Mapping, Optional, Union
4
+
5
+ import builtins as _builtins
6
+ import logging
7
+
8
+ from .helpers import (
9
+ AsyncSession,
10
+ Session,
11
+ NoResultFound,
12
+ select,
13
+ sa_delete,
14
+ _apply_filters,
15
+ _apply_sort,
16
+ _coerce_filters,
17
+ _coerce_pk_value,
18
+ _filter_in_values,
19
+ _immutable_columns,
20
+ _maybe_delete,
21
+ _maybe_execute,
22
+ _maybe_flush,
23
+ _maybe_get,
24
+ _normalize_list_call,
25
+ _set_attrs,
26
+ _single_pk_name,
27
+ _validate_enum_values,
28
+ )
29
+
30
+ logging.getLogger("uvicorn").setLevel(logging.DEBUG)
31
+ logger = logging.getLogger("uvicorn")
32
+
33
+
34
+ async def create(
35
+ model: type, data: Mapping[str, Any], db: Union[Session, AsyncSession]
36
+ ) -> Any:
37
+ """
38
+ Insert a single row. Returns the persisted model instance.
39
+ Flush-only (commit happens later in END_TX).
40
+ """
41
+ logger.debug("create called with model=%s data=%s", model, data)
42
+ data = _filter_in_values(model, data or {}, "create")
43
+ _validate_enum_values(model, data)
44
+ obj = model(**data)
45
+ if hasattr(db, "add"):
46
+ db.add(obj)
47
+ await _maybe_flush(db)
48
+ logger.debug("create persisted obj=%s", obj)
49
+ return obj
50
+
51
+
52
+ async def read(model: type, ident: Any, db: Union[Session, AsyncSession]) -> Any:
53
+ """
54
+ Load a single row by primary key. Raises NoResultFound if not found.
55
+ """
56
+ logger.debug("read called with model=%s ident=%s", model, ident)
57
+ obj = await _maybe_get(db, model, ident)
58
+ if obj is None:
59
+ logger.debug("read did not find model=%s ident=%s", model, ident)
60
+ raise NoResultFound(f"{model.__name__}({ident!r}) not found")
61
+ logger.debug("read returning obj=%s", obj)
62
+ return obj
63
+
64
+
65
+ async def update(
66
+ model: type, ident: Any, data: Mapping[str, Any], db: Union[Session, AsyncSession]
67
+ ) -> Any:
68
+ """
69
+ Partial update by primary key. Missing keys are left unchanged.
70
+ Returns the updated model instance. Flush-only.
71
+ """
72
+ logger.debug("update called with model=%s ident=%s data=%s", model, ident, data)
73
+ data = _filter_in_values(model, data or {}, "update")
74
+ _validate_enum_values(model, data)
75
+ obj = await read(model, ident, db)
76
+ skip = _immutable_columns(model, "update")
77
+ _set_attrs(obj, data, allow_missing=True, skip=skip)
78
+ await _maybe_flush(db)
79
+ logger.debug("update returning obj=%s", obj)
80
+ return obj
81
+
82
+
83
+ async def replace(
84
+ model: type, ident: Any, data: Mapping[str, Any], db: Union[Session, AsyncSession]
85
+ ) -> Any:
86
+ """
87
+ PUT semantics with upsert behaviour.
88
+
89
+ If the row exists it is replaced entirely (missing attributes are nulled).
90
+ If the row does not exist it is created with the provided identifier.
91
+ Flush-only.
92
+ """
93
+ logger.debug("replace called with model=%s ident=%s data=%s", model, ident, data)
94
+ data = _filter_in_values(model, data or {}, "replace")
95
+ _validate_enum_values(model, data)
96
+ pk = _single_pk_name(model)
97
+ obj = await _maybe_get(db, model, ident)
98
+ if obj is None:
99
+ payload = {pk: ident, **data}
100
+ result = await create(model, payload, db=db)
101
+ logger.debug("replace created obj=%s", result)
102
+ return result
103
+ skip = _immutable_columns(model, "replace")
104
+ _set_attrs(obj, data, allow_missing=False, skip=skip)
105
+ await _maybe_flush(db)
106
+ logger.debug("replace updated obj=%s", obj)
107
+ return obj
108
+
109
+
110
+ async def merge(
111
+ model: type, ident: Any, data: Mapping[str, Any], db: Union[Session, AsyncSession]
112
+ ) -> Any:
113
+ """PATCH semantics with upsert behaviour."""
114
+ logger.debug("merge called with model=%s ident=%s data=%s", model, ident, data)
115
+ pk = _single_pk_name(model)
116
+ ident = _coerce_pk_value(model, ident)
117
+ obj = await _maybe_get(db, model, ident)
118
+
119
+ verb = "update" if obj is not None else "create"
120
+ data = _filter_in_values(model, data or {}, verb)
121
+ _validate_enum_values(model, data)
122
+ data_no_pk = {k: v for k, v in data.items() if k != pk}
123
+ if obj is None:
124
+ payload = {pk: ident, **data_no_pk}
125
+ result = await create(model, payload, db=db)
126
+ logger.debug("merge created obj=%s", result)
127
+ return result
128
+ skip = _immutable_columns(model, "update")
129
+ _set_attrs(obj, data_no_pk, allow_missing=True, skip=skip)
130
+ await _maybe_flush(db)
131
+ logger.debug("merge updated obj=%s", obj)
132
+ return obj
133
+
134
+
135
+ async def delete(
136
+ model: type, ident: Any, db: Union[Session, AsyncSession]
137
+ ) -> Dict[str, int]:
138
+ """
139
+ Delete by primary key. Returns {"deleted": 1} if removed, else raises NoResultFound.
140
+ Flush-only.
141
+ """
142
+ logger.debug("delete called with model=%s ident=%s", model, ident)
143
+ obj = await read(model, ident, db)
144
+ await _maybe_delete(db, obj)
145
+ await _maybe_flush(db)
146
+ logger.debug("delete removed obj=%s", obj)
147
+ return {"deleted": 1}
148
+
149
+
150
+ # NOTE: tolerant signature: accepts positional/keyword and ignores stray args
151
+ async def list(*_args: Any, **_kwargs: Any) -> List[Any]: # noqa: A001 (shadow built-in)
152
+ """
153
+ Simple list with equality filters + skip/limit (+ optional sort).
154
+ Tolerant to:
155
+ - missing filters (defaults to {})
156
+ - accidental bound-method 'self' (first positional arg)
157
+ - positional or keyword args
158
+ - stray extras (e.g., request) which are ignored
159
+ """
160
+ logger.debug("list called with args=%s kwargs=%s", _args, _kwargs)
161
+ model, params = _normalize_list_call(_args, _kwargs)
162
+
163
+ filters: Mapping[str, Any] = _coerce_filters(model, params["filters"])
164
+ skip: Optional[int] = params["skip"]
165
+ limit: Optional[int] = params["limit"]
166
+ db: Union[Session, AsyncSession] = params["db"]
167
+ sort = params["sort"]
168
+
169
+ if select is None: # pragma: no cover
170
+ # Fallback: legacy query API
171
+ q = db.query(model) # type: ignore[attr-defined]
172
+ if filters:
173
+ q = q.filter_by(**filters) # type: ignore[attr-defined]
174
+ if isinstance(skip, int):
175
+ q = q.offset(max(skip, 0)) # type: ignore[attr-defined]
176
+ if isinstance(limit, int) and limit is not None:
177
+ q = q.limit(max(limit, 0)) # type: ignore[attr-defined]
178
+ return _builtins.list(q.all()) # type: ignore[attr-defined]
179
+
180
+ where = _apply_filters(model, filters)
181
+ stmt = select(model)
182
+ if where is not None:
183
+ stmt = stmt.where(where)
184
+
185
+ order_exprs = _apply_sort(model, sort)
186
+ if order_exprs:
187
+ for ob in order_exprs:
188
+ stmt = stmt.order_by(ob)
189
+
190
+ if isinstance(skip, int):
191
+ stmt = stmt.offset(max(skip, 0))
192
+ if isinstance(limit, int) and limit is not None:
193
+ stmt = stmt.limit(max(limit, 0))
194
+
195
+ result = await _maybe_execute(db, stmt)
196
+ items = _builtins.list(result.scalars().all()) # type: ignore[attr-defined]
197
+ logger.debug("list returning %d items", len(items))
198
+ return items
199
+
200
+
201
+ async def clear(
202
+ *args: Any,
203
+ **kwargs: Any,
204
+ ) -> Dict[str, int]:
205
+ """
206
+ Delete many rows matching equality filters. Returns {"deleted": N}.
207
+ Flush-only. Tolerant to the same calling variations as `list`.
208
+ """
209
+ # Reuse normalizer to accept the same shapes
210
+ logger.debug("clear called with args=%s kwargs=%s", args, kwargs)
211
+ model, params = _normalize_list_call(args, kwargs)
212
+ raw_filters: Mapping[str, Any] = params["filters"]
213
+ db: Union[Session, AsyncSession] = params["db"]
214
+
215
+ if sa_delete is None: # pragma: no cover
216
+ # Fallback path: manual iteration
217
+ items = await list(model, raw_filters, db=db)
218
+ n = 0
219
+ for obj in items:
220
+ await _maybe_delete(db, obj)
221
+ n += 1
222
+ await _maybe_flush(db)
223
+ return {"deleted": n}
224
+
225
+ filt = _coerce_filters(model, raw_filters)
226
+ where = _apply_filters(model, filt)
227
+ stmt = sa_delete(model)
228
+ if where is not None:
229
+ stmt = stmt.where(where)
230
+
231
+ res = await _maybe_execute(db, stmt)
232
+ await _maybe_flush(db)
233
+ n = int(getattr(res, "rowcount", 0) or 0)
234
+ logger.debug("clear removed %d rows", n)
235
+ return {"deleted": n}