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:

  1. Client

  2. User

  3. Tree model (Notebooks -> Notebook -> NotebookDirectory / NotebookPage)

  4. Entry model (Entries and Entry subclasses)

  5. Utility layer (labapi.util helpers such as NotebookPath, 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:

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:

  • NotebookPath path normalization/resolution

  • typed 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:

  • children and mapping access call the internal _ensure_populated() helper.

  • _ensure_populated() fetches tree_tools/get_tree_level once and marks the container as populated.

  • refresh() clears _children and 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 None means “not loaded yet”.

  • first access fetches from tree_tools/get_entries_for_page and stores an Entries object.

  • refresh() resets _entries back to None.

Note

Repeated page.entries access should return the same Entries object until refresh.

Path Cache (_has_path)#

Every tree node memoizes its NotebookPath:

  • first path access computes and caches a NotebookPath for 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:

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 _children

  • switches _parent

  • appends to destination _children

  • invalidates 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 Items directory under notebook root

  • rename 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 reconciled

  • page 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:

  1. Identify which cache invariants your change touches (_populated, _entries, _has_path).

  2. Decide whether in-memory objects must be mutated immediately or whether refresh() / refresh() should be required.

  3. Ensure parent/child bookkeeping stays symmetric for moves/deletes.

  4. If you add or change entry types, confirm entry registration plus from_part_type() dispatch behavior.

  5. Update this page (and related guide pages) when module boundaries or invariants change.