Writing a memory backend¶
Built and verified
MemoryBackend, the conformance suite and the registration routes below
exist and are tested. The testing extra installs pytest for you
(pip install "coniti[testing]").
A new backend, such as a database or a remote service, subclasses
MemoryBackend and implements eight underscore methods. The public methods
check the request and result contracts for you, so you only write the storage.
# my_pkg/backend.py
from coniti.memory import (
ClearRequest, ClearResult, CountRequest, CountResult, DeleteRequest,
DeleteResult, GetRequest, GetResult, MemoryBackend, MetaGetRequest,
MetaResult, MetaSetRequest, PutRequest, PutResult, ScanRequest, ScanResult,
)
class MyBackend(MemoryBackend):
def _put(self, request: PutRequest) -> PutResult: ...
def _get(self, request: GetRequest) -> GetResult: ...
def _delete(self, request: DeleteRequest) -> DeleteResult: ...
def _scan(self, request: ScanRequest) -> ScanResult: ...
def _count(self, request: CountRequest) -> CountResult: ...
def _get_meta(self, request: MetaGetRequest) -> MetaResult: ...
def _set_meta(self, request: MetaSetRequest) -> MetaResult: ...
def _clear(self, request: ClearRequest) -> ClearResult: ...
The base class provides the public put, get, delete, scan, count,
get_meta, set_meta, clear and iter_all, plus async versions that run the
sync call in a worker thread. A remote backend can override the async methods
natively, and can extend (not replace) the request and result models by setting
the matching class attributes, for example scan_result.
Give the class a params_model (a subclass of Params) if it takes settings.
The contract to honour¶
See memory for the full list. In short:
putis all-or-nothing; an existing or repeated id raisesDuplicateRecord.- Unknown ids go in
missing;getanddeletenever raise for them. scanis ordered by (timestamp, write order), half-open, paginated, with opaque cursors that resume exactly, andInvalidCursorfor any cursor you did not issue for that request (including the empty string).- Namespaces never see each other.
- Records are copied in and out.
dumpandload(the memory file) are inherited fromMemoryBackend; the suite checks that your backend round-trips through them, as a stream and as plain and gzip paths, and that a dump covers only the requested namespace. See backup-restore.
Run the conformance suite¶
The suite lives in coniti.testing.memory and needs pytest.
With pytest. Subclass MemoryBackendContract and implement make_backend().
pytest then runs every check against a fresh backend:
# tests/test_my_backend.py
from coniti.testing.memory import MemoryBackendContract
from my_pkg.backend import MyBackend
class TestMyBackend(MemoryBackendContract):
def make_backend(self):
return MyBackend()
Without the pytest runner. run_contract(make_backend) runs the same checks
and returns a dict mapping each check name to its failure, or None if it passed:
from coniti.testing.memory import run_contract
from my_pkg.backend import MyBackend
results = run_contract(MyBackend)
failures = {name: exc for name, exc in results.items() if exc is not None}
assert not failures, failures
The checks cover duplicates, missing ids, ordering, half-open ranges, pagination
at every page size, cursor resumption after changes, bad cursors, namespace
isolation, metadata, clear, copying, wrong request types, async parity, and a
seeded random sequence of operations compared with a simple list model. The
suite is itself tested: it catches six deliberately broken backends (partial
batch writes, reversed pages, records that are not copied, raising on missing
ids, leaking across namespaces, a bad end-of-scan cursor). A record(...)
helper builds records for your own extra tests.
Register it¶
Bind by class path, with no registration:
or give it a short name that others can use. Register it with @register and
expose the module through the coniti.plugins entry-point group:
# my_pkg/coniti_plugins.py
from coniti import Params, register
from my_pkg.backend import MyBackend
register("memory", "my_store", Params)(MyBackend) # or your own Params subclass
# your package's pyproject.toml
[project.entry-points."coniti.plugins"]
my_plugins = "my_pkg.coniti_plugins"
After installing your package, kind = "my_store" works. See plugins for how
discovery behaves.