tigrbl 0.0.1.dev1__py3-none-any.whl → 0.3.0__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 (269) 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 +97 -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 +291 -0
  9. tigrbl/app/__init__.py +0 -0
  10. tigrbl/app/_app.py +86 -0
  11. tigrbl/app/_model_registry.py +41 -0
  12. tigrbl/app/app_spec.py +42 -0
  13. tigrbl/app/mro_collect.py +67 -0
  14. tigrbl/app/shortcuts.py +65 -0
  15. tigrbl/app/tigrbl_app.py +319 -0
  16. tigrbl/bindings/__init__.py +73 -0
  17. tigrbl/bindings/api/__init__.py +12 -0
  18. tigrbl/bindings/api/common.py +109 -0
  19. tigrbl/bindings/api/include.py +256 -0
  20. tigrbl/bindings/api/resource_proxy.py +149 -0
  21. tigrbl/bindings/api/rpc.py +111 -0
  22. tigrbl/bindings/columns.py +49 -0
  23. tigrbl/bindings/handlers/__init__.py +11 -0
  24. tigrbl/bindings/handlers/builder.py +119 -0
  25. tigrbl/bindings/handlers/ctx.py +74 -0
  26. tigrbl/bindings/handlers/identifiers.py +228 -0
  27. tigrbl/bindings/handlers/namespaces.py +51 -0
  28. tigrbl/bindings/handlers/steps.py +276 -0
  29. tigrbl/bindings/hooks.py +311 -0
  30. tigrbl/bindings/model.py +194 -0
  31. tigrbl/bindings/model_helpers.py +139 -0
  32. tigrbl/bindings/model_registry.py +77 -0
  33. tigrbl/bindings/rest/__init__.py +7 -0
  34. tigrbl/bindings/rest/attach.py +34 -0
  35. tigrbl/bindings/rest/collection.py +286 -0
  36. tigrbl/bindings/rest/common.py +120 -0
  37. tigrbl/bindings/rest/fastapi.py +76 -0
  38. tigrbl/bindings/rest/helpers.py +119 -0
  39. tigrbl/bindings/rest/io.py +317 -0
  40. tigrbl/bindings/rest/io_headers.py +49 -0
  41. tigrbl/bindings/rest/member.py +386 -0
  42. tigrbl/bindings/rest/router.py +296 -0
  43. tigrbl/bindings/rest/routing.py +153 -0
  44. tigrbl/bindings/rpc.py +364 -0
  45. tigrbl/bindings/schemas/__init__.py +11 -0
  46. tigrbl/bindings/schemas/builder.py +348 -0
  47. tigrbl/bindings/schemas/defaults.py +260 -0
  48. tigrbl/bindings/schemas/utils.py +193 -0
  49. tigrbl/column/README.md +62 -0
  50. tigrbl/column/__init__.py +72 -0
  51. tigrbl/column/_column.py +96 -0
  52. tigrbl/column/column_spec.py +40 -0
  53. tigrbl/column/field_spec.py +31 -0
  54. tigrbl/column/infer/__init__.py +25 -0
  55. tigrbl/column/infer/core.py +92 -0
  56. tigrbl/column/infer/jsonhints.py +44 -0
  57. tigrbl/column/infer/planning.py +133 -0
  58. tigrbl/column/infer/types.py +102 -0
  59. tigrbl/column/infer/utils.py +59 -0
  60. tigrbl/column/io_spec.py +136 -0
  61. tigrbl/column/mro_collect.py +59 -0
  62. tigrbl/column/shortcuts.py +89 -0
  63. tigrbl/column/storage_spec.py +65 -0
  64. tigrbl/config/__init__.py +19 -0
  65. tigrbl/config/constants.py +224 -0
  66. tigrbl/config/defaults.py +29 -0
  67. tigrbl/config/resolver.py +295 -0
  68. tigrbl/core/__init__.py +47 -0
  69. tigrbl/core/crud/__init__.py +36 -0
  70. tigrbl/core/crud/bulk.py +168 -0
  71. tigrbl/core/crud/helpers/__init__.py +76 -0
  72. tigrbl/core/crud/helpers/db.py +92 -0
  73. tigrbl/core/crud/helpers/enum.py +86 -0
  74. tigrbl/core/crud/helpers/filters.py +162 -0
  75. tigrbl/core/crud/helpers/model.py +123 -0
  76. tigrbl/core/crud/helpers/normalize.py +99 -0
  77. tigrbl/core/crud/ops.py +235 -0
  78. tigrbl/ddl/__init__.py +344 -0
  79. tigrbl/decorators.py +17 -0
  80. tigrbl/deps/__init__.py +20 -0
  81. tigrbl/deps/fastapi.py +45 -0
  82. tigrbl/deps/favicon.svg +4 -0
  83. tigrbl/deps/jinja.py +27 -0
  84. tigrbl/deps/pydantic.py +10 -0
  85. tigrbl/deps/sqlalchemy.py +94 -0
  86. tigrbl/deps/starlette.py +36 -0
  87. tigrbl/engine/__init__.py +45 -0
  88. tigrbl/engine/_engine.py +144 -0
  89. tigrbl/engine/bind.py +33 -0
  90. tigrbl/engine/builders.py +236 -0
  91. tigrbl/engine/capabilities.py +29 -0
  92. tigrbl/engine/collect.py +111 -0
  93. tigrbl/engine/decorators.py +110 -0
  94. tigrbl/engine/docs/PLUGINS.md +49 -0
  95. tigrbl/engine/engine_spec.py +355 -0
  96. tigrbl/engine/plugins.py +52 -0
  97. tigrbl/engine/registry.py +36 -0
  98. tigrbl/engine/resolver.py +224 -0
  99. tigrbl/engine/shortcuts.py +216 -0
  100. tigrbl/hook/__init__.py +21 -0
  101. tigrbl/hook/_hook.py +22 -0
  102. tigrbl/hook/decorators.py +28 -0
  103. tigrbl/hook/hook_spec.py +24 -0
  104. tigrbl/hook/mro_collect.py +98 -0
  105. tigrbl/hook/shortcuts.py +44 -0
  106. tigrbl/hook/types.py +76 -0
  107. tigrbl/op/__init__.py +50 -0
  108. tigrbl/op/_op.py +31 -0
  109. tigrbl/op/canonical.py +31 -0
  110. tigrbl/op/collect.py +11 -0
  111. tigrbl/op/decorators.py +238 -0
  112. tigrbl/op/model_registry.py +301 -0
  113. tigrbl/op/mro_collect.py +99 -0
  114. tigrbl/op/resolver.py +216 -0
  115. tigrbl/op/types.py +136 -0
  116. tigrbl/orm/__init__.py +1 -0
  117. tigrbl/orm/mixins/_RowBound.py +83 -0
  118. tigrbl/orm/mixins/__init__.py +95 -0
  119. tigrbl/orm/mixins/bootstrappable.py +113 -0
  120. tigrbl/orm/mixins/bound.py +47 -0
  121. tigrbl/orm/mixins/edges.py +40 -0
  122. tigrbl/orm/mixins/fields.py +165 -0
  123. tigrbl/orm/mixins/hierarchy.py +54 -0
  124. tigrbl/orm/mixins/key_digest.py +44 -0
  125. tigrbl/orm/mixins/lifecycle.py +115 -0
  126. tigrbl/orm/mixins/locks.py +51 -0
  127. tigrbl/orm/mixins/markers.py +16 -0
  128. tigrbl/orm/mixins/operations.py +57 -0
  129. tigrbl/orm/mixins/ownable.py +337 -0
  130. tigrbl/orm/mixins/principals.py +98 -0
  131. tigrbl/orm/mixins/tenant_bound.py +301 -0
  132. tigrbl/orm/mixins/upsertable.py +118 -0
  133. tigrbl/orm/mixins/utils.py +49 -0
  134. tigrbl/orm/tables/__init__.py +72 -0
  135. tigrbl/orm/tables/_base.py +8 -0
  136. tigrbl/orm/tables/audit.py +56 -0
  137. tigrbl/orm/tables/client.py +25 -0
  138. tigrbl/orm/tables/group.py +29 -0
  139. tigrbl/orm/tables/org.py +30 -0
  140. tigrbl/orm/tables/rbac.py +76 -0
  141. tigrbl/orm/tables/status.py +106 -0
  142. tigrbl/orm/tables/tenant.py +22 -0
  143. tigrbl/orm/tables/user.py +39 -0
  144. tigrbl/response/README.md +34 -0
  145. tigrbl/response/__init__.py +33 -0
  146. tigrbl/response/bind.py +12 -0
  147. tigrbl/response/decorators.py +37 -0
  148. tigrbl/response/resolver.py +83 -0
  149. tigrbl/response/shortcuts.py +171 -0
  150. tigrbl/response/types.py +49 -0
  151. tigrbl/rest/__init__.py +27 -0
  152. tigrbl/runtime/README.md +129 -0
  153. tigrbl/runtime/__init__.py +20 -0
  154. tigrbl/runtime/atoms/__init__.py +102 -0
  155. tigrbl/runtime/atoms/emit/__init__.py +42 -0
  156. tigrbl/runtime/atoms/emit/paired_post.py +158 -0
  157. tigrbl/runtime/atoms/emit/paired_pre.py +106 -0
  158. tigrbl/runtime/atoms/emit/readtime_alias.py +120 -0
  159. tigrbl/runtime/atoms/out/__init__.py +38 -0
  160. tigrbl/runtime/atoms/out/masking.py +135 -0
  161. tigrbl/runtime/atoms/refresh/__init__.py +38 -0
  162. tigrbl/runtime/atoms/refresh/demand.py +130 -0
  163. tigrbl/runtime/atoms/resolve/__init__.py +40 -0
  164. tigrbl/runtime/atoms/resolve/assemble.py +167 -0
  165. tigrbl/runtime/atoms/resolve/paired_gen.py +147 -0
  166. tigrbl/runtime/atoms/response/__init__.py +19 -0
  167. tigrbl/runtime/atoms/response/headers_from_payload.py +57 -0
  168. tigrbl/runtime/atoms/response/negotiate.py +30 -0
  169. tigrbl/runtime/atoms/response/negotiation.py +43 -0
  170. tigrbl/runtime/atoms/response/render.py +36 -0
  171. tigrbl/runtime/atoms/response/renderer.py +116 -0
  172. tigrbl/runtime/atoms/response/template.py +44 -0
  173. tigrbl/runtime/atoms/response/templates.py +88 -0
  174. tigrbl/runtime/atoms/schema/__init__.py +40 -0
  175. tigrbl/runtime/atoms/schema/collect_in.py +21 -0
  176. tigrbl/runtime/atoms/schema/collect_out.py +21 -0
  177. tigrbl/runtime/atoms/storage/__init__.py +38 -0
  178. tigrbl/runtime/atoms/storage/to_stored.py +167 -0
  179. tigrbl/runtime/atoms/wire/__init__.py +45 -0
  180. tigrbl/runtime/atoms/wire/build_in.py +166 -0
  181. tigrbl/runtime/atoms/wire/build_out.py +87 -0
  182. tigrbl/runtime/atoms/wire/dump.py +206 -0
  183. tigrbl/runtime/atoms/wire/validate_in.py +227 -0
  184. tigrbl/runtime/context.py +206 -0
  185. tigrbl/runtime/errors/__init__.py +61 -0
  186. tigrbl/runtime/errors/converters.py +214 -0
  187. tigrbl/runtime/errors/exceptions.py +124 -0
  188. tigrbl/runtime/errors/mappings.py +71 -0
  189. tigrbl/runtime/errors/utils.py +150 -0
  190. tigrbl/runtime/events.py +209 -0
  191. tigrbl/runtime/executor/__init__.py +6 -0
  192. tigrbl/runtime/executor/guards.py +132 -0
  193. tigrbl/runtime/executor/helpers.py +88 -0
  194. tigrbl/runtime/executor/invoke.py +150 -0
  195. tigrbl/runtime/executor/types.py +84 -0
  196. tigrbl/runtime/kernel.py +644 -0
  197. tigrbl/runtime/labels.py +353 -0
  198. tigrbl/runtime/opview.py +89 -0
  199. tigrbl/runtime/ordering.py +256 -0
  200. tigrbl/runtime/system.py +279 -0
  201. tigrbl/runtime/trace.py +330 -0
  202. tigrbl/schema/__init__.py +38 -0
  203. tigrbl/schema/_schema.py +27 -0
  204. tigrbl/schema/builder/__init__.py +17 -0
  205. tigrbl/schema/builder/build_schema.py +209 -0
  206. tigrbl/schema/builder/cache.py +24 -0
  207. tigrbl/schema/builder/compat.py +16 -0
  208. tigrbl/schema/builder/extras.py +85 -0
  209. tigrbl/schema/builder/helpers.py +51 -0
  210. tigrbl/schema/builder/list_params.py +117 -0
  211. tigrbl/schema/builder/strip_parent_fields.py +70 -0
  212. tigrbl/schema/collect.py +79 -0
  213. tigrbl/schema/decorators.py +68 -0
  214. tigrbl/schema/get_schema.py +86 -0
  215. tigrbl/schema/schema_spec.py +20 -0
  216. tigrbl/schema/shortcuts.py +42 -0
  217. tigrbl/schema/types.py +34 -0
  218. tigrbl/schema/utils.py +143 -0
  219. tigrbl/session/README.md +14 -0
  220. tigrbl/session/__init__.py +28 -0
  221. tigrbl/session/abc.py +76 -0
  222. tigrbl/session/base.py +151 -0
  223. tigrbl/session/decorators.py +43 -0
  224. tigrbl/session/default.py +118 -0
  225. tigrbl/session/shortcuts.py +50 -0
  226. tigrbl/session/spec.py +112 -0
  227. tigrbl/shortcuts.py +22 -0
  228. tigrbl/specs.py +44 -0
  229. tigrbl/system/__init__.py +13 -0
  230. tigrbl/system/diagnostics/__init__.py +24 -0
  231. tigrbl/system/diagnostics/compat.py +31 -0
  232. tigrbl/system/diagnostics/healthz.py +41 -0
  233. tigrbl/system/diagnostics/hookz.py +51 -0
  234. tigrbl/system/diagnostics/kernelz.py +20 -0
  235. tigrbl/system/diagnostics/methodz.py +43 -0
  236. tigrbl/system/diagnostics/router.py +73 -0
  237. tigrbl/system/diagnostics/utils.py +43 -0
  238. tigrbl/system/uvicorn.py +60 -0
  239. tigrbl/table/__init__.py +9 -0
  240. tigrbl/table/_base.py +260 -0
  241. tigrbl/table/_table.py +54 -0
  242. tigrbl/table/mro_collect.py +69 -0
  243. tigrbl/table/shortcuts.py +57 -0
  244. tigrbl/table/table_spec.py +28 -0
  245. tigrbl/transport/__init__.py +74 -0
  246. tigrbl/transport/jsonrpc/__init__.py +19 -0
  247. tigrbl/transport/jsonrpc/dispatcher.py +352 -0
  248. tigrbl/transport/jsonrpc/helpers.py +115 -0
  249. tigrbl/transport/jsonrpc/models.py +41 -0
  250. tigrbl/transport/rest/__init__.py +25 -0
  251. tigrbl/transport/rest/aggregator.py +132 -0
  252. tigrbl/types/__init__.py +170 -0
  253. tigrbl/types/allow_anon_provider.py +19 -0
  254. tigrbl/types/authn_abc.py +30 -0
  255. tigrbl/types/nested_path_provider.py +22 -0
  256. tigrbl/types/op.py +35 -0
  257. tigrbl/types/op_config_provider.py +17 -0
  258. tigrbl/types/op_verb_alias_provider.py +33 -0
  259. tigrbl/types/request_extras_provider.py +22 -0
  260. tigrbl/types/response_extras_provider.py +22 -0
  261. tigrbl/types/table_config_provider.py +13 -0
  262. tigrbl/types/uuid.py +55 -0
  263. tigrbl-0.3.0.dist-info/METADATA +516 -0
  264. tigrbl-0.3.0.dist-info/RECORD +266 -0
  265. {tigrbl-0.0.1.dev1.dist-info → tigrbl-0.3.0.dist-info}/WHEEL +1 -1
  266. tigrbl-0.3.0.dist-info/licenses/LICENSE +201 -0
  267. tigrbl/ExampleAgent.py +0 -1
  268. tigrbl-0.0.1.dev1.dist-info/METADATA +0 -18
  269. tigrbl-0.0.1.dev1.dist-info/RECORD +0 -5
@@ -0,0 +1,119 @@
1
+ # tigrbl/v3/bindings/handlers/builder.py
2
+ from __future__ import annotations
3
+
4
+ import logging
5
+ from typing import Optional, Sequence, Tuple
6
+
7
+ from ...op import OpSpec
8
+ from ...op.types import StepFn
9
+ from .namespaces import _ensure_alias_handlers_ns, _ensure_alias_hooks_ns
10
+ from .steps import _wrap_core, _wrap_custom
11
+
12
+ logging.getLogger("uvicorn").setLevel(logging.DEBUG)
13
+ logger = logging.getLogger("uvicorn")
14
+ logger.debug("Loaded module v3/bindings/handlers/builder")
15
+
16
+ _Key = Tuple[str, str]
17
+
18
+
19
+ def _attach_one(model: type, sp: OpSpec) -> None:
20
+ alias = sp.alias
21
+ handlers_ns = _ensure_alias_handlers_ns(model, alias)
22
+ hooks_ns = _ensure_alias_hooks_ns(model, alias)
23
+ chain: list[StepFn] = getattr(hooks_ns, "HANDLER")
24
+
25
+ custom_step = _wrap_custom(model, sp, sp.handler) if sp.handler else None
26
+ core_step: Optional[StepFn] = (
27
+ None if sp.target == "custom" else _wrap_core(model, sp.target)
28
+ )
29
+
30
+ placement = sp.persist
31
+ raw_step: Optional[StepFn] = None
32
+
33
+ logger.debug(
34
+ "Attaching handler for %s.%s target=%s placement=%s",
35
+ model.__name__,
36
+ alias,
37
+ sp.target,
38
+ placement,
39
+ )
40
+
41
+ if placement == "skip":
42
+ logger.debug("Placement 'skip' selected")
43
+ if custom_step is not None:
44
+ logger.debug("Appending custom step for alias %s", alias)
45
+ chain.append(custom_step)
46
+ raw_step = custom_step
47
+ elif sp.target == "custom":
48
+ logger.debug("Target 'custom' with alias %s", alias)
49
+ if custom_step is not None:
50
+ logger.debug("Appending custom step for alias %s", alias)
51
+ chain.append(custom_step)
52
+ raw_step = custom_step
53
+ elif custom_step is not None and core_step is not None:
54
+ logger.debug("Both custom and core steps present")
55
+ if placement == "append":
56
+ logger.debug("Appending core then custom step")
57
+ chain.extend([core_step, custom_step])
58
+ raw_step = core_step
59
+ elif placement == "override":
60
+ logger.debug("Overriding core step with custom step")
61
+ chain.append(custom_step)
62
+ raw_step = custom_step
63
+ core_step = None
64
+ else:
65
+ logger.debug("Prepending custom then core step")
66
+ chain.extend([custom_step, core_step])
67
+ raw_step = core_step
68
+ elif core_step is not None:
69
+ logger.debug("Only core step present; appending")
70
+ chain.append(core_step)
71
+ raw_step = core_step
72
+ elif custom_step is not None:
73
+ logger.debug("Only custom step present; appending")
74
+ chain.append(custom_step)
75
+ raw_step = custom_step
76
+
77
+ if raw_step is None:
78
+ logger.debug("No raw step produced; exiting")
79
+ return
80
+
81
+ setattr(handlers_ns, "raw", raw_step)
82
+ setattr(handlers_ns, "handler", raw_step)
83
+
84
+ if core_step is not None:
85
+ object.__setattr__(sp, "core", core_step)
86
+ object.__setattr__(sp, "core_raw", core_step)
87
+ setattr(handlers_ns, "core", core_step)
88
+ setattr(handlers_ns, "core_raw", core_step)
89
+ logger.debug("Core step registered for %s.%s", model.__name__, alias)
90
+ else:
91
+ object.__setattr__(sp, "core", raw_step)
92
+ object.__setattr__(sp, "core_raw", raw_step)
93
+ setattr(handlers_ns, "core", raw_step)
94
+ setattr(handlers_ns, "core_raw", raw_step)
95
+ logger.debug("Raw step registered as core for %s.%s", model.__name__, alias)
96
+
97
+ logger.debug(
98
+ "handlers: %s.%s → handler chain updated (persist=%s)",
99
+ model.__name__,
100
+ alias,
101
+ sp.persist,
102
+ )
103
+
104
+
105
+ def build_and_attach(
106
+ model: type, specs: Sequence[OpSpec], *, only_keys: Optional[Sequence[_Key]] = None
107
+ ) -> None:
108
+ wanted = set(only_keys or ())
109
+ logger.debug("Building handlers for %s (only_keys=%s)", model.__name__, wanted)
110
+ for sp in specs:
111
+ key = (sp.alias, sp.target)
112
+ if wanted and key not in wanted:
113
+ logger.debug("Skipping spec %s due to only_keys filter", key)
114
+ continue
115
+ logger.debug("Processing spec %s", key)
116
+ _attach_one(model, sp)
117
+
118
+
119
+ __all__ = ["build_and_attach"]
@@ -0,0 +1,74 @@
1
+ # tigrbl/v3/bindings/handlers/ctx.py
2
+ from __future__ import annotations
3
+ import logging
4
+
5
+ from typing import Any, Mapping, Sequence
6
+
7
+ logging.getLogger("uvicorn").setLevel(logging.DEBUG)
8
+ logger = logging.getLogger("uvicorn")
9
+ logger.debug("Loaded module v3/bindings/handlers/ctx")
10
+
11
+
12
+ def _ctx_get(ctx: Mapping[str, Any], key: str, default: Any = None) -> Any:
13
+ logger.debug("_ctx_get retrieving key '%s'", key)
14
+ try:
15
+ value = ctx[key]
16
+ logger.debug("Key '%s' found via mapping access", key)
17
+ return value
18
+ except Exception:
19
+ logger.debug("Key '%s' not found; using getattr fallback", key)
20
+ return getattr(ctx, key, default)
21
+
22
+
23
+ def _ctx_payload(ctx: Mapping[str, Any]) -> Any:
24
+ temp = _ctx_get(ctx, "temp", None)
25
+ raw = _ctx_get(ctx, "payload", None)
26
+ if isinstance(temp, Mapping):
27
+ av = temp.get("assembled_values")
28
+ if isinstance(av, Mapping) and isinstance(raw, Mapping):
29
+ merged = dict(raw)
30
+ merged.update(av)
31
+ logger.debug("Payload from assembled values: %s", merged)
32
+ return merged
33
+
34
+ v = raw
35
+ if isinstance(v, Mapping):
36
+ logger.debug("Payload is a mapping")
37
+ logger.debug("Payload: %s", v)
38
+ return v
39
+ if isinstance(v, Sequence) and not isinstance(v, (str, bytes)):
40
+ logger.debug("Payload is a non-string sequence")
41
+ logger.debug("Payload: %s", v)
42
+ return v
43
+ logger.debug("Payload absent or unsupported; defaulting to empty dict")
44
+ v = {}
45
+ logger.debug("Payload: %s", v)
46
+ return v
47
+
48
+
49
+ def _ctx_db(ctx: Mapping[str, Any]) -> Any:
50
+ logger.debug("Retrieving 'db' from context")
51
+ return _ctx_get(ctx, "db")
52
+
53
+
54
+ def _ctx_request(ctx: Mapping[str, Any]) -> Any:
55
+ logger.debug("Retrieving 'request' from context")
56
+ return _ctx_get(ctx, "request")
57
+
58
+
59
+ def _ctx_path_params(ctx: Mapping[str, Any]) -> Mapping[str, Any]:
60
+ v = _ctx_get(ctx, "path_params", None)
61
+ if isinstance(v, Mapping):
62
+ logger.debug("Path params found: %s", list(v.keys()))
63
+ return v
64
+ logger.debug("No path params found; returning empty mapping")
65
+ return {}
66
+
67
+
68
+ __all__ = [
69
+ "_ctx_get",
70
+ "_ctx_payload",
71
+ "_ctx_db",
72
+ "_ctx_request",
73
+ "_ctx_path_params",
74
+ ]
@@ -0,0 +1,228 @@
1
+ # tigrbl/v3/bindings/handlers/identifiers.py
2
+ from __future__ import annotations
3
+ import logging
4
+
5
+ import uuid
6
+ from typing import Any, Mapping, Optional
7
+
8
+ from .ctx import _ctx_payload, _ctx_path_params
9
+
10
+ logging.getLogger("uvicorn").setLevel(logging.DEBUG)
11
+ logger = logging.getLogger("uvicorn")
12
+ logger.debug("Loaded module v3/bindings/handlers/identifiers")
13
+
14
+ try: # pragma: no cover
15
+ from sqlalchemy.inspection import inspect as _sa_inspect # type: ignore
16
+ except Exception: # pragma: no cover
17
+ _sa_inspect = None # type: ignore
18
+ try: # pragma: no cover
19
+ from sqlalchemy.sql import ClauseElement as SAClause # type: ignore
20
+ except Exception: # pragma: no cover
21
+ SAClause = None # type: ignore
22
+
23
+
24
+ def _pk_name(model: type) -> str:
25
+ """Best-effort primary-key column name."""
26
+ logger.debug("Resolving PK name for %s", model.__name__)
27
+ if _sa_inspect is not None:
28
+ try:
29
+ mapper = _sa_inspect(model)
30
+ pk_cols = list(getattr(mapper, "primary_key", []) or [])
31
+ logger.debug("SQLAlchemy mapper found %d PK cols", len(pk_cols))
32
+ if len(pk_cols) == 1:
33
+ col = pk_cols[0]
34
+ name = getattr(col, "key", None) or getattr(col, "name", None)
35
+ logger.debug("PK name via mapper: %s", name)
36
+ if isinstance(name, str) and name:
37
+ return name
38
+ except Exception as exc:
39
+ logger.debug("SQLAlchemy inspection failed: %s", exc)
40
+
41
+ table = getattr(model, "__table__", None)
42
+ if table is not None:
43
+ try:
44
+ pk = getattr(table, "primary_key", None)
45
+ cols_iter = getattr(pk, "columns", None)
46
+ cols = [c for c in cols_iter] if cols_iter is not None else []
47
+ logger.debug("Table inspection found %d PK cols", len(cols))
48
+ if len(cols) == 1:
49
+ col = cols[0]
50
+ name = getattr(col, "key", None) or getattr(col, "name", None)
51
+ logger.debug("PK name via table: %s", name)
52
+ if isinstance(name, str) and name:
53
+ return name
54
+ except Exception as exc:
55
+ logger.debug("Table inspection failed: %s", exc)
56
+
57
+ logger.debug("Falling back to default PK name 'id'")
58
+ return "id"
59
+
60
+
61
+ def _pk_type_info(model: type) -> tuple[Optional[type], Optional[Any]]:
62
+ """Return (python_type, sqlatype_instance) for the PK column if discoverable."""
63
+ logger.debug("Resolving PK type info for %s", model.__name__)
64
+ col = None
65
+ if _sa_inspect is not None:
66
+ try:
67
+ mapper = _sa_inspect(model)
68
+ pk_cols = list(getattr(mapper, "primary_key", []) or [])
69
+ logger.debug("Mapper returned %d PK cols", len(pk_cols))
70
+ if len(pk_cols) == 1:
71
+ col = pk_cols[0]
72
+ except Exception as exc:
73
+ logger.debug("Mapper inspection failed: %s", exc)
74
+ col = None
75
+
76
+ if col is None:
77
+ table = getattr(model, "__table__", None)
78
+ if table is not None:
79
+ try:
80
+ pk = getattr(table, "primary_key", None)
81
+ cols_iter = getattr(pk, "columns", None)
82
+ cols = [c for c in cols_iter] if cols_iter is not None else []
83
+ logger.debug("Table inspection yielded %d PK cols", len(cols))
84
+ if len(cols) == 1:
85
+ col = cols[0]
86
+ except Exception as exc:
87
+ logger.debug("Table inspection failed: %s", exc)
88
+ col = None
89
+
90
+ if col is None:
91
+ logger.debug("PK column not found; returning (None, None)")
92
+ return (None, None)
93
+
94
+ try:
95
+ coltype = getattr(col, "type", None)
96
+ except Exception as exc:
97
+ logger.debug("Failed to get column type: %s", exc)
98
+ coltype = None
99
+
100
+ py_t = None
101
+ try:
102
+ py_t = getattr(coltype, "python_type", None)
103
+ except Exception as exc:
104
+ logger.debug("Failed to get python_type: %s", exc)
105
+ py_t = None
106
+
107
+ logger.debug("Resolved PK type info py_t=%s sa_type=%s", py_t, coltype)
108
+ return (py_t, coltype)
109
+
110
+
111
+ def _looks_like_uuid_string(s: str) -> bool:
112
+ if not isinstance(s, str):
113
+ logger.debug("Value %r is not a string; cannot be UUID", s)
114
+ return False
115
+ try:
116
+ uuid.UUID(s)
117
+ logger.debug("Value %s looks like a UUID string", s)
118
+ return True
119
+ except Exception:
120
+ logger.debug("Value %s is not a valid UUID string", s)
121
+ return False
122
+
123
+
124
+ def _is_uuid_type(py_t: Optional[type], sa_type: Optional[Any]) -> bool:
125
+ if py_t is uuid.UUID:
126
+ logger.debug("PK python type is UUID")
127
+ return True
128
+ try:
129
+ if getattr(sa_type, "as_uuid", False):
130
+ logger.debug("SQLAlchemy type %r uses as_uuid", sa_type)
131
+ return True
132
+ except Exception:
133
+ pass
134
+ try:
135
+ tname = type(sa_type).__name__.lower() if sa_type is not None else ""
136
+ if "uuid" in tname:
137
+ logger.debug("SQLAlchemy type name contains 'uuid': %s", tname)
138
+ return True
139
+ except Exception:
140
+ pass
141
+ return False
142
+
143
+
144
+ def _coerce_ident_to_pk_type(model: type, value: Any) -> Any:
145
+ py_t, sa_t = _pk_type_info(model)
146
+ logger.debug("Coercing identifier %r to PK type %s", value, py_t)
147
+ if SAClause is not None and isinstance(value, SAClause): # pragma: no cover
148
+ logger.debug("Value is SAClause; returning as-is")
149
+ return value
150
+ if _is_uuid_type(py_t, sa_t):
151
+ logger.debug("PK type identified as UUID")
152
+ if isinstance(value, uuid.UUID):
153
+ return value
154
+ if isinstance(value, str):
155
+ return uuid.UUID(value)
156
+ if isinstance(value, (bytes, bytearray)) and len(value) == 16:
157
+ return uuid.UUID(bytes=bytes(value))
158
+ if _looks_like_uuid_string(str(value)):
159
+ return uuid.UUID(str(value))
160
+ return value
161
+ if py_t is int:
162
+ logger.debug("PK type identified as int")
163
+ if isinstance(value, int):
164
+ return value
165
+ if isinstance(value, str):
166
+ return int(value)
167
+ try:
168
+ return int(value) # type: ignore[arg-type]
169
+ except Exception as exc:
170
+ logger.debug("Failed int coercion: %s", exc)
171
+ return value
172
+ return value
173
+
174
+
175
+ def _is_clause(x: Any) -> bool:
176
+ return SAClause is not None and isinstance(x, SAClause) # type: ignore[truthy-bool]
177
+
178
+
179
+ def _resolve_ident(model: type, ctx: Mapping[str, Any]) -> Any:
180
+ payload = _ctx_payload(ctx)
181
+ path = _ctx_path_params(ctx)
182
+ pk = _pk_name(model)
183
+ logger.debug("Resolving identifier for %s using pk %s", model.__name__, pk)
184
+
185
+ candidates_keys = [
186
+ (path, pk),
187
+ (payload, pk),
188
+ (path, "id"),
189
+ (payload, "id"),
190
+ (path, "item_id"),
191
+ (payload, "item_id"),
192
+ ]
193
+ if pk != "id":
194
+ candidates_keys.extend(
195
+ [
196
+ (path, f"{pk}_id"),
197
+ (payload, f"{pk}_id"),
198
+ ]
199
+ )
200
+ candidates_keys.append((payload, "ident"))
201
+
202
+ for source, key in candidates_keys:
203
+ try:
204
+ v = source.get(key) # type: ignore[call-arg]
205
+ except Exception:
206
+ v = None
207
+ logger.debug("Checking candidate key '%s' → %r", key, v)
208
+ if v is None:
209
+ continue
210
+ if _is_clause(v):
211
+ logger.debug("Value for key '%s' is a clause; skipping", key)
212
+ continue
213
+ try:
214
+ return _coerce_ident_to_pk_type(model, v)
215
+ except Exception:
216
+ logger.debug("Invalid identifier %r for pk %s", v, pk)
217
+ raise TypeError(f"Invalid identifier for '{pk}': {v!r}")
218
+
219
+ logger.debug("Identifier for pk %s not found", pk)
220
+ raise TypeError(f"Missing identifier '{pk}' in path or payload")
221
+
222
+
223
+ __all__ = [
224
+ "_pk_name",
225
+ "_pk_type_info",
226
+ "_resolve_ident",
227
+ "_coerce_ident_to_pk_type",
228
+ ]
@@ -0,0 +1,51 @@
1
+ # tigrbl/v3/bindings/handlers/namespaces.py
2
+ from __future__ import annotations
3
+ import logging
4
+
5
+ from types import SimpleNamespace
6
+
7
+ logging.getLogger("uvicorn").setLevel(logging.DEBUG)
8
+ logger = logging.getLogger("uvicorn")
9
+ logger.debug("Loaded module v3/bindings/handlers/namespaces")
10
+
11
+
12
+ def _ensure_alias_hooks_ns(model: type, alias: str) -> SimpleNamespace:
13
+ hooks_root = getattr(model, "hooks", None)
14
+ if hooks_root is None:
15
+ logger.debug("Creating hooks namespace for %s", model.__name__)
16
+ hooks_root = SimpleNamespace()
17
+ setattr(model, "hooks", hooks_root)
18
+
19
+ ns = getattr(hooks_root, alias, None)
20
+ if ns is None:
21
+ logger.debug("Creating hooks alias namespace '%s'", alias)
22
+ ns = SimpleNamespace()
23
+ setattr(hooks_root, alias, ns)
24
+
25
+ if not hasattr(ns, "HANDLER"):
26
+ logger.debug("Initializing HANDLER list for hooks '%s'", alias)
27
+ setattr(ns, "HANDLER", [])
28
+ return ns
29
+
30
+
31
+ def _ensure_alias_handlers_ns(model: type, alias: str) -> SimpleNamespace:
32
+ handlers_root = getattr(model, "handlers", None)
33
+ if handlers_root is None:
34
+ logger.debug("Creating handlers namespace for %s", model.__name__)
35
+ handlers_root = SimpleNamespace()
36
+ setattr(model, "handlers", handlers_root)
37
+
38
+ ns = getattr(handlers_root, alias, None)
39
+ if ns is None:
40
+ logger.debug("Creating handlers alias namespace '%s'", alias)
41
+ ns = SimpleNamespace()
42
+ setattr(handlers_root, alias, ns)
43
+
44
+ hooks_ns = _ensure_alias_hooks_ns(model, alias)
45
+ if not hasattr(ns, "HANDLER"):
46
+ logger.debug("Linking HANDLER list from hooks namespace for '%s'", alias)
47
+ setattr(ns, "HANDLER", getattr(hooks_ns, "HANDLER"))
48
+ return ns
49
+
50
+
51
+ __all__ = ["_ensure_alias_hooks_ns", "_ensure_alias_handlers_ns"]