an.library.stores#
The library mall: records, versions and blobs, each an injected MutableMapping.
ADR 0005 decision 11 and design §10. Three entities, three stores:
store |
key |
on disk (default backend) |
|---|---|---|
records |
|
|
versions |
|
|
blobs |
|
|
labels |
|
|
labels is write-once too: the append-only statements made about an existing
version after it was published (a relabel of its unchanged content, an#307),
each its own document, never rewritten or deleted.
``dol`` stores, unlike the project mall’s hand-written folder classes: a byte store (
LocalFiles) seen throughdol.wrap_kvs()with a JSON codec and a key transform. Each store is replaced by injection (build_library_mall(blobs=my_s3_store)), and business logic sees only the mapping interface — the move to S3 is an injection, not a rewrite.LocalFilesrather thandol.Filesbecause the library needs threethings
dol.Filesdoes not give: writes that cannot tear (temp file +os.replace), an exclusive create for write-once versions, and containment (dol.Fileswrites a../xkey outside its root).``versions`` is write-once (
WriteOnce): a version is immutable, so overwriting or deleting one raisesVersionExistsError— projects pin versions, and a pin that could change under them is no pin. On the folder backend the create is exclusive, so racing publishers cannot both win.``blobs`` is content-addressed and undeletable (
ImmutableBlobs, adol.content.ContentAddressedStore): every file of every version once, keyed by its SHA-256; a key can never point at changed bytes, and a blob a version pins cannot be deleted from under it.Keys are validated twice before they become paths: by the id grammars (
an.library.ids), and byLocalFiles’ containment check.Nothing is created until the first write.
>>> import tempfile
>>> with tempfile.TemporaryDirectory() as d:
... lib = build_library_mall(d)
... sorted(lib)
['blob_rights', 'blobs', 'labels', 'records', 'versions']
>>> mem = build_library_mall(records={}, versions={}, blobs={}) # all in memory
>>> ref = mem["blobs"].add(b"<svg/>")
>>> mem["blobs"][ref.item_id]
b'<svg/>'
Module Attributes
The three stores every library mall holds. |
Functions
|
The library mall of |
|
JSON with sorted keys and no locale or platform dependence. |
|
The |
|
|
|
The |
|
|
Classes
|
The content-addressed blob store, with deletion refused. |
|
|
|
A mapping whose keys, once written, can be neither overwritten nor deleted. |
Exceptions
A write-once key was written twice, or deleted. |
- class an.library.stores.ImmutableBlobs(store=None, *, hasher=<built-in function openssl_sha256>, length=None, field='content')[source]#
Bases:
ContentAddressedStoreThe content-addressed blob store, with deletion refused.
Versions are write-once and pin their files by hash, so deleting a blob would leave a published version pointing at nothing. Reclaiming unreferenced blobs is a maintenance job (a garbage collector that counts references), not a mapping operation.
>>> blobs = ImmutableBlobs({}) >>> ref = blobs.add(b"x") >>> del blobs[ref.item_id] Traceback (most recent call last): VersionExistsError: ...
- an.library.stores.LIBRARY_STORES: tuple[str, ...] = ('records', 'versions', 'blobs')#
The three stores every library mall holds.
- class an.library.stores.LocalFiles(root)[source]#
Bases:
MutableMappingrelative/posix/path -> bytesunder one folder, with atomic, contained writes.The default backend of every library store:
contained: a key that would resolve outside the folder (
.., an absolute path, a drive) raisesKeyErrorand never touches the disk;atomic: a write goes to a temp file in the target folder and is moved into place with
os.replace, so a crash never leaves a torn document;create-only (
create_only()): a hard link of the fully written temp file, which fails if the target exists — the primitiveWriteOnceuses so two publishers cannot both create one version;lazy: nothing is created until the first write, so opening (or mistyping) a library never creates folders.
>>> import tempfile >>> with tempfile.TemporaryDirectory() as d: ... f = LocalFiles(d + "/lib") ... f["a/b.json"] = b"{}" ... sorted(f), f["a/b.json"] (['a/b.json'], b'{}')
- exception an.library.stores.VersionExistsError[source]#
Bases:
KeyErrorA write-once key was written twice, or deleted. Versions are immutable.
- class an.library.stores.WriteOnce(store)[source]#
Bases:
MutableMappingA mapping whose keys, once written, can be neither overwritten nor deleted.
Wraps any injected
MutableMapping. Rewriting a key with the same value is refused too: immutability is about the key, and a silent no-op would hide a caller that believes it is replacing something.Atomicity. When the wrapped store offers
create_only(key, value)— an exclusive create that raisesVersionExistsErrorif the key exists — every write goes through it, so two publishers racing for one key cannot both win. The default folder backend has one (a hard link of a fully written temp file); an S3 backend would map it to a conditional put (If-None-Match: *). A store without it gets a check-then-set, which is correct for one writer.>>> versions = WriteOnce({}) >>> versions["character.a@v001"] = {"x": 1} >>> versions["character.a@v001"] = {"x": 2} Traceback (most recent call last): VersionExistsError: ...
- an.library.stores.build_library_mall(root=None, *, package='an', **overrides)[source]#
The library mall of
package:records,versions(write-once),blobs(CAS).- Return type:
- root: the package’s data root (default: resolved by
an.library.root.library_root()—root→<PKG>_HOME→ the platform data folder); the stores live under<root>/library/
package: whose library this is (
an, or a genre such ascutan) overrides: a store per name to inject instead of the folder default — adictfor tests, an S3 or database mapping later. An injectedversionsis still made write-once and an injectedblobsstill content-addressed and undeletable, so injection cannot drop an invariant.Nothing is created until the first write: opening a library (or mistyping one on a search path) leaves the disk as it was.
- an.library.stores.canonical_json(obj, *, indent=None)[source]#
JSON with sorted keys and no locale or platform dependence.
With
indent=Noneit is the compact form hashed for a manifest; with an indent it is the on-disk form (same content, readable).- Return type:
>>> canonical_json({"b": 1, "a": [1, 2]}) '{"a":[1,2],"b":1}'
- an.library.stores.label_key(asset_id, version, label_id)[source]#
The
labelskey of one statement about a version.- Return type:
>>> label_key("character.alice", "v002", "0123456789abcdef") 'character.alice@v002/0123456789abcdef'
- an.library.stores.split_label_key(key)[source]#
(asset_id, version, label_id)of alabelskey, validated.>>> split_label_key("character.alice@v002/0123456789abcdef") ('character.alice', 'v002', '0123456789abcdef')