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,49 @@
1
+
2
+ # Tigrbl Engine Plugins
3
+
4
+ Tigrbl supports external engine kinds via an entry-point group: `tigrbl.engine`.
5
+
6
+ An external package registers itself by exposing a `register()` function and
7
+ declaring an entry point:
8
+
9
+ ```toml
10
+ [project.entry-points."tigrbl.engine"]
11
+ duckdb = "tigrbl_engine_duckdb.plugin:register"
12
+ ```
13
+
14
+ Inside `register()` call:
15
+
16
+ ```python
17
+ from tigrbl.engine.registry import register_engine
18
+ from .builder import duckdb_engine, duckdb_capabilities
19
+
20
+ def register():
21
+ register_engine("duckdb", duckdb_engine, duckdb_capabilities)
22
+ ```
23
+
24
+ At runtime, `EngineSpec(kind="duckdb")` will look up the registration and use
25
+ the external builder or raise a helpful `RuntimeError` if the plugin is not
26
+ installed.
27
+
28
+
29
+ ## Capabilities / supports()
30
+
31
+ External engines should expose a capabilities callable when registering:
32
+
33
+ ```python
34
+ from tigrbl.engine.registry import register_engine
35
+
36
+ def my_engine_builder(...): ...
37
+ def my_engine_capabilities(**kw):
38
+ # Return a dict describing what the engine supports
39
+ return {
40
+ "transactional": True,
41
+ "isolation_levels": {"read_committed","serializable"},
42
+ "read_only_enforced": True,
43
+ "async_native": False,
44
+ "engine": "myengine",
45
+ }
46
+
47
+ def register():
48
+ register_engine("myengine", my_engine_builder, capabilities=my_engine_capabilities)
49
+ ```
@@ -0,0 +1,355 @@
1
+ # tigrbl/v3/engine/engine_spec.py
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass, field
5
+ from typing import Optional, Mapping, Union, Any, Tuple
6
+ from urllib.parse import urlsplit, urlunsplit
7
+
8
+ from ._engine import Engine, Provider, SessionFactory
9
+ from .builders import (
10
+ async_postgres_engine,
11
+ async_sqlite_engine,
12
+ blocking_postgres_engine,
13
+ blocking_sqlite_engine,
14
+ )
15
+
16
+ # The value stored by @engine_ctx on App/API/Table/Op.
17
+ EngineCfg = Union[str, Mapping[str, object], "EngineSpec", Provider, Engine]
18
+
19
+
20
+ @dataclass
21
+ class EngineSpec:
22
+ """
23
+ Canonical, normalized engine spec → Provider factory.
24
+
25
+ Input comes from @engine_ctx attached to an App/API/Table/Op:
26
+ • DSN string:
27
+ "sqlite://:memory:" ,
28
+ "sqlite:///./file.db" ,
29
+ "sqlite+aiosqlite:///./file.db" ,
30
+ "postgresql://user:pwd@host:5432/db" ,
31
+ "postgresql+asyncpg://user:pwd@host:5432/db"
32
+ • Mapping (recommended for clarity/portability):
33
+ {"kind":"sqlite","async":True,"path":"./file.db"}
34
+ {"kind":"postgres","async":True,"host":"db","db":"app_db",...}
35
+ {<external kind> ...} # for plugin engines
36
+ """
37
+
38
+ # normalized
39
+ kind: Optional[str] = None # "sqlite" | "postgres" | <external>
40
+ async_: bool = False
41
+
42
+ # canonical DSN (optional) and raw mapping (for external engines)
43
+ dsn: Optional[str] = None
44
+ mapping: Optional[Mapping[str, object]] = None
45
+
46
+ # sqlite
47
+ path: Optional[str] = None # file path (None → memory)
48
+ memory: bool = False
49
+
50
+ # postgres
51
+ user: Optional[str] = None
52
+ pwd: Optional[str] = field(default=None, repr=False)
53
+ host: Optional[str] = None
54
+ port: Optional[int] = None
55
+ name: Optional[str] = None
56
+ pool_size: int = 10
57
+ max: int = 20 # max_overflow (sync) or max_size (async)
58
+
59
+ # ---------- parsing ----------
60
+
61
+ @staticmethod
62
+ def from_any(x: EngineCfg | None) -> Optional["EngineSpec"]:
63
+ """Parse DSN/Mapping/Provider/Engine into an :class:`EngineSpec`."""
64
+ if x is None:
65
+ return None
66
+
67
+ if isinstance(x, EngineSpec):
68
+ return x
69
+
70
+ if isinstance(x, Provider):
71
+ return x.spec
72
+
73
+ if isinstance(x, Engine):
74
+ return x.spec
75
+
76
+ # DSN string
77
+ if isinstance(x, str):
78
+ s = x.strip()
79
+ # sqlite async
80
+ if s.startswith("sqlite+aiosqlite://") or s.startswith("sqlite+aiosqlite:"):
81
+ path = urlsplit(s).path or ""
82
+ if s.startswith("sqlite+aiosqlite:////"):
83
+ if path.startswith("//"):
84
+ path = path[1:]
85
+ path = path or None
86
+ else:
87
+ path = path.lstrip("/") or None
88
+ mem = path in {None, ":memory:", "/:memory:"} or s.endswith(":memory:")
89
+ return EngineSpec(
90
+ kind="sqlite", async_=True, path=path, memory=mem, dsn=s
91
+ )
92
+ # sqlite sync
93
+ if s.startswith("sqlite://") or s.startswith("sqlite:"):
94
+ # handle sqlite://:memory: and sqlite:///file.db
95
+ if s.startswith("sqlite://:memory:") or s.endswith(":memory:"):
96
+ return EngineSpec(
97
+ kind="sqlite", async_=False, path=None, memory=True, dsn=s
98
+ )
99
+ # Take the path part after scheme; urlsplit handles both sqlite:// and sqlite:/// forms
100
+ p = urlsplit(s).path or ""
101
+ if s.startswith("sqlite:////"):
102
+ if p.startswith("//"):
103
+ p = p[1:]
104
+ p = p or None
105
+ else:
106
+ p = p.lstrip("/") or None
107
+ mem = p is None
108
+ return EngineSpec(
109
+ kind="sqlite", async_=False, path=p, memory=mem, dsn=s
110
+ )
111
+
112
+ # postgres async
113
+ if s.startswith("postgresql+asyncpg://") or s.startswith(
114
+ "postgres+asyncpg://"
115
+ ):
116
+ return EngineSpec(kind="postgres", async_=True, dsn=s)
117
+ # postgres sync
118
+ if s.startswith("postgresql://") or s.startswith("postgres://"):
119
+ return EngineSpec(kind="postgres", async_=False, dsn=s)
120
+
121
+ raise ValueError(f"Unsupported DSN: {s}")
122
+
123
+ # Mapping
124
+ m = x # type: ignore[assignment]
125
+
126
+ # Helpers
127
+ def _get_bool(key: str, *aliases: str, default: bool = False) -> bool:
128
+ for k in (key, *aliases):
129
+ if k in m:
130
+ return bool(m[k]) # type: ignore[index]
131
+ return default
132
+
133
+ def _get_str(
134
+ key: str, *aliases: str, default: Optional[str] = None
135
+ ) -> Optional[str]:
136
+ for k in (key, *aliases):
137
+ if k in m and m[k] is not None:
138
+ return str(m[k]) # type: ignore[index]
139
+ return default
140
+
141
+ def _get_int(
142
+ key: str, *aliases: str, default: Optional[int] = None
143
+ ) -> Optional[int]:
144
+ for k in (key, *aliases):
145
+ if k in m and m[k] is not None:
146
+ try:
147
+ return int(m[k]) # type: ignore[index]
148
+ except Exception:
149
+ return default
150
+ return default
151
+
152
+ k = str(m.get("kind", m.get("engine", ""))).lower() # type: ignore[index]
153
+ if k == "sqlite":
154
+ async_ = _get_bool("async", "async_", default=False)
155
+ path = _get_str("path")
156
+ memory = (
157
+ _get_bool("memory", default=False)
158
+ or (str(m.get("mode", "")).lower() == "memory")
159
+ or (path is None)
160
+ )
161
+ return EngineSpec(
162
+ kind="sqlite",
163
+ async_=async_,
164
+ path=path,
165
+ memory=memory,
166
+ dsn=_get_str("dsn", "url"),
167
+ mapping=m,
168
+ )
169
+
170
+ if k == "postgres":
171
+ async_ = _get_bool("async", "async_", default=False)
172
+ return EngineSpec(
173
+ kind="postgres",
174
+ async_=async_,
175
+ user=_get_str("user"),
176
+ pwd=_get_str("pwd", "password"),
177
+ host=_get_str("host"),
178
+ port=_get_int("port"),
179
+ name=_get_str("db", "name"),
180
+ pool_size=_get_int("pool_size", default=10) or 10,
181
+ max=_get_int("max", "max_overflow", "max_size", default=20) or 20,
182
+ dsn=_get_str("dsn", "url"),
183
+ mapping=m,
184
+ )
185
+
186
+ # External / unknown kinds – keep mapping and defer to registry at build()
187
+ return EngineSpec(
188
+ kind=k or None,
189
+ async_=_get_bool("async", "async_", default=False),
190
+ dsn=_get_str("dsn", "url"),
191
+ mapping=m,
192
+ )
193
+
194
+ # ---------- realization ----------
195
+
196
+ def build(self) -> Tuple[Any, SessionFactory]:
197
+ """Construct the engine and sessionmaker for this spec."""
198
+ if self.kind == "sqlite":
199
+ if self.memory:
200
+ if self.async_:
201
+ return async_sqlite_engine(path=None)
202
+ return blocking_sqlite_engine(path=None)
203
+ if not self.path:
204
+ raise ValueError("sqlite file requires 'path'")
205
+ if self.async_:
206
+ return async_sqlite_engine(path=self.path)
207
+ return blocking_sqlite_engine(path=self.path)
208
+
209
+ if self.kind == "postgres":
210
+ if self.dsn:
211
+ if self.async_:
212
+ return async_postgres_engine(dsn=self.dsn)
213
+ return blocking_postgres_engine(dsn=self.dsn)
214
+ # keyword build
215
+ kwargs: dict[str, Any] = {
216
+ "user": self.user or "app",
217
+ "host": self.host or "localhost",
218
+ "port": self.port or 5432,
219
+ "db": self.name or "app_db",
220
+ "pool_size": int(self.pool_size or 10),
221
+ }
222
+ if self.pwd is not None:
223
+ kwargs["pwd"] = self.pwd
224
+ if self.async_:
225
+ kwargs["max_size"] = int(self.max or 20)
226
+ return async_postgres_engine(**kwargs)
227
+ else:
228
+ kwargs["max_overflow"] = int(self.max or 20)
229
+ return blocking_postgres_engine(**kwargs)
230
+
231
+ # External/registered engines
232
+ try:
233
+ from .plugins import load_engine_plugins
234
+ from .registry import get_engine_registration, known_engine_kinds
235
+
236
+ load_engine_plugins()
237
+ reg = get_engine_registration(self.kind or "")
238
+ except Exception:
239
+ reg = None
240
+ if reg:
241
+ mapping = self.mapping or {}
242
+ return reg.build(mapping=mapping, spec=self, dsn=self.dsn)
243
+
244
+ # No registration found: helpful error
245
+ try:
246
+ from .registry import known_engine_kinds # re-import defensive
247
+
248
+ kinds = ", ".join(known_engine_kinds()) or "(none)"
249
+ except Exception:
250
+ kinds = "(unknown)"
251
+ raise RuntimeError(
252
+ f"Unknown or unavailable engine kind '{self.kind}'. Installed engine kinds: {kinds}. "
253
+ f"If this is an optional extension, install its package (e.g., 'pip install tigrbl_engine_{self.kind}')."
254
+ )
255
+
256
+ def supports(self) -> dict[str, Any]:
257
+ """Return capability dictionary for this engine spec.
258
+ For external kinds, consult the plugin registry if available.
259
+ """
260
+ # Built-ins
261
+ if self.kind == "sqlite":
262
+ try:
263
+ from .capabilities import sqlite_capabilities
264
+
265
+ return sqlite_capabilities(async_=self.async_, memory=self.memory)
266
+ except Exception:
267
+ pass
268
+ if self.kind == "postgres":
269
+ try:
270
+ from .capabilities import postgres_capabilities
271
+
272
+ return postgres_capabilities(async_=self.async_)
273
+ except Exception:
274
+ pass
275
+ # External/registered engines
276
+ try:
277
+ from .plugins import load_engine_plugins
278
+ from .registry import get_engine_registration
279
+
280
+ load_engine_plugins()
281
+ reg = get_engine_registration(self.kind or "")
282
+ except Exception:
283
+ reg = None
284
+ if reg and getattr(reg, "capabilities", None):
285
+ try:
286
+ # Try flexible signature: capabilities(spec=..., mapping=...)
287
+ return reg.capabilities(spec=self, mapping=self.mapping)
288
+ except TypeError:
289
+ try:
290
+ return reg.capabilities()
291
+ except Exception:
292
+ pass
293
+ except Exception:
294
+ pass
295
+ # Fallback minimal shape
296
+ return {
297
+ "transactional": False,
298
+ "async_native": bool(self.async_),
299
+ "isolation_levels": set(),
300
+ "read_only_enforced": False,
301
+ "engine": self.kind or "unknown",
302
+ }
303
+
304
+ def to_provider(self) -> Provider:
305
+ """Materialize a lazy :class:`Provider` for this spec."""
306
+ return Provider(self)
307
+
308
+ def __repr__(self) -> str: # pragma: no cover - deterministic output
309
+ def _redact_dsn(dsn: Optional[str]) -> Optional[str]:
310
+ if not dsn:
311
+ return dsn
312
+ try:
313
+ parts = urlsplit(dsn)
314
+ except Exception:
315
+ return dsn
316
+ if not parts.scheme or parts.password is None:
317
+ return dsn
318
+ user = parts.username or ""
319
+ userinfo = f"{user}:***" if user else "***"
320
+ host = parts.hostname or ""
321
+ netloc = f"{userinfo}@{host}" if host else userinfo
322
+ if parts.port is not None:
323
+ netloc = f"{netloc}:{parts.port}"
324
+ return urlunsplit(
325
+ (parts.scheme, netloc, parts.path, parts.query, parts.fragment)
326
+ )
327
+
328
+ def _redact_mapping(
329
+ mapping: Optional[Mapping[str, object]],
330
+ ) -> Optional[dict[str, object]]:
331
+ if mapping is None:
332
+ return None
333
+ redacted: dict[str, object] = {}
334
+ for key, value in mapping.items():
335
+ if str(key).lower() in {"pwd", "password", "pass", "secret"}:
336
+ redacted[key] = "***"
337
+ else:
338
+ redacted[key] = value
339
+ return redacted
340
+
341
+ fields = [
342
+ ("kind", self.kind),
343
+ ("async_", self.async_),
344
+ ("dsn", _redact_dsn(self.dsn)),
345
+ ("mapping", _redact_mapping(self.mapping)),
346
+ ("path", self.path),
347
+ ("memory", self.memory),
348
+ ("user", self.user),
349
+ ("host", self.host),
350
+ ("port", self.port),
351
+ ("name", self.name),
352
+ ("pool_size", self.pool_size),
353
+ ("max", self.max),
354
+ ]
355
+ return "EngineSpec(" + ", ".join(f"{k}={v!r}" for k, v in fields) + ")"
@@ -0,0 +1,52 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ _loaded = False
5
+
6
+
7
+ def load_engine_plugins() -> None:
8
+ """Discover and load external engine plugins via entry points.
9
+ Safe and idempotent; does nothing if already loaded.
10
+ """
11
+ global _loaded
12
+ if _loaded:
13
+ return
14
+
15
+ # importlib.metadata API differs across Python versions; support both.
16
+ eps = None
17
+ try:
18
+ from importlib.metadata import entry_points # Python >= 3.10
19
+
20
+ eps = entry_points()
21
+ # New API: .select(group="tigrbl.engine")
22
+ selected = (
23
+ eps.select(group="tigrbl.engine")
24
+ if hasattr(eps, "select")
25
+ else eps.get("tigrbl.engine", [])
26
+ )
27
+ except Exception:
28
+ try:
29
+ from importlib_metadata import entry_points as entry_points_backport
30
+
31
+ eps = entry_points_backport()
32
+ selected = (
33
+ eps.select(group="tigrbl.engine")
34
+ if hasattr(eps, "select")
35
+ else eps.get("tigrbl.engine", [])
36
+ )
37
+ except Exception:
38
+ selected = []
39
+
40
+ for ep in selected or []:
41
+ try:
42
+ fn = ep.load()
43
+ except Exception:
44
+ # Ignore broken entry points; the engine remains unavailable.
45
+ continue
46
+ try:
47
+ fn() # call plugin's register() to register_engine(kind, build, ...)
48
+ except Exception:
49
+ # Defensive: a broken plugin must not crash core import
50
+ continue
51
+
52
+ _loaded = True
@@ -0,0 +1,36 @@
1
+ from __future__ import annotations
2
+ from dataclasses import dataclass
3
+ from typing import Any, Callable, Optional, Tuple, Dict
4
+
5
+
6
+ # A registration for an engine kind provided by an external (or built-in) package.
7
+ @dataclass
8
+ class EngineRegistration:
9
+ build: Callable[..., Tuple[Any, Callable[[], Any]]]
10
+ capabilities: Optional[Callable[[], Any]] = None
11
+
12
+
13
+ _registry: Dict[str, EngineRegistration] = {}
14
+
15
+
16
+ def register_engine(
17
+ kind: str,
18
+ build: Callable[..., Tuple[Any, Callable[[], Any]]],
19
+ capabilities: Optional[Callable[[], Any]] = None,
20
+ ) -> None:
21
+ """Register an engine kind → (builder, capabilities). Idempotent."""
22
+ k = (kind or "").strip().lower()
23
+ if not k:
24
+ raise ValueError("engine kind must be a non-empty string")
25
+ if k in _registry:
26
+ # idempotent registration
27
+ return
28
+ _registry[k] = EngineRegistration(build=build, capabilities=capabilities)
29
+
30
+
31
+ def get_engine_registration(kind: str) -> Optional[EngineRegistration]:
32
+ return _registry.get((kind or "").strip().lower())
33
+
34
+
35
+ def known_engine_kinds() -> list[str]:
36
+ return sorted(_registry.keys())