Session Module¶
The notebook front door. dvma.launch(settings) starts the whole
logger — acquisition bridge, native compute engine, session journal and
embedded web UI — on a background thread inside the calling kernel, and
returns a Session handle for pulling data out and pushing data back.
It is the successor to the removed dvma.Logger.
The server process, not the browser tab, owns the session document, so closing the tab loses nothing: reopen the URL and the app offers the session back.
Launching a Session¶
launch ¶
Start a pydvma session and return its :class:Session handle.
Runs the same server as pydvma-serve — acquisition bridge,
native /engine compute host, session journal, embedded UI — on
a daemon thread with its own event loop inside the calling process,
so this works unchanged from a plain script and from inside Jupyter
(whose kernel already runs an asyncio loop of its own).
The default port=0 takes an ephemeral port, so several sessions
can run side by side; read the one actually bound from
:attr:Session.url. Printing that URL is how a session announces
itself, matching pydvma-serve's startup line.
Raises ImportError naming pip install pydvma[serve] when the
optional server dependencies are absent — the retired dvma.Logger
tombstone sends people straight here, and a base install would
otherwise fail deep inside the background thread with an opaque
RuntimeError about the server not starting. Raises
RuntimeError if the server does not bind within
:data:STARTUP_TIMEOUT_S — chained to the underlying error (an
OSError when an explicit port is already taken) when there
was one.
Parameters:
-
settings(MySettings or None, default:None) –acquisition settings the app opens with. Prefills Setup via
/config(see :func:_settings_to_config_json) and supplies the default acquisition driver;Noneprefills nothing and leaves the driver at'auto'— a capture that names no device then records from the OS default input soundcard when there is one, else the mock generator (see :func:pydvma.serve.resolve_default_device). -
open_browser(bool, default:True) –open the app in a browser tab (default True). Pass False for a headless or scripted launch — the URL is still printed and on :attr:
Session.url. -
port(int, default:0) –TCP port to bind, or 0 (default) for an ephemeral one.
-
ui_dir(str or Path or None, default:None) –built UI directory to serve.
None(default) resolves exactly aspydvma-servedoes — the dev checkout'swebui/distif present, else the copy packaged in the wheel, else no UI at all (the bridge still serves, with a help page). -
session_dir(Path or str or None, default:None) –where the journal's spill file lives and previous-run sessions are recovered from. Passed through to :class:
~pydvma.serve.BridgeServer;Nonemeans the system temp dir. Tests and advanced users point this at a directory they control. -
recover(bool, default:True) –offer a previous run's spill file for recovery in the app (default True). Passed through to :class:
~pydvma.serve.BridgeServer; False skips the startup scan entirely.
The Session Handle¶
Session ¶
Bases: object
A running pydvma session: the served app plus its data.
Returned by :func:launch; not constructed directly. Usable as a
context manager, which closes the server on exit.
Parameters:
-
server(BridgeServer) –the running server, whose :attr:
~pydvma.serve.BridgeServer.journalholds the authoritative session document. -
thread(Thread) –the daemon thread running it.
-
loop(AbstractEventLoop) –that thread's event loop.
-
url(str) –where the app is served, with a trailing slash.
Attributes:
-
url(str) –where the app is served (e.g.
'http://127.0.0.1:8760/') — hand it to a browser on another window, or re-open it after closing the tab; the session document survives (that is what the journal is for).
Attributes¶
data
property
¶
The session's data as a fresh DataSet (read-only view).
Materialised from the journal on EVERY access: the posted
session document, with any captures logged since that post
merged in by :func:_merge_dataset. Nothing here is shared
with the app or the engine — mutating what you pull changes
nothing until you :meth:push it back, and the ids carried
through the container round-trip are what make that push land
in place.
An empty DataSet when the session has no document and no
pending captures yet. Still readable after :meth:close —
pulling your data out of a session you have finished with is
the point of the explicit handoff, and the journal outlives
the server thread that fed it.
Methods:¶
_snapshot ¶
Return (dataset, generation) — the journal, materialised.
The generation belongs to the same read as the data, so a
writer can hand it back to
:meth:pydvma.journal.SessionJournal.set_doc as
expect_generation and be refused if anything changed in
between. :attr:data is this without the bookkeeping.
push ¶
Hand data to the session; connected apps offer to reload.
Merges into the CURRENT session data (:attr:data) rather than
replacing it, by :func:_merge_dataset's id rule — so pushing
back something you pulled and edited updates that item in
place, while genuinely new items append.
The merge replaces a matched item WHOLE, so what you push is
what is stored — including the app-side display state a pulled
item carries invisibly (channel labels, per-set analysis
settings: the manifest keys pydvma.container stashes as
_container_extra rather than dropping). Pull → modify →
push therefore preserves it; pushing a NEWLY built item in
place of a stored one legitimately replaces that state with
nothing, and the app re-seeds its defaults on reload.
The read-merge-write cycle is guarded by the journal's
generation counter, so nothing is lost to a race: if a capture
lands, or the app's autosave posts, between the read and the
write, the post is REFUSED and this retries from a fresh read
(up to :data:PUSH_MAX_ATTEMPTS times). Two concurrent pushes
therefore serialise — one wins, the other re-merges on top of
the winner's document — instead of one silently overwriting
the other.
Raises TypeError if data is not a DataSet and is not
something DataSet.add_to_dataset recognises (which would
otherwise post an unchanged document and look like success),
RuntimeError if the session is closed, and RuntimeError
if the journal kept changing under every attempt.
Parameters:
-
data(pydvma.datastructure.DataSet or a single data item) –what to hand over. Anything that is not already a DataSet is wrapped in one, so a lone :class:
~pydvma.datastructure.TimeData(or any other itemDataSet.add_to_datasetaccepts) works directly.
close ¶
Stop the server and its thread. Idempotent and thread-safe.
A second call — from this thread or another — is a clean no-op
rather than a second stop injected into a shutdown already in
progress. Prints a WARNING if the thread has not finished
within :data:SHUTDOWN_TIMEOUT_S, in which case the port may
still be bound; the thread is a daemon, so it cannot keep the
interpreter alive either way.
:attr:data keeps working afterwards (the journal is plain
memory); :meth:push does not, since there is no longer an
app to notify.
__exit__ ¶
Close the session on context-manager exit.
Returns None, so an exception raised inside the with block
propagates after the server is stopped.
The Session Journal¶
The server-side store behind Session.data and Session.push. Held by
pydvma.serve.BridgeServer; the browser app reads and writes it over
the /engine socket. Normally used through Session rather than
directly.
SessionJournal ¶
Bases: object
In-memory session document + pending captures + listeners.
Parameters:
-
spill_path(Path or str or None, default:None) –file to mirror the current document into on every update (best-effort; errors are swallowed).
Nonedisables spilling; it can be set later with :meth:set_spill_path(a server on an ephemeral port only knows its identity after binding).
Attributes¶
generation
property
¶
How many writes this journal has accepted (read-only).
Starts at 0 and increments on every accepted :meth:set_doc
and every :meth:add_capture — see the module docstring. A
refused set_doc does not increment it. Read it through
:meth:state when the value must match the data you read;
this property is for tests and diagnostics, where a bare
counter is enough.
spill_failures
property
¶
How many spills failed end-to-end and were swallowed (read-only).
Counts every :meth:_spill that had a document and a path but
could not land it on disk — temp-file creation, the write, or
an os.replace still failing after
:func:_replace_with_retry's whole ladder. A non-zero count
means the spill file may be STALE relative to :meth:state;
the next successful spill overwrites it, but nothing rewinds
this counter. Disabled spilling (no path) and empty journals
do not count — those are no-ops, not failures.
Methods:¶
set_doc ¶
Replace the session document (and clear pending captures).
Only the captures this document PROVABLY contains are cleared:
the document's unique_id set is read from its manifest
(:func:pydvma.container.manifest_ids) and a pending capture is
dropped only when its own ids are a subset of it, so a capture
that landed after the poster serialised its document — or one
belonging to a different tab — survives the post and is still
offered on the next :meth:state. A capture with no readable
ids is cleared by any post (see the module docstring's
clears-pending contract).
Returns True when the document was written, and False only when
an expect_generation was supplied and no longer matches — in
which case NOTHING happens: the document is untouched, pending
captures are untouched, no spill is written and no listener
fires. Callers that pass expect_generation must handle the
False by re-reading :meth:state and re-merging (see
:meth:pydvma.session.Session.push).
Parameters:
-
doc_bytes(bytes, bytearray, or memoryview) –the full session document (the same bytes written to a
.dvmafile). -
notify(bool, default:False) –also call every registered listener after the replace. Used by :meth:
pydvma.session.Session.pushso connected apps reload; the app's own autosave posts use the defaultFalse(silent — the app already has what it just posted). -
expect_generation(int or None, default:None) –the :attr:
generationthis document was built from. When given, the post is refused unless the journal is still at that generation, so a capture or another writer's post that landed in between can never be silently overwritten (see the module docstring).None(the default) posts unconditionally — right for a writer that owns the whole document already.
add_capture ¶
Register one capture's .dvma bytes, pending until a
document containing it is posted (module docstring's contract).
The capture's identity — its TimeData unique_ids, read
from the manifest by :func:pydvma.container.manifest_ids and
stored beside the bytes — is what a later
:meth:set_doc matches against to decide whether that document
already holds this capture. Reading it here, once, keeps the
cost off every subsequent post.
This does NOT touch the spill file — the spill mirrors only the posted document (see the module docstring), so a capture registered here is absent from the crash artifact until the app's next autosave lands.
If appending pushes the pending list's total size over
:data:PENDING_CAPTURES_MAX_BYTES, the OLDEST pending entries
are evicted (silently — there is no error, no truncation
signal, just fewer captures on the next :meth:state) until
the total is back under the cap or the list is empty. Eviction
is strictly oldest-first with no special case for the entry
just appended: a single capture that alone exceeds the whole
budget is evicted too, leaving the list empty rather than
holding one capture over cap. See the module docstring's
"Pending-captures budget" paragraph — this only matters when
nothing ever posts a clearing document.
Parameters:
-
dvma_bytes(bytes, bytearray, or memoryview) –one capture's full
.dvmabytes.
state ¶
Current (doc_bytes_or_None, [capture_bytes, ...], generation).
The list is a fresh list of the capture BYTES (the per-capture
id sets kept alongside them are the journal's own bookkeeping
and never leave it) — mutating it never touches the journal.
The document is returned by reference, but bytes is
immutable so that is equivalent to a copy for callers. The
generation describes THIS snapshot: hand it back as
:meth:set_doc's expect_generation to post an update that
refuses to clobber anything that landed since.
add_listener ¶
Register a zero-arg callable invoked on notify updates.
Returns an unsubscribe callable. Listener exceptions are
swallowed (one broken listener must not silence the rest). An
update already in flight when unsubscribe() returns may
still call cb once more — :meth:set_doc snapshots the
listener list before releasing the lock, so a race between an
in-progress notify and a concurrent unsubscribe is possible;
consumers must tolerate one extra call.
Parameters:
-
cb(callable) –zero-argument callable to invoke on every
notify=Trueupdate, until unsubscribed.
set_spill_path ¶
Set (or move) the spill target after construction —
BridgeServer only knows its real port after binding.
Parameters:
-
path(Path or str or None) –file to mirror the current document into on every update, or
Noneto disable spilling.
adopt_recovered ¶
Read a PREVIOUS run's spill file into memory as a recovery offer. Reading now — not at offer time — makes a later overwrite of the same path harmless. A missing or unreadable file is a no-op, as is an empty one.
Parameters:
-
path(Path or str) –the previous run's spill file to read.
Returns:
-
adopted(bool) –True if
pathwas actually adopted (readable and non-empty), False on the no-op paths above. Callers that try several candidates in order (see :func:pydvma.serve._adopt_previous_session) use this to know whether to keep trying the next one, rather than checking :meth:recoveredfor a change.
discard_recovered ¶
Drop the recovery offer and delete its file (the app's Dismiss).
Skips the delete when the recovered file IS the current spill
target — a same-port restart adopts what is now its own live
spill file, and deleting it would remove the live mirror, not
just the stale offer. The comparison is normalised through
os.path.abspath on both sides so a str spill path and a
pathlib.Path recovered path (or vice versa) still compare
equal when they name the same file. Best-effort on the delete;
idempotent.
_spill ¶
Mirror the current doc to spill_path, atomically.
Serialised against concurrent calls via _spill_lock (held
for the whole body) so overlapping spills can never interleave
their writes; each call re-reads the CURRENT document under the
lock, so whichever spill runs last always writes the latest
doc. The write itself goes to a temporary file in the same
directory, then os.replaces over spill_path — the same
tempfile-then-rename idiom as :func:pydvma.container.save —
so a crash mid-write can never truncate or tear the previous
good copy. The replace runs through :func:_replace_with_retry
because on Windows an external scanner transiently holding the
temp file or the target makes it fail spuriously. Best-effort
beyond that: any OSError (including the temp file's own
creation, e.g. a missing directory) is swallowed after cleaning
up any partial temp file, and counted in
:attr:spill_failures so a stale spill is observable.