tigrbl_engine_rom 0.4.5__tar.gz

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.
@@ -0,0 +1,62 @@
1
+ # Root policy: ignore every new root-level entry unless explicitly allowed.
2
+ /*
3
+
4
+ # Root entries intentionally allowed by repository policy.
5
+ !/.codex/
6
+ !/.github/
7
+ !/.gitignore
8
+ !/.ssot/
9
+ !/CODE_OF_CONDUCT.md
10
+ !/CONTRIBUTING.md
11
+ !/LICENSE
12
+ !/NOTICE
13
+ !/README.md
14
+ !/SECURITY.md
15
+ !/assets/
16
+ !/bindings/
17
+ !/docs/
18
+ !/examples/
19
+ !/pkgs/
20
+ !/pyproject.toml
21
+ !/tests/
22
+ !/tools/
23
+ !/uv.lock
24
+
25
+ # Generated caches and build artifacts.
26
+ __pycache__/
27
+ .pytest_cache/
28
+ .ruff_cache/
29
+ .mypy_cache/
30
+ .tox/
31
+ .nox/
32
+ .venv/
33
+ .coverage
34
+ htmlcov/
35
+ build/
36
+ dist/
37
+ site/
38
+ target/
39
+ *.egg-info/
40
+ *.pyc
41
+ *.pyd
42
+ *.tmp*
43
+ *.zip
44
+ *.body
45
+ perf.sqlite
46
+ node_modules/
47
+
48
+ # Root-local generated caches and temporary package artifacts.
49
+ /.benchmarks/
50
+ /.pip-cache/
51
+ /.tmp
52
+ /.tmp/
53
+ /.tmp*/
54
+ /.uv-cache*/
55
+ /.uv-pytest*/
56
+ /.uv-pytest-tigrbl-tests/
57
+ /.uv-python*/
58
+ /.vendor
59
+ /.venv*/
60
+ /.venv-gitvs*/
61
+ /generation_examples/
62
+ /tmp/
@@ -0,0 +1,17 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ https://www.apache.org/licenses/LICENSE-2.0
4
+
5
+ Copyright 2026 Swarmauri
6
+
7
+ Licensed under the Apache License, Version 2.0 (the "License");
8
+ you may not use this file except in compliance with the License.
9
+ You may obtain a copy of the License at
10
+
11
+ https://www.apache.org/licenses/LICENSE-2.0
12
+
13
+ Unless required by applicable law or agreed to in writing, software
14
+ distributed under the License is distributed on an "AS IS" BASIS,
15
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ See the License for the specific language governing permissions and
17
+ limitations under the License.
@@ -0,0 +1,7 @@
1
+ Tigrbl
2
+ Copyright 2026 Swarmauri
3
+
4
+ This product includes software developed by Swarmauri.
5
+
6
+ Tigrbl is licensed under the Apache License, Version 2.0.
7
+ See the LICENSE file distributed with this work for the full license text.
@@ -0,0 +1,167 @@
1
+ Metadata-Version: 2.5
2
+ Name: tigrbl_engine_rom
3
+ Version: 0.4.5
4
+ Summary: Immutable read-only memory engine plugin for Tigrbl.
5
+ Project-URL: Homepage, https://github.com/tigrbl/tigrbl
6
+ Project-URL: Repository, https://github.com/tigrbl/tigrbl/tree/master/pkgs/90_engines/tigrbl_engine_rom
7
+ Project-URL: Issues, https://github.com/tigrbl/tigrbl/issues
8
+ Author-email: Jacob Stewart <jacob@swarmauri.com>
9
+ License: Apache License
10
+ Version 2.0, January 2004
11
+ https://www.apache.org/licenses/LICENSE-2.0
12
+
13
+ Copyright 2026 Swarmauri
14
+
15
+ Licensed under the Apache License, Version 2.0 (the "License");
16
+ you may not use this file except in compliance with the License.
17
+ You may obtain a copy of the License at
18
+
19
+ https://www.apache.org/licenses/LICENSE-2.0
20
+
21
+ Unless required by applicable law or agreed to in writing, software
22
+ distributed under the License is distributed on an "AS IS" BASIS,
23
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
24
+ See the License for the specific language governing permissions and
25
+ limitations under the License.
26
+ License-File: LICENSE
27
+ License-File: NOTICE
28
+ Keywords: engine,immutable,memory,plugin,readonly,tigrbl
29
+ Classifier: Development Status :: 3 - Alpha
30
+ Classifier: Framework :: AsyncIO
31
+ Classifier: License :: OSI Approved :: Apache Software License
32
+ Classifier: Operating System :: OS Independent
33
+ Classifier: Programming Language :: Python :: 3
34
+ Classifier: Programming Language :: Python :: 3 :: Only
35
+ Classifier: Programming Language :: Python :: 3.10
36
+ Classifier: Programming Language :: Python :: 3.11
37
+ Classifier: Programming Language :: Python :: 3.12
38
+ Classifier: Programming Language :: Python :: 3.13
39
+ Classifier: Programming Language :: Python :: 3.14
40
+ Classifier: Topic :: Database
41
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
42
+ Requires-Python: <3.15,>=3.10
43
+ Requires-Dist: tigrbl>=0.3.0.dev4
44
+ Description-Content-Type: text/markdown
45
+
46
+ <div align="center">
47
+ <h1>tigrbl_engine_rom</h1>
48
+ <p><strong>Immutable read-only memory engine plugin for Tigrbl.</strong></p>
49
+ <a href="https://pypi.org/project/tigrbl_engine_rom/"><img src="https://img.shields.io/pypi/v/tigrbl_engine_rom?label=PyPI" alt="PyPI version"/></a>
50
+ <a href="https://discord.gg/K4YTAPapjR"><img src="https://img.shields.io/badge/Discord-Join%20chat-5865F2?logo=discord&logoColor=white" alt="Discord"/></a>
51
+ <a href="LICENSE"><img src="https://img.shields.io/badge/license-Apache%202.0-525252" alt="Apache 2.0 license"/></a>
52
+ <a href="pyproject.toml"><img src="https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12%20%7C%203.13%20%7C%203.14-3776ab" alt="Python 3.10 | 3.11 | 3.12 | 3.13 | 3.14"/></a>
53
+ </div>
54
+
55
+ ## What is tigrbl_engine_rom?
56
+
57
+ `tigrbl_engine_rom` loads one mapping-backed row image and serves it through Tigrbl's engine-session contract. The engine configuration is bound as an input to a table.
58
+
59
+ ## Why use tigrbl_engine_rom?
60
+
61
+ Use it for embedded catalogs, fixtures, policy data, reference tables, and other datasets that must never change at runtime.
62
+
63
+ ## When should I install tigrbl_engine_rom?
64
+
65
+ Install it when your application needs fast reads from a fixed memory image and should fail closed on every write.
66
+
67
+ ## Who is tigrbl_engine_rom for?
68
+
69
+ It is for application developers shipping trusted reference data or deterministic read-only test images.
70
+
71
+ ## Where does tigrbl_engine_rom fit?
72
+
73
+ It is a standalone package under `pkgs/90_engines` and is discovered through the `tigrbl.engine` entry-point group as kind `rom`.
74
+
75
+ ## How does tigrbl_engine_rom work?
76
+
77
+ `ROMEngine` subclasses Tigrbl's `EngineBase`. Construction validates the bound table's rows and primary key, deeply detaches the source data, and freezes it. Reads always return caller-owned copies. Sessions force `read_only=True`, even if a caller requests otherwise.
78
+
79
+ ## Install
80
+
81
+ ```bash
82
+ uv add tigrbl_engine_rom
83
+ ```
84
+
85
+ ## Surface Coverage
86
+
87
+ | Surface | Value |
88
+ |---|---|
89
+ | Engine kind | `rom` |
90
+ | Python import | `tigrbl_engine_rom` |
91
+ | Entry point | `tigrbl.engine` |
92
+ | Persistence | process-local immutable memory |
93
+ | Supported Python | `3.10 | 3.11 | 3.12 | 3.13 | 3.14` |
94
+
95
+ ## What It Owns
96
+
97
+ The package owns the ROM image, read-only session, plugin registration, and scalar result facade.
98
+
99
+ ## Public API and Import Surface
100
+
101
+ Public symbols are `ROMEngine`, `ROMSession`, `ROMResult`, `build_rom`, `capabilities`, and `register`.
102
+
103
+ ## Usage Examples
104
+
105
+ ```python
106
+ from tigrbl_engine_rom import build_rom
107
+
108
+ engine, sessionmaker = build_rom(mapping={
109
+ "rows": [
110
+ {"code": "US", "name": "United States"},
111
+ {"code": "CA", "name": "Canada"},
112
+ ],
113
+ "primary_key": "code",
114
+ })
115
+
116
+ session = sessionmaker()
117
+ assert session.get_row("CA")["name"] == "Canada"
118
+ ```
119
+
120
+ Bind the engine configuration into a table:
121
+
122
+ ```python
123
+ from tigrbl.factories.table import defineTableSpec
124
+
125
+ class CountrySpec(defineTableSpec(engine={
126
+ "kind": "rom",
127
+ "rows": [
128
+ {"code": "US", "name": "United States"},
129
+ {"code": "CA", "name": "Canada"},
130
+ ],
131
+ "primary_key": "code",
132
+ })):
133
+ pass
134
+ ```
135
+
136
+ ## How To Choose This Package
137
+
138
+ Choose ROM for immutable embedded data. Choose `tigrbl_engine_inmemory` when transactions and runtime writes are required.
139
+
140
+ ## Related Packages
141
+
142
+ - `tigrbl`
143
+ - `tigrbl_engine_inmemory`
144
+ - `tigrbl_engine_memkv`
145
+
146
+ ## Documentation Links
147
+
148
+ - [Workspace docs](../../../docs/README.md)
149
+ - [Package catalog](../../../docs/developer/PACKAGE_CATALOG.md)
150
+ - [SSOT registry](../../../.ssot/registry.json)
151
+
152
+ ## Support
153
+
154
+ - [Discord](https://discord.gg/K4YTAPapjR)
155
+ - [GitHub issues](https://github.com/tigrbl/tigrbl/issues)
156
+
157
+ ## Certification Status
158
+
159
+ The package is implemented with package-local behavioral tests. Certification remains governed by the repository SSOT registry and release workflows.
160
+
161
+ ## License
162
+
163
+ Licensed under the Apache License, Version 2.0. See `LICENSE` and `NOTICE`.
164
+
165
+ ## Workspace documentation
166
+
167
+ This README is the package-local distribution entry point. See [workspace documentation](../../../docs/README.md), [current conformance target](../../../docs/conformance/CURRENT_TARGET.md), [package catalog](../../../docs/developer/PACKAGE_CATALOG.md), [package layout](../../../docs/developer/PACKAGE_LAYOUT.md), and [SSOT registry](../../../.ssot/registry.json).
@@ -0,0 +1,122 @@
1
+ <div align="center">
2
+ <h1>tigrbl_engine_rom</h1>
3
+ <p><strong>Immutable read-only memory engine plugin for Tigrbl.</strong></p>
4
+ <a href="https://pypi.org/project/tigrbl_engine_rom/"><img src="https://img.shields.io/pypi/v/tigrbl_engine_rom?label=PyPI" alt="PyPI version"/></a>
5
+ <a href="https://discord.gg/K4YTAPapjR"><img src="https://img.shields.io/badge/Discord-Join%20chat-5865F2?logo=discord&logoColor=white" alt="Discord"/></a>
6
+ <a href="LICENSE"><img src="https://img.shields.io/badge/license-Apache%202.0-525252" alt="Apache 2.0 license"/></a>
7
+ <a href="pyproject.toml"><img src="https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12%20%7C%203.13%20%7C%203.14-3776ab" alt="Python 3.10 | 3.11 | 3.12 | 3.13 | 3.14"/></a>
8
+ </div>
9
+
10
+ ## What is tigrbl_engine_rom?
11
+
12
+ `tigrbl_engine_rom` loads one mapping-backed row image and serves it through Tigrbl's engine-session contract. The engine configuration is bound as an input to a table.
13
+
14
+ ## Why use tigrbl_engine_rom?
15
+
16
+ Use it for embedded catalogs, fixtures, policy data, reference tables, and other datasets that must never change at runtime.
17
+
18
+ ## When should I install tigrbl_engine_rom?
19
+
20
+ Install it when your application needs fast reads from a fixed memory image and should fail closed on every write.
21
+
22
+ ## Who is tigrbl_engine_rom for?
23
+
24
+ It is for application developers shipping trusted reference data or deterministic read-only test images.
25
+
26
+ ## Where does tigrbl_engine_rom fit?
27
+
28
+ It is a standalone package under `pkgs/90_engines` and is discovered through the `tigrbl.engine` entry-point group as kind `rom`.
29
+
30
+ ## How does tigrbl_engine_rom work?
31
+
32
+ `ROMEngine` subclasses Tigrbl's `EngineBase`. Construction validates the bound table's rows and primary key, deeply detaches the source data, and freezes it. Reads always return caller-owned copies. Sessions force `read_only=True`, even if a caller requests otherwise.
33
+
34
+ ## Install
35
+
36
+ ```bash
37
+ uv add tigrbl_engine_rom
38
+ ```
39
+
40
+ ## Surface Coverage
41
+
42
+ | Surface | Value |
43
+ |---|---|
44
+ | Engine kind | `rom` |
45
+ | Python import | `tigrbl_engine_rom` |
46
+ | Entry point | `tigrbl.engine` |
47
+ | Persistence | process-local immutable memory |
48
+ | Supported Python | `3.10 | 3.11 | 3.12 | 3.13 | 3.14` |
49
+
50
+ ## What It Owns
51
+
52
+ The package owns the ROM image, read-only session, plugin registration, and scalar result facade.
53
+
54
+ ## Public API and Import Surface
55
+
56
+ Public symbols are `ROMEngine`, `ROMSession`, `ROMResult`, `build_rom`, `capabilities`, and `register`.
57
+
58
+ ## Usage Examples
59
+
60
+ ```python
61
+ from tigrbl_engine_rom import build_rom
62
+
63
+ engine, sessionmaker = build_rom(mapping={
64
+ "rows": [
65
+ {"code": "US", "name": "United States"},
66
+ {"code": "CA", "name": "Canada"},
67
+ ],
68
+ "primary_key": "code",
69
+ })
70
+
71
+ session = sessionmaker()
72
+ assert session.get_row("CA")["name"] == "Canada"
73
+ ```
74
+
75
+ Bind the engine configuration into a table:
76
+
77
+ ```python
78
+ from tigrbl.factories.table import defineTableSpec
79
+
80
+ class CountrySpec(defineTableSpec(engine={
81
+ "kind": "rom",
82
+ "rows": [
83
+ {"code": "US", "name": "United States"},
84
+ {"code": "CA", "name": "Canada"},
85
+ ],
86
+ "primary_key": "code",
87
+ })):
88
+ pass
89
+ ```
90
+
91
+ ## How To Choose This Package
92
+
93
+ Choose ROM for immutable embedded data. Choose `tigrbl_engine_inmemory` when transactions and runtime writes are required.
94
+
95
+ ## Related Packages
96
+
97
+ - `tigrbl`
98
+ - `tigrbl_engine_inmemory`
99
+ - `tigrbl_engine_memkv`
100
+
101
+ ## Documentation Links
102
+
103
+ - [Workspace docs](../../../docs/README.md)
104
+ - [Package catalog](../../../docs/developer/PACKAGE_CATALOG.md)
105
+ - [SSOT registry](../../../.ssot/registry.json)
106
+
107
+ ## Support
108
+
109
+ - [Discord](https://discord.gg/K4YTAPapjR)
110
+ - [GitHub issues](https://github.com/tigrbl/tigrbl/issues)
111
+
112
+ ## Certification Status
113
+
114
+ The package is implemented with package-local behavioral tests. Certification remains governed by the repository SSOT registry and release workflows.
115
+
116
+ ## License
117
+
118
+ Licensed under the Apache License, Version 2.0. See `LICENSE` and `NOTICE`.
119
+
120
+ ## Workspace documentation
121
+
122
+ This README is the package-local distribution entry point. See [workspace documentation](../../../docs/README.md), [current conformance target](../../../docs/conformance/CURRENT_TARGET.md), [package catalog](../../../docs/developer/PACKAGE_CATALOG.md), [package layout](../../../docs/developer/PACKAGE_LAYOUT.md), and [SSOT registry](../../../.ssot/registry.json).
@@ -0,0 +1,47 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.25.0"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "tigrbl_engine_rom"
7
+ version = "0.4.5"
8
+ description = "Immutable read-only memory engine plugin for Tigrbl."
9
+ readme = "README.md"
10
+ license = { file = "LICENSE" }
11
+ keywords = ["tigrbl", "engine", "plugin", "memory", "readonly", "immutable"]
12
+ classifiers = [
13
+ "Development Status :: 3 - Alpha",
14
+ "Framework :: AsyncIO",
15
+ "License :: OSI Approved :: Apache Software License",
16
+ "Operating System :: OS Independent",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3 :: Only",
19
+ "Programming Language :: Python :: 3.10",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Programming Language :: Python :: 3.13",
23
+ "Programming Language :: Python :: 3.14",
24
+ "Topic :: Database",
25
+ "Topic :: Software Development :: Libraries :: Python Modules",
26
+ ]
27
+ authors = [{ name = "Jacob Stewart", email = "jacob@swarmauri.com" }]
28
+ requires-python = ">=3.10,<3.15"
29
+ dependencies = ["tigrbl>=0.3.0.dev4"]
30
+
31
+ [project.urls]
32
+ Homepage = "https://github.com/tigrbl/tigrbl"
33
+ Repository = "https://github.com/tigrbl/tigrbl/tree/master/pkgs/90_engines/tigrbl_engine_rom"
34
+ Issues = "https://github.com/tigrbl/tigrbl/issues"
35
+
36
+ [project.entry-points."tigrbl.engine_plugins"]
37
+ rom = "tigrbl_engine_rom.plugin:register"
38
+
39
+ [dependency-groups]
40
+ dev = ["pytest>=8.0", "pytest-asyncio>=0.24.0", "ruff>=0.11.0", "sqlalchemy>=2.0"]
41
+
42
+ [tool.uv.sources]
43
+ tigrbl = { workspace = true }
44
+
45
+ [tool.pytest.ini_options]
46
+ asyncio_mode = "auto"
47
+ testpaths = ["tests"]
@@ -0,0 +1,18 @@
1
+ """Read-only memory engine for Tigrbl."""
2
+
3
+ from .engine import ROMEngine
4
+ from .plugin import build_rom, capabilities, register
5
+ from .session import READ_ONLY_ERROR, ROMResult, ROMSession
6
+
7
+ __all__ = [
8
+ "READ_ONLY_ERROR",
9
+ "ROMEngine",
10
+ "ROMResult",
11
+ "ROMSession",
12
+ "build_rom",
13
+ "capabilities",
14
+ "register",
15
+ "__version__",
16
+ ]
17
+
18
+ __version__ = "0.4.5.dev4"
@@ -0,0 +1,105 @@
1
+ from __future__ import annotations
2
+
3
+ from copy import deepcopy
4
+ from types import MappingProxyType
5
+ from typing import Any, Iterable, Mapping
6
+
7
+ from tigrbl_base._base import EngineBase
8
+
9
+
10
+ def _freeze(value: Any) -> Any:
11
+ """Return an immutable, detached representation of configuration data."""
12
+ if isinstance(value, Mapping):
13
+ return MappingProxyType({key: _freeze(item) for key, item in value.items()})
14
+ if isinstance(value, (list, tuple)):
15
+ return tuple(_freeze(item) for item in value)
16
+ if isinstance(value, (set, frozenset)):
17
+ return frozenset(_freeze(item) for item in value)
18
+ return deepcopy(value)
19
+
20
+
21
+ def _thaw(value: Any) -> Any:
22
+ """Return a caller-owned copy of a frozen value."""
23
+ if isinstance(value, Mapping):
24
+ return {key: _thaw(item) for key, item in value.items()}
25
+ if isinstance(value, tuple):
26
+ return [_thaw(item) for item in value]
27
+ if isinstance(value, frozenset):
28
+ return {_thaw(item) for item in value}
29
+ return deepcopy(value)
30
+
31
+
32
+ class ROMEngine(EngineBase):
33
+ """Process-local immutable row image bound as an input to one table."""
34
+
35
+ def __init__(
36
+ self,
37
+ rows: Iterable[Mapping[str, Any]] | None = None,
38
+ *,
39
+ primary_key: str = "id",
40
+ namespace: str = "default",
41
+ spec: Any = None,
42
+ ) -> None:
43
+ if not isinstance(namespace, str) or not namespace:
44
+ raise ValueError("namespace must be a non-empty string")
45
+ if not isinstance(primary_key, str) or not primary_key:
46
+ raise ValueError("primary_key must be a non-empty string")
47
+ self.namespace = namespace
48
+ self.spec = spec
49
+ self.primary_key = primary_key
50
+ frozen_rows: list[Mapping[str, Any]] = []
51
+ index: dict[Any, Mapping[str, Any]] = {}
52
+ for position, source_row in enumerate(rows or ()):
53
+ if not isinstance(source_row, Mapping):
54
+ raise TypeError(f"row {position} must be a mapping")
55
+ if primary_key not in source_row:
56
+ raise ValueError(
57
+ f"row {position} is missing primary key {primary_key!r}"
58
+ )
59
+ key = deepcopy(source_row[primary_key])
60
+ try:
61
+ duplicate = key in index
62
+ except TypeError as exc:
63
+ raise TypeError(
64
+ f"primary key {primary_key!r} must be hashable"
65
+ ) from exc
66
+ if duplicate:
67
+ raise ValueError(f"duplicate primary key {key!r}")
68
+ frozen_row = _freeze(source_row)
69
+ frozen_rows.append(frozen_row)
70
+ index[key] = frozen_row
71
+
72
+ self._rows = tuple(frozen_rows)
73
+ self._index = MappingProxyType(index)
74
+
75
+ def to_provider(self) -> "ROMEngine":
76
+ """Expose this immutable engine as its own provider-like object."""
77
+ return self
78
+
79
+ async def _executeloop_impl(self, statements: Any) -> Any:
80
+ from .session import ROMSession
81
+
82
+ session = ROMSession(self)
83
+ try:
84
+ return await session.executeloop(statements)
85
+ finally:
86
+ await session.close()
87
+
88
+ async def _executemany_impl(self, stmt: Any, parameter_sets: Any) -> Any:
89
+ from .session import ROMSession
90
+
91
+ session = ROMSession(self)
92
+ try:
93
+ return await session.executemany(stmt, parameter_sets)
94
+ finally:
95
+ await session.close()
96
+
97
+ def get(self, ident: Any) -> dict[str, Any] | None:
98
+ row = self._index.get(ident)
99
+ return None if row is None else _thaw(row)
100
+
101
+ def scan(self) -> list[dict[str, Any]]:
102
+ return [_thaw(row) for row in self._rows]
103
+
104
+
105
+ __all__ = ["ROMEngine"]
@@ -0,0 +1,54 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from tigrbl.engine.registry import register_engine
6
+
7
+ from .engine import ROMEngine
8
+ from .session import ROMSession
9
+
10
+
11
+ def capabilities(*, spec: Any = None, mapping: Any = None) -> dict[str, Any]:
12
+ return {
13
+ "engine": "rom",
14
+ "transactional": False,
15
+ "async_native": True,
16
+ "isolation_levels": set(),
17
+ "read_only_enforced": True,
18
+ "persistence": "process",
19
+ "storage": "immutable-memory",
20
+ "read_consistency": "immutable-snapshot",
21
+ "binding_scopes": {"table"},
22
+ }
23
+
24
+
25
+ def build_rom(*, mapping=None, spec=None, dsn=None, **_: Any) -> tuple[object, object]:
26
+ if dsn:
27
+ raise ValueError("ROM engine accepts embedded mapping data, not a DSN")
28
+ config = dict(mapping or {})
29
+ engine = ROMEngine(
30
+ rows=config.get("rows"),
31
+ primary_key=str(config.get("primary_key", "id")),
32
+ namespace=str(config.get("namespace", "default")),
33
+ spec=spec,
34
+ )
35
+
36
+ def sessionmaker(session_spec=None) -> ROMSession:
37
+ return ROMSession(engine, spec=session_spec)
38
+
39
+ return engine, sessionmaker
40
+
41
+
42
+ class _Registration:
43
+ def build(self, *, mapping, spec, dsn):
44
+ return build_rom(mapping=mapping, spec=spec, dsn=dsn)
45
+
46
+ def capabilities(self, *, spec, mapping=None):
47
+ return capabilities(spec=spec, mapping=mapping)
48
+
49
+
50
+ def register() -> None:
51
+ register_engine("rom", _Registration())
52
+
53
+
54
+ __all__ = ["build_rom", "capabilities", "register"]
@@ -0,0 +1,227 @@
1
+ from __future__ import annotations
2
+
3
+ from copy import deepcopy
4
+ from dataclasses import replace
5
+ from typing import Any, Callable, Iterable, Mapping, Sequence
6
+
7
+ from tigrbl_base._base import EngineSessionBase
8
+ from tigrbl_core._spec.engine_session_spec import EngineSessionSpec
9
+
10
+ from .engine import ROMEngine
11
+
12
+
13
+ READ_ONLY_ERROR = "write attempted against read-only memory engine"
14
+
15
+
16
+ class ROMResult:
17
+ """Small result facade compatible with Tigrbl's scalar query helpers."""
18
+
19
+ def __init__(self, items: Sequence[Any]) -> None:
20
+ self._items = tuple(items)
21
+ self.rowcount = len(self._items)
22
+
23
+ def scalars(self) -> "ROMResult":
24
+ return self
25
+
26
+ def all(self) -> list[Any]:
27
+ return list(self._items)
28
+
29
+ def first(self) -> Any | None:
30
+ return self._items[0] if self._items else None
31
+
32
+ def scalar_one(self) -> Any:
33
+ if len(self._items) != 1:
34
+ raise RuntimeError(f"expected exactly one row, found {len(self._items)}")
35
+ return self._items[0]
36
+
37
+
38
+ class ROMSession(EngineSessionBase):
39
+ """Async Tigrbl session over an immutable :class:`ROMEngine` snapshot."""
40
+
41
+ def __init__(
42
+ self, engine: ROMEngine, spec: EngineSessionSpec | None = None
43
+ ) -> None:
44
+ enforced = replace(spec or EngineSessionSpec(), read_only=True)
45
+ super().__init__(enforced)
46
+ self._engine = engine
47
+ self._closed = False
48
+ self._tracked: dict[int, tuple[Any, dict[str, Any]]] = {}
49
+
50
+ def apply_spec(self, spec: EngineSessionSpec | None) -> None:
51
+ enforced = replace(spec or EngineSessionSpec(), read_only=True)
52
+ super().apply_spec(enforced)
53
+
54
+ def get_row(self, ident: Any) -> dict[str, Any] | None:
55
+ self._require_open_session()
56
+ return self._engine.get(ident)
57
+
58
+ def query_rows(
59
+ self,
60
+ predicate: Callable[[Mapping[str, Any]], bool] | None = None,
61
+ ) -> list[dict[str, Any]]:
62
+ self._require_open_session()
63
+ rows = self._engine.scan()
64
+ if predicate is not None:
65
+ rows = [row for row in rows if predicate(row)]
66
+ return rows
67
+
68
+ async def _tx_begin_impl(self) -> None:
69
+ self._require_open_session()
70
+
71
+ async def _tx_commit_impl(self) -> None:
72
+ self._require_open_session()
73
+
74
+ async def _tx_rollback_impl(self) -> None:
75
+ self._require_open_session()
76
+ for obj, snapshot in self._tracked.values():
77
+ for name, value in snapshot.items():
78
+ setattr(obj, name, deepcopy(value))
79
+
80
+ def _add_impl(self, obj: Any) -> None:
81
+ raise RuntimeError(READ_ONLY_ERROR)
82
+
83
+ async def _delete_impl(self, obj: Any) -> None:
84
+ raise RuntimeError(READ_ONLY_ERROR)
85
+
86
+ async def _flush_impl(self) -> None:
87
+ self._require_open_session()
88
+ for obj, snapshot in self._tracked.values():
89
+ if self._snapshot(obj) != snapshot:
90
+ raise RuntimeError(READ_ONLY_ERROR)
91
+
92
+ async def _refresh_impl(self, obj: Any) -> None:
93
+ self._require_open_session()
94
+ row = self._engine.get(getattr(obj, self._engine.primary_key))
95
+ if row is None:
96
+ return
97
+ for name, value in row.items():
98
+ setattr(obj, name, value)
99
+ self._track(obj)
100
+
101
+ async def _get_impl(self, model: type, ident: Any) -> Any | None:
102
+ self._require_open_session()
103
+ row = self._engine.get(ident)
104
+ return None if row is None else self._inflate(model, row)
105
+
106
+ async def _execute_impl(self, stmt: Any) -> ROMResult:
107
+ self._require_open_session()
108
+ if type(stmt).__name__.lower() != "select":
109
+ raise RuntimeError(READ_ONLY_ERROR)
110
+ model = self._extract_model(stmt)
111
+ items = [self._inflate(model, row) for row in self._engine.scan()]
112
+ for name, operator, value in self._extract_predicates(stmt):
113
+ if operator == "eq":
114
+ items = [item for item in items if getattr(item, name, None) == value]
115
+ elif operator == "in":
116
+ accepted = set(value)
117
+ items = [
118
+ item for item in items if getattr(item, name, None) in accepted
119
+ ]
120
+ else: # pragma: no cover - protected by extraction
121
+ raise NotImplementedError(f"unsupported ROM predicate: {operator}")
122
+ for name, descending in reversed(self._extract_ordering(stmt)):
123
+ items.sort(key=lambda item: getattr(item, name, None), reverse=descending)
124
+ offset = self._clause_int(getattr(stmt, "_offset_clause", None)) or 0
125
+ limit = self._clause_int(getattr(stmt, "_limit_clause", None))
126
+ items = items[max(0, offset) :]
127
+ if limit is not None:
128
+ items = items[: max(0, limit)]
129
+ return ROMResult(items)
130
+
131
+ async def _executeloop_impl(self, statements: Iterable[Any]) -> list[ROMResult]:
132
+ return [await self._execute_impl(statement) for statement in statements]
133
+
134
+ async def _executemany_impl(self, stmt: Any, parameter_sets: Any) -> Any:
135
+ raise RuntimeError(
136
+ "parameterized batch execution is unavailable on read-only memory engine"
137
+ )
138
+
139
+ async def _close_impl(self) -> None:
140
+ self._closed = True
141
+ self._open = False
142
+ self._dirty = False
143
+ self._tracked.clear()
144
+
145
+ def _require_open_session(self) -> None:
146
+ if self._closed:
147
+ raise RuntimeError("ROM session is closed")
148
+
149
+ def _inflate(self, model: type, row: Mapping[str, Any]) -> Any:
150
+ try:
151
+ obj = model(**row)
152
+ except TypeError:
153
+ obj = model()
154
+ for name, value in row.items():
155
+ setattr(obj, name, value)
156
+ self._track(obj)
157
+ return obj
158
+
159
+ def _track(self, obj: Any) -> None:
160
+ self._tracked[id(obj)] = (obj, self._snapshot(obj))
161
+
162
+ @staticmethod
163
+ def _snapshot(obj: Any) -> dict[str, Any]:
164
+ return {
165
+ name: deepcopy(value)
166
+ for name, value in vars(obj).items()
167
+ if not name.startswith("_sa_")
168
+ }
169
+
170
+ @staticmethod
171
+ def _extract_model(stmt: Any) -> type:
172
+ for description in getattr(stmt, "column_descriptions", ()):
173
+ entity = description.get("entity")
174
+ if isinstance(entity, type):
175
+ return entity
176
+ raise NotImplementedError("ROM selects must target one mapped model")
177
+
178
+ @classmethod
179
+ def _extract_predicates(cls, stmt: Any) -> list[tuple[str, str, Any]]:
180
+ predicates: list[tuple[str, str, Any]] = []
181
+ for criterion in getattr(stmt, "_where_criteria", ()):
182
+ clauses = getattr(criterion, "clauses", None)
183
+ if clauses is not None:
184
+ boolean_operator = getattr(
185
+ getattr(criterion, "operator", None), "__name__", ""
186
+ )
187
+ if boolean_operator != "and_":
188
+ raise NotImplementedError(
189
+ "ROM supports conjunctions of equality and IN predicates only"
190
+ )
191
+ for clause in clauses:
192
+ predicates.extend(cls._extract_binary_predicate(clause))
193
+ else:
194
+ predicates.extend(cls._extract_binary_predicate(criterion))
195
+ return predicates
196
+
197
+ @staticmethod
198
+ def _extract_binary_predicate(expr: Any) -> list[tuple[str, str, Any]]:
199
+ name = getattr(getattr(expr, "left", None), "key", None)
200
+ operator_name = getattr(getattr(expr, "operator", None), "__name__", "")
201
+ right = getattr(expr, "right", None)
202
+ value = getattr(right, "effective_value", getattr(right, "value", None))
203
+ if not name or operator_name not in {"eq", "in_op"}:
204
+ raise NotImplementedError("ROM supports equality and IN predicates only")
205
+ return [(name, "in" if operator_name == "in_op" else "eq", value)]
206
+
207
+ @staticmethod
208
+ def _extract_ordering(stmt: Any) -> list[tuple[str, bool]]:
209
+ ordering: list[tuple[str, bool]] = []
210
+ for clause in getattr(stmt, "_order_by_clauses", ()):
211
+ element = getattr(clause, "element", clause)
212
+ name = getattr(element, "key", None)
213
+ if not name:
214
+ raise NotImplementedError("ROM ordering must target mapped columns")
215
+ modifier = getattr(getattr(clause, "modifier", None), "__name__", "")
216
+ ordering.append((name, modifier == "desc_op"))
217
+ return ordering
218
+
219
+ @staticmethod
220
+ def _clause_int(clause: Any) -> int | None:
221
+ if clause is None:
222
+ return None
223
+ value = getattr(clause, "effective_value", getattr(clause, "value", clause))
224
+ return int(value)
225
+
226
+
227
+ __all__ = ["READ_ONLY_ERROR", "ROMResult", "ROMSession"]
@@ -0,0 +1,229 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ import pytest
6
+ from sqlalchemy import Integer, String, delete, or_, select
7
+ from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
8
+ from tigrbl.factories.table import defineTableSpec
9
+ from tigrbl_base._base import EngineBase
10
+ from tigrbl_concrete._concrete import engine_resolver
11
+ from tigrbl_core._spec.engine_spec import EngineSpec
12
+ from tigrbl_core._spec.engine_session_spec import EngineSessionSpec
13
+ from tigrbl_ops_oltp.crud.ops import merge, replace as replace_row, update
14
+
15
+ from tigrbl_engine_rom import (
16
+ ROMEngine,
17
+ ROMSession,
18
+ build_rom,
19
+ capabilities,
20
+ register,
21
+ )
22
+
23
+
24
+ class Base(DeclarativeBase):
25
+ pass
26
+
27
+
28
+ class Widget(Base):
29
+ __tablename__ = "widgets"
30
+
31
+ id: Mapped[int] = mapped_column(Integer, primary_key=True)
32
+ name: Mapped[str] = mapped_column(String)
33
+ rank: Mapped[int] = mapped_column(Integer)
34
+
35
+
36
+ @dataclass
37
+ class Record:
38
+ id: int
39
+ value: str
40
+
41
+
42
+ def test_engine_detaches_and_protects_nested_source_data() -> None:
43
+ source = [{"id": 1, "value": "alpha", "tags": ["stable"]}]
44
+ engine = ROMEngine(source)
45
+ assert isinstance(engine, EngineBase)
46
+ assert engine.to_provider() is engine
47
+
48
+ source[0]["value"] = "changed"
49
+ source[0]["tags"].append("mutated")
50
+ first = engine.get(1)
51
+ assert first == {"id": 1, "value": "alpha", "tags": ["stable"]}
52
+
53
+ assert first is not None
54
+ first["value"] = "caller mutation"
55
+ first["tags"].append("caller mutation")
56
+ assert engine.get(1) == {
57
+ "id": 1,
58
+ "value": "alpha",
59
+ "tags": ["stable"],
60
+ }
61
+
62
+
63
+ @pytest.mark.parametrize(
64
+ ("rows", "message"),
65
+ [
66
+ ([{"value": "missing"}], "missing primary key"),
67
+ ([{"id": 1}, {"id": 1}], "duplicate primary key"),
68
+ (["not-a-row"], "must be a mapping"),
69
+ ],
70
+ )
71
+ def test_engine_rejects_invalid_rom_images(rows, message: str) -> None:
72
+ with pytest.raises((TypeError, ValueError), match=message):
73
+ ROMEngine(rows)
74
+
75
+
76
+ def test_engine_supports_a_custom_primary_key() -> None:
77
+ engine = ROMEngine(
78
+ [{"name": "theme", "value": "dark"}],
79
+ primary_key="name",
80
+ )
81
+ assert engine.primary_key == "name"
82
+ assert engine.get("theme") == {"name": "theme", "value": "dark"}
83
+
84
+
85
+ def test_rom_configuration_is_an_input_to_a_table_spec() -> None:
86
+ rom = {
87
+ "kind": "rom",
88
+ "rows": [{"id": 1, "name": "one", "rank": 1}],
89
+ "primary_key": "id",
90
+ }
91
+ Spec = defineTableSpec(engine=rom)
92
+
93
+ assert Spec.table_config["engine"] is rom
94
+ assert Spec.table_config["db"] is rom
95
+
96
+
97
+ @pytest.mark.asyncio
98
+ async def test_table_bound_engine_resolves_a_rom_session_for_that_model() -> None:
99
+ register()
100
+ engine_resolver.reset(dispose=False)
101
+ rom = {
102
+ "kind": "rom",
103
+ "rows": [{"id": 1, "name": "one", "rank": 1}],
104
+ }
105
+ Spec = defineTableSpec(engine=rom)
106
+ engine_resolver.register_table(Widget, Spec.table_config["engine"])
107
+
108
+ session, _release = engine_resolver.acquire(model=Widget)
109
+ try:
110
+ widget = await session.get(Widget, 1)
111
+ assert widget.name == "one"
112
+ finally:
113
+ await session.close()
114
+ engine_resolver.reset(dispose=False)
115
+
116
+
117
+ @pytest.mark.asyncio
118
+ async def test_session_supports_point_reads_scans_and_sqlalchemy_selects() -> None:
119
+ engine = ROMEngine(
120
+ [
121
+ {"id": 1, "name": "one", "rank": 2},
122
+ {"id": 2, "name": "two", "rank": 1},
123
+ {"id": 3, "name": "three", "rank": 3},
124
+ ]
125
+ )
126
+ session = ROMSession(engine)
127
+
128
+ widget = await session.get(Widget, 2)
129
+ assert (widget.id, widget.name, widget.rank) == (2, "two", 1)
130
+ assert await session.get(Widget, 99) is None
131
+ assert session.query_rows(lambda row: row["rank"] > 1) == [
132
+ {"id": 1, "name": "one", "rank": 2},
133
+ {"id": 3, "name": "three", "rank": 3},
134
+ ]
135
+
136
+ result = await session.execute(
137
+ select(Widget)
138
+ .where(Widget.id.in_([1, 3]))
139
+ .order_by(Widget.rank.desc())
140
+ .limit(1)
141
+ )
142
+ assert [item.id for item in result.scalars().all()] == [3]
143
+
144
+ result = await session.execute(select(Widget).where(Widget.name == "two"))
145
+ assert result.scalar_one().id == 2
146
+
147
+ with pytest.raises(NotImplementedError, match="conjunctions"):
148
+ await session.execute(select(Widget).where(or_(Widget.id == 1, Widget.id == 2)))
149
+
150
+
151
+ @pytest.mark.asyncio
152
+ async def test_session_is_always_read_only_and_lifecycle_safe() -> None:
153
+ engine = ROMEngine([{"id": 1, "value": "alpha"}])
154
+ session = ROMSession(engine, EngineSessionSpec(read_only=False))
155
+ assert session.read_only is True
156
+
157
+ with pytest.raises(RuntimeError, match="read-only"):
158
+ session.add(Record(2, "beta"))
159
+ with pytest.raises(RuntimeError, match="read-only"):
160
+ await session.delete(Record(1, "alpha"))
161
+ with pytest.raises(RuntimeError, match="read-only"):
162
+ await session.execute(delete(Widget))
163
+ with pytest.raises(RuntimeError, match="read-only"):
164
+ await session.executemany(select(Widget), [{"id": 1}])
165
+
166
+ await session.begin()
167
+ assert session.in_transaction() is True
168
+ await session.commit()
169
+ assert session.in_transaction() is False
170
+ await session.close()
171
+ assert session.in_transaction() is False
172
+ with pytest.raises(RuntimeError, match="closed"):
173
+ session.get_row(1)
174
+
175
+
176
+ @pytest.mark.asyncio
177
+ @pytest.mark.parametrize("operation", [update, replace_row, merge])
178
+ async def test_generic_mutation_helpers_fail_closed(operation) -> None:
179
+ engine = ROMEngine([{"id": 1, "name": "original", "rank": 1}])
180
+ session = ROMSession(engine)
181
+
182
+ with pytest.raises(RuntimeError, match="read-only"):
183
+ await operation(Widget, 1, {"name": operation.__name__}, session)
184
+ assert engine.get(1)["name"] == "original"
185
+
186
+
187
+ @pytest.mark.asyncio
188
+ async def test_rollback_restores_locally_mutated_read_objects() -> None:
189
+ session = ROMSession(ROMEngine([{"id": 1, "name": "original", "rank": 1}]))
190
+ widget = await session.get(Widget, 1)
191
+ widget.name = "local mutation"
192
+
193
+ with pytest.raises(RuntimeError, match="read-only"):
194
+ await session.commit()
195
+ await session.rollback()
196
+ assert widget.name == "original"
197
+
198
+
199
+ def test_plugin_factory_and_capabilities_describe_rom_semantics() -> None:
200
+ engine, maker = build_rom(mapping={"rows": [{"id": 1, "value": "alpha"}]})
201
+ assert isinstance(engine, ROMEngine)
202
+ assert isinstance(maker(), ROMSession)
203
+ assert capabilities()["read_only_enforced"] is True
204
+ assert capabilities()["storage"] == "immutable-memory"
205
+ assert capabilities()["binding_scopes"] == {"table"}
206
+ with pytest.raises(ValueError, match="not a DSN"):
207
+ build_rom(dsn="rom://image")
208
+
209
+
210
+ def test_registration_builds_through_engine_spec() -> None:
211
+ register()
212
+ spec = EngineSpec.from_any({"kind": "rom", "rows": [{"id": 1, "value": "alpha"}]})
213
+ assert spec is not None
214
+ engine, maker = spec.build()
215
+ assert isinstance(engine, EngineBase)
216
+ assert engine.spec is spec
217
+ assert engine.get(1) == {"id": 1, "value": "alpha"}
218
+ assert isinstance(maker(), ROMSession)
219
+ assert spec.supports()["read_only_enforced"] is True
220
+
221
+
222
+ @pytest.mark.asyncio
223
+ async def test_engine_base_batch_surface_delegates_to_rom_sessions() -> None:
224
+ engine = ROMEngine([{"id": 1, "name": "one", "rank": 1}])
225
+
226
+ results = await engine.executeloop([select(Widget)])
227
+ assert results[0].scalar_one().name == "one"
228
+ with pytest.raises(RuntimeError, match="read-only"):
229
+ await engine.executemany(select(Widget), [{"id": 1}])