Skip to content

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).