Skip to content

Cheat Sheet

Basics

Item Overview
Instance

Create an instance:

syncwave = Syncwave()  # default store folder is `./syncstores/`

Choose a different location:

syncwave = Syncwave(root_path="path/to/dir")
Stores

Create a store:

syncwave.create_store(list[int], name="numbers")  # syncstores/numbers.json

Default value:

syncwave.create_store(int, name="counter", default=0)

Manage stores:

len(syncwave), list(syncwave), "numbers" in syncwave
del syncwave["numbers"]  # deletes the file too!
Read and write
syncwave["numbers"] = [1, 2, 3]  # written to the file
syncwave["numbers"]  # [1, 2, 3], updated on file change
Types and validation

Flexible types:

syncwave.create_store(Any, name="anything")  # no validation
syncwave.create_store(dict[str, list[int] | int], name="data")
syncwave.create_store(Literal["light", "dark"], name="theme", default="light")

Validated both ways:

syncwave["numbers"] = [1, "two"]  # ValidationError
# invalid edits to the file are reverted
Copies (non-reactive types)

Reading a non-reactive value returns a copy:

numbers = syncwave["numbers"]  # `numbers` is a copy
numbers.append(4)  # doesn't reach the store
syncwave["numbers"].append(4)  # still no change

Values are copied on insert as well:

numbers = [1, 2, 3]
syncwave["numbers"] = numbers
numbers.append(4)  # only `numbers` is mutated, not the store or file

Reactivity

Item Overview
Reactive types

Syncwave is itself reactive.

dict -> SyncDict

list -> SyncList

set -> SyncSet

pydantic.BaseModel -> SyncModel

pydantic.RootModel -> SyncRoot

pydantic.dataclasses.dataclass -> @sync_dataclass

Reactive stores

Use a reactive type for the store:

numbers = syncwave.create_store(SyncList[int], name="numbers")

Mutate it in place:

numbers.append(4)  # written to the file
syncwave["numbers"].append(5)  # same thing
numbers  # [4, 5], updated on file change
Reactive chain

Reactive types compose:

SyncDict[str, SyncList[int]]  # nested collections
SyncList[Customer]  # reactive model in collection


class Settings(SyncModel):
    recent_files: SyncList[str]  # reactive field
    window: Window  # another reactive model

Plain data becomes reactive on the way in, at any depth:

playlists = syncwave.create_store(SyncDict[str, SyncList[str]], name="playlists")
playlists["road_trip"] = []  # stored as a live SyncList

Non-reactive values inside a reactive one are copied, like at the store level:

store = syncwave.create_store(SyncDict[str, list[int]], name="store")
store["key"] = [1, 2, 3]
store["key"].append(4)  # a copy, doesn't reach the store
Lifecycle

Created by hand, an object is inert and acts like its plain counterpart:

inert = SyncList([1, 2, 3])  # SyncState.INERT
syncwave["numbers"] = inert  # the store keeps a copy, `inert` stays inert

Read from a store, it's live:

numbers = syncwave["numbers"]  # SyncState.LIVE

Removed from the store, it's dead (and so is everything inside it):

del syncwave["numbers"]  # SyncState.DEAD
numbers.append(4)  # DeadReferenceError

Check the state:

numbers.sync_state  # SyncState.INERT, LIVE, or DEAD
numbers.sync_live  # True only if live
Unions
syncwave.create_store(SyncList[int] | SyncDict[str, int], name="union")

A change of type kills the old object:

ref = syncwave["union"]  # a SyncDict
syncwave["union"] = [1, 2]  # now a SyncList
ref.sync_state  # SyncState.DEAD
ref = syncwave["union"]  # take a new ref, now a live SyncList

Collections

Item Overview
SyncDict
settings = syncwave.create_store(SyncDict[str, int], name="settings")
settings["volume"] = 8

Keys must round-trip through a JSON string:

SyncDict[int, str]  # fine, keys are strings in the file
SyncDict[int | str, str]  # TypeError, ambiguous keys
SyncDict  # same as SyncDict[str, Any]
SyncList
tags = syncwave.create_store(SyncList[str], name="tags")
tags.append("python")

References follow positions, not content:

first = songs[0]
songs.insert(0, new_song)
first  # now `new_song`
SyncSet
ids = syncwave.create_store(SyncSet[int], name="ids")
ids.add(1)  # a JSON array in the file

Items must be hashable, so reactive items aren't allowed:

SyncSet[list[int]]  # TypeError
SyncSet[SyncList[int]]  # TypeError

Models

Item Overview
SyncModel

Subclass SyncModel instead of BaseModel:

class Settings(SyncModel):
    volume: int = 8
    theme: Literal["light", "dark"] = "light"


settings = syncwave.create_store(Settings, name="settings")
settings.volume = 7  # validated, written to the file

Pass a default if some fields don't have one:

syncwave.create_store(Settings, name="settings", default={"volume": 8})
store decorator

Define a model and create its store in one step:

@syncwave.store(name="customers")
class Customer(SyncModel):
    name: str
    age: int


customers: SyncList[Customer] = syncwave["customers"]

By default (collection="auto"), the store type is:

  • SyncDict[<key type>, <model>] if the model has a key field
  • SyncList[<model>] without a key field
  • <model> itself (no wrapping) if it's a SyncRoot

Or pick the collection yourself:

collection = SyncDict[int]  # SyncDict[int, <model>]
collection = SyncList  # SyncList[<model>]
collection = None  # <model>, no wrapping
SyncRoot

A reactive single value:

locale = syncwave.create_store(SyncRoot[str], name="locale", default="en-US")
locale.root = "fr-CA"  # the file holds "fr-CA"
sync_dataclass
@sync_dataclass
class Point:
    x: int = 0
    tags: SyncList[str] = field(default_factory=SyncList)


is_sync_dataclass(Point)  # True
Restrictions
  • No frozen models or fields.
  • Defaults must be valid (checked when the class is defined).
  • Validators and serializers must be pure and idempotent.
  • No exclude=True fields.
  • Fields can't be deleted on a live instance.