Sending it to the platform
A history on your disk and a history on the platform are two ends of one lineage. push
carries a branch one way, pull carries it the other, and neither end keeps a record of
the other.
import chatty_lab as cl
here = cl.Workdir(cl.Store.at("./work"), graph=LOCAL_ID)there = cl.Store.on("https://chatty-lab.com", token=TOKEN, graph=REMOTE_ID)
carried = here.push(there)
carried.sent # [Version, …] — what the far end did not have, oldest first, in ITS idscarried.head # Ref — where the branch points over there afterwardsPush the same branch twice and sent comes back empty. Nothing was missing.
What you need
Section titled “What you need”-
An address. Either the API root or the address of the site —
Store.onadds the/apiif it is not there. -
A token. The same Supabase access token the REST API takes. Read it from the environment; do not put it in the script.
-
The lineage id over there. It is the
graphparameter in the Studio URL:…/studio?graph=3f47b2c8-…&branch=main. A graph created withPOST /api/graphsreturns the same id. -
A workspace, if the graph belongs to a team rather than to you.
Store.on(..., workspace=TEAM_ID)sets theX-Workspace-Idheader on every call.
import osimport chatty_lab as cl
there = cl.Store.on( os.environ["CHATTY_API"], token=os.environ["CHATTY_TOKEN"], graph=os.environ["CHATTY_GRAPH"], workspace=os.environ.get("CHATTY_WORKSPACE"),)Why no uuid crosses
Section titled “Why no uuid crosses”Every store mints its own ids, so one version has a different uuid at each end and always will.
local = here.at("HEAD")remote = carried.sent[-1]
local.id == remote.id # False — alwayslocal.digest == remote.digest # True — this is the version being the same versionlocal.n == remote.n # maybe. Do not rely on it.The ordinal is minted by whoever writes: the same content can be v2 here and v3 there.
See n is local.
Pulling
Section titled “Pulling”carried = here.pull(there)carried.sent # what arrived HERE, oldest firstpull is push with the ends the other way round. It brings the history and stops:
there is no working copy to land it in, so what arrived is read with content().
here.pull(there)blob = here.content("HEAD").graphBranches
Section titled “Branches”The workdir’s own branch is the one carried. Another line is spelled the same way it is everywhere else:
here.push(there) # mainhere.on("stricter").push(there) # the variantWhen a push is refused
Section titled “When a push is refused”A push is refused when the branch over there holds versions this one has never seen. That is git’s non-fast-forward, and it wants a merge rather than a flag:
try: here.push(there)except cl.SyncError: here.pull(there) # bring their side in here.merge("main", message="…") # decide what the two lines make together here.push(there)Two other SyncErrors are not divergence at all and are worth retrying rather than
merging: the far branch moved between reading where it was and writing, or an ordinal
was taken by another writer against that same store in the moment between. Nothing was
written either time; asking again decides afresh against what is there now.
What you see afterwards
Section titled “What you see afterwards”Open the graph in the Studio at …/studio?graph=<id>&branch=<name> and the branch you
pushed is there, with its versions, their messages, and the diff between any two. A
version pushed from a script and a version committed from the canvas are the same kind of
thing — the canvas cannot tell which one made it, because there is nothing to tell.
An experiment can then be pinned to one of those versions, and the numbers it produces belong to it. See End to end.