Contributor Architecture Guide#
This guide documents the current internal architecture of labapi for
contributors working on tree, entry, and client internals.
The intent is to describe current behavior and invariants, including places where the design is intentionally incomplete.
Subsystem Map#
At a high level, runtime flow goes through these layers:
Tree model (
Notebooks->Notebook->NotebookDirectory/NotebookPage)Utility layer (
labapi.utilhelpers such asNotebookPath, indexing types, and XML extraction)
Client and User Boundary#
Client is responsible for connection/auth concerns and
request signing. Its URL construction/signing helpers append the AKID, expiry,
and HMAC-SHA512 signature to requests.
User wraps an authenticated session and provides
api_get()/api_post() that always
add uid. Most internal modules call the API through
User rather than directly through
Client.
Tree Model Boundary#
The tree model is split between mapping-style collections and node/container mixins:
Notebookstracks the user-visible notebook list.Notebookis both the logical root and a container.NotebookDirectoryis a container node.NotebookPageis a leaf node with lazy-loaded entries.
Most operations that mutate the notebook hierarchy are implemented in
tree/mixins.py and then reused by concrete node types.
Entry Model Boundary#
entries lazily fetches page entries and
materializes them as Entry subclasses.
The Entries collection owns page-level entry
creation methods and appends newly created entries to local state after
successful API calls.
Utility Layer Boundary#
The labapi.util package provides shared primitives that keep core modules
small and predictable:
NotebookPathpath normalization/resolutiontyped index markers (
Index.Id/Index.Name)XML extraction and conversion helpers
constants such as known part types
Cache model and invariants#
Container Population Cache (_populated)#
AbstractTreeContainer lazily loads children on
first access:
childrenand mapping access call the internal_ensure_populated()helper._ensure_populated()fetchestree_tools/get_tree_levelonce and marks the container as populated.refresh()clears_childrenand resets_populated=False.
Note
While _populated is true, read APIs for that container should be served
from local _children without another tree-level API call.
Page Entries Cache (_entries)#
entries is also lazy:
_entries is Nonemeans “not loaded yet”.first access fetches from
tree_tools/get_entries_for_pageand stores anEntriesobject.refresh()resets_entriesback toNone.
Note
Repeated page.entries access should return the same
Entries object until refresh.
Path Cache (_has_path)#
Every tree node memoizes its NotebookPath:
first
pathaccess computes and caches aNotebookPathfor the node.rename and move operations clear the cached path for the current node and any loaded descendants.
Note
Cached paths are stable until the node or one of its loaded ancestors is renamed or moved.
Path Stability and Traversal Expectations#
NotebookPath canonicalizes path-like input and
supports:
absolute and relative forms
composition with
/resolution with parent anchors
relative conversion via
relative_to()
Tree traversal uses path resolution semantics from
NotebookPath.
traverse() resolves relative
paths against self.path and then walks segments from self.root.
Enumeration helpers build on the same traversal model:
enumerate_nodes()returns(relative_path, node)pairs for concrete descendant objects.enumerate_all(),enumerate_dirs(), andenumerate_pages()derive from that shared traversal so duplicate names do not get re-resolved throughtraverse().
Mutation Invariants and Refresh Expectations#
General Rule#
Mutating methods first call the API and then update local in-memory state when the API call succeeds.
Create Operations#
create() inserts new nodes and
appends them to self._children. For container nodes, the new node starts
with _populated=True and empty children.
When if_exists=InsertBehavior.Replace, existing matching nodes are deleted
before creating a replacement.
Move Operations#
move_to() updates the server parent,
then mutates both local parents:
removes the node from old parent
_childrenswitches
_parentappends to destination
_childreninvalidates cached paths for the moved node and any loaded descendants
Delete Operations#
delete() is implemented as
move-to-trash semantics:
ensure/create an
API Deleted Itemsdirectory under notebook rootrename node to include deletion timestamp
call
move_to()with the trash folder as destination
This means local state continues to reference the same Python object, but under its new parent and new name.
When to Call Refresh#
Use refresh() (or
refresh() for page entries) when external
changes may have occurred or when you need to force a re-fetch.
Current behavior is intentionally shallow:
container
refresh()clears child caches, but pre-existing child objects held elsewhere are not automatically reconciledpage
refresh()clears the page’s entries cache, but existing entry instances are not invalidated in place
Other Entry Types#
When page entries are loaded, recognized-but-unimplemented and fully unknown types are
wrapped as UnknownEntry with warnings in
entries.
Known Shortcuts and TODO Areas#
These are current incomplete areas that contributors should treat carefully:
container refresh clears the owner’s child cache, but detached child objects held elsewhere are not reconciled in place
page refresh clears the page-level entries cache, but existing entry objects are not invalidated in place
individual entry deletion is not implemented
create_json_entry()still creates two concrete entries and does not yet model that pair as one logical unit
Contributor Checklist for Internal Changes#
Before changing tree/entry/client internals:
Identify which cache invariants your change touches (
_populated,_entries,_has_path).Decide whether in-memory objects must be mutated immediately or whether
refresh()/refresh()should be required.Ensure parent/child bookkeeping stays symmetric for moves/deletes.
If you add or change entry types, confirm entry registration plus
from_part_type()dispatch behavior.Update this page (and related guide pages) when module boundaries or invariants change.