Persistent store (save / load)
Schema-versioned read / migrate / merge / write of one dict to one save file.
Overview
PersistentStore owns exactly one concern: read, migrate, merge, and write one
dict to one file, with a top-level version stamp and a single-step migration
ladder. Everything domain-specific — the envelope's keys, what to persist, where
the file lives — stays with the caller.
The version stamp is a top-level key in the payload (save_version by
default), not a {version, data} wrapper, so existing flat save files load
unchanged. fmt selects "yaml" or "json" (built on the fs.py
load/save primitives).
| Method | Does |
|---|---|
load() |
read + migrate to the current version, or None if missing/unreadable/unmigratable |
save(data) |
stamp the version and write |
migrate(data) |
run the ladder (no file I/O — standalone-testable) |
update(**sections) |
read-modify-write merge, returns the merged dict |
Semantics:
- migrate — absent version → 1; a save newer than this build loads
unchanged (best-effort, never rewritten); otherwise the ladder runs
while v < version and v in migrations; any exception →None(caller treats as "no save" / New Game). - load — missing/unreadable/unmigratable →
None; backs the file up once (path + '.bak') before an upgrading migration, so a bad migration is recoverable.
Quick example
from sbs_utils.procedural.persistence import PersistentStore
MIGRATIONS = {
1: lambda d: {**d, "credits": d.pop("money", 0)}, # v1 save -> v2 (rename)
}
store = PersistentStore(get_mission_dir_filename("save.yaml"),
version=2, migrations=MIGRATIONS)
data = store.load() # None => New Game
if data is None:
data = {"credits": 100}
# read-modify-write merge (replaces `d = load() or {}; d[k] = v; save(d)`)
store.update(credits=data["credits"] + 50)
Thin functional wrappers — persist_load, persist_save, persist_migrate —
exist for one-off calls that don't need to hold a store instance.
API
Schema-versioned save/load for a single keyed-blob envelope file.
A small persistence framework extracted from the Open Universe's save layer. It owns exactly one concern: read / migrate / merge / write one dict to one file, with a top-level version stamp and a single-step migration ladder. Everything domain-specific (the envelope's key names, WHAT to persist, where the file lives, mirroring live state into inventory) stays with the caller.
Built on the fs.py primitives (load/save_yaml_data, load/save_json_data), so
fmt selects the reader/writer pair. The version stamp is a top-level key in the
payload (configurable version_key, default "save_version"), NOT a {version,
data} wrapper - so existing flat save files load unchanged.
Semantics (carried over verbatim from the OU save layer):
- migrate: absent version -> 1; v > version returns the data unchanged (a
newer-than-build save loads best-effort, don't rewrite); the ladder runs
while v < version and v in migrations; any exception -> None (caller treats
as "no save" / New Game).
- load: missing/unreadable/unmigratable -> None; backs the file up once
(path + '.bak') before an UPGRADING migration, so a bad migration is
recoverable.
PersistentStore
Versioned save/load for one envelope file. migrations is a single-step
ladder {v: fn(data)->data} upgrading a v save to v+1.
load()
Read + migrate to the current version, or None when the file is missing/unreadable/unmigratable. Backs up once before an upgrade.
migrate(data)
Run the ladder up to version. Returns the upgraded dict; the dict
unchanged if it is NEWER than this build; or None if it can't be
migrated. No file I/O - standalone-testable.
save(data)
Stamp data[version_key] = version and write it.
update(**sections)
Read-modify-write merge: load() or {}, apply sections, save.
Returns the merged dict. Replaces the ubiquitous
data = load() or {}; data[k] = v; save(data).