Skip to content

Document

matchlab.document

Serialise a plan to a portable document, and rebuild it somewhere else.

A PlanDocument is a derived view of a plan, not a source format. Humans write Python, and a document is what you hand to another environment so it can run the same plan. It is dumped, transferred and loaded, never hand-authored. That is why nodes refer to each other by position rather than by any human-facing name.

Nodes come out in lineage.walk order, so every input index is smaller than the index of the step that consumes it. Positions also preserve structural sharing. A record step feeding two models is one node referenced twice. Nesting each step's inputs inside it would have inlined the whole subtree twice over instead.

A node is three things, and they are separate for a reason. Its spec is what the fingerprint hashes, so it carries everything that changes the step's output and nothing else. Its inputs are the edges, which a fingerprint already covers by folding in its parents' fingerprints — recording them in the spec too would mean a rename invalidating a subtree that produces identical bytes. Its resources name what the step reads through, which changes no output and so belongs in neither.

Putting any two of them in one field would force a choice between a spec that lies about identity and a document that cannot be rebuilt.

What a document cannot carry:

  • Resources. Engines, connections, credentials, frames. A document records only the name each one was given (matchlab.resources.Resource), in StepNode.resources, keyed by the settings field it fills. load takes the real objects and fills them in, so nothing secret ever enters a document. Because they live in their own field rather than among the settings, no resource can reach a fingerprint — that is structural, not a convention. Every kind of step may name one; in practice only sources do, because a resource answers "where do these rows come from".
  • Code. model_class and resolver_class are registry names, so the target environment must have the same classes registered (add_model_class). A document is portable across environments, not across codebases.
  • Labels. Publishing is something you do to a result, using Resolver.publish. It is not part of the plan, so the receiving environment collects and then publishes under whatever label it wants. The only names in a document are sources' names, which are part of their output.
  • Data, or any hash of it. A source's fingerprint folds in a content hash of what it actually read, and that hash is derived on load by reading the target's own rows. Same rows give the same fingerprints, so the target store hits cache instead of recomputing. Different rows give different fingerprints, so it re-runs. Both are the intended behaviour.

A rebuilt plan therefore fingerprints identically, as long as every methodology it names declares a version. One that declares none is keyed by a nonce on every collect, here as anywhere else, so the steps from it down re-run wherever the document is loaded. See matchlab.core.versioning.

Classes:

  • StepNode

    One step: its kind, what it specifies, what it reads, and what it needs.

  • PlanDocument

    A whole plan: its steps in topological order, and the edges between them.

Functions:

  • dump

    Describe root and everything it reads as a portable document.

  • load

    Rebuild a plan from a document, returning its apex.

Attributes:

StepSpec module-attribute

SpecT module-attribute

SpecT = TypeVar('SpecT', bound=BaseModel)

StepT module-attribute

StepT = TypeVar('StepT', bound=Step)

StepNode

Bases: BaseModel


              flowchart TD
              matchlab.document.StepNode[StepNode]

              

              click matchlab.document.StepNode href "" "matchlab.document.StepNode"
            

One step: its kind, what it specifies, what it reads, and what it needs.

Attributes:

model_config class-attribute instance-attribute

model_config = ConfigDict(frozen=True)

kind class-attribute instance-attribute

kind: StepKind = Field(description='Which kind of step this is.')

spec class-attribute instance-attribute

spec: StepSpec = Field(description="This step's own settings, exactly what its fingerprint hashes.")

inputs class-attribute instance-attribute

inputs: tuple[int, ...] = Field(default=(), description="Positions of this step's inputs, in order. Always smaller than this step's own position, and order matters: it is the order the fingerprint folds parents in, and the order a linker's left and right arrive in.")

resources class-attribute instance-attribute

resources: dict[str, str] = Field(default_factory=dict, description="Settings field name to resource name, for every field this step's methodology was given as a `matchlab.resources.Resource`. Here rather than in the spec because a resource describes reconstruction, not output: renaming one changes no fingerprint. `load` fills each from its `resources` argument.")

PlanDocument

Bases: BaseModel


              flowchart TD
              matchlab.document.PlanDocument[PlanDocument]

              

              click matchlab.document.PlanDocument href "" "matchlab.document.PlanDocument"
            

A whole plan: its steps in topological order, and the edges between them.

Methods:

Attributes:

model_config class-attribute instance-attribute

model_config = ConfigDict(frozen=True)

steps class-attribute instance-attribute

steps: tuple[StepNode, ...] = Field(description="Every step reachable from the plan's apex, inputs before consumers.")

required_resources

required_resources() -> set[str]

Every resource name load must be given to rebuild this plan.

Read straight off each node, because a resource is named in its own field rather than hidden among settings. See matchlab.resources.

dump

dump(root: Step, *, indent: int | None = None) -> str

Describe root and everything it reads as a portable document.

JSON rather than an object, because a document exists to be put somewhere: a column, an object store, a request body, a file.

Parameters:

  • root

    (Step) –

    The apex of the plan. Only what is reachable upstream of it is included, exactly as collect() would run.

  • indent

    (int | None, default: None ) –

    How the JSON is laid out. None, the default, is compact.

Returns:

  • str

    The document, as JSON.

Raises:

  • ResourceError

    If a resource was passed without a name, so there is nothing for a document to record. A name covering two different objects is refused earlier, when the plan is built.

load

Rebuild a plan from a document, returning its apex.

Nothing is collected. This reconstructs the same lazy plan, so the returned step fingerprints identically to the one that was dumped, given the same data.

Parameters:

  • document

    (str | bytes) –

    The JSON dump produced.

  • resources

    (Mapping[str, Any]) –

    Name to object, for every resource the document names.

Returns:

  • Step

    The plan's apex, the last step in the document.

Raises:

  • ValidationError

    If the JSON does not describe a plan document.

  • ValueError

    If the document is empty, names an unregistered location class, or wires a step to an input of the wrong kind.

  • ResourceError

    If a named resource was not supplied.