Skip to content

procedure

procedure #

The Procedure[T] base class — a node in the capture DAG.

A procedure is one node of a directed acyclic graph that the companion app executes; Python only declares the graph. The generic parameter T is the CGType the node produces — a value (Image, Array[Image]), a Path[...] position, or Void.

The class body is a top-down index of the node's abilities: each concern is defined in its own module and imported here, so go to definition on any member lands on its implementation.

The public authoring names re-export through here, so a node module needs only::

from capturegraph.procedures.procedure import (
    Input,
    Procedure,
    Setting,
    make_procedure,
)

Procedure #

Bases: ProcedureState

Base class for every procedure node.

Subclasses are declared with [make_procedure][], parameterized by the CGType they return, and use the field markers to declare their fields::

@make_procedure
class AssertBool(Procedure[Void]):
    condition: Input[Bool]
    message: Setting[str]

They inherit construction-time validation, identity by uuid, and the fluent operators. Python never executes procedures.

Attributes:

Name Type Description
label str | None

Optional human-readable description of what this node does.

Source code in capturegraph-lib/capturegraph/procedures/procedure/__init__.py
@make_procedure
class Procedure[T: CGType](ProcedureState, metaclass=ProcedureMeta):
    """Base class for every procedure node.

    Subclasses are declared with [make_procedure][], parameterized by the
    ``CGType`` they return, and use the field markers to declare their fields::

        @make_procedure
        class AssertBool(Procedure[Void]):
            condition: Input[Bool]
            message: Setting[str]

    They inherit construction-time validation, identity by ``uuid``, and the
    fluent operators. Python never executes procedures.

    Attributes:
        label: Optional human-readable description of what this node does.
    """

    label: str | None = None

    ### Methods — one leaf module per concern (see the module docstring) ###

    from capturegraph.procedures.procedure.methods.export import (
        __repr__ as __repr__,
        to_graphviz as to_graphviz,
        to_json as to_json,
    )
    from capturegraph.procedures.procedure.methods.fields import (
        _inputs as _inputs,
        _settings as _settings,
        _substeps as _substeps,
    )
    from capturegraph.procedures.procedure.methods.identity import (
        __eq__ as __eq__,
        __hash__ as __hash__,
        __str__ as __str__,
    )
    from capturegraph.procedures.procedure.methods.initialization import (
        __init_subclass__ as __init_subclass__,
        __post_init__ as __post_init__,
    )
    from capturegraph.procedures.procedure.methods.navigation import (
        __getitem__ as __getitem__,
        __iand__ as __iand__,
        __ior__ as __ior__,
        __setitem__ as __setitem__,
        exists as exists,
        keys as keys,
        load as load,
        load_or as load_or,
        navigate as __getattr__,
        values as values,
    )
    from capturegraph.procedures.procedure.methods.operators import (
        __and__ as __and__,
        __ge__ as __ge__,
        __gt__ as __gt__,
        __invert__ as __invert__,
        __le__ as __le__,
        __lt__ as __lt__,
        __or__ as __or__,
    )
    from capturegraph.procedures.procedure.methods.preloading import (
        preload as preload,
    )

    if TYPE_CHECKING:
        # The runtime ``__setattr__`` (installed by ``make_procedure``) keeps nodes
        # immutable while absorbing the ``|=`` / ``&=`` cache rebind (whose value is
        # the ``CacheProcedure`` the operator returns). Declared here so the type
        # checker allows ``path.field |= value`` on a navigable node.
        def __setattr__(self, name: str, value: CacheProcedure[Any]) -> None:
            """Type-checker-only stub; ``make_procedure`` installs the real version."""
            ...

    ### Output refinements ###

    def static_map_keys(self) -> frozenset[str] | None:
        """The keys of the ``Map`` this node produces, when known at authoring time.

        E.g. ``CaptureImagesByLabel``'s labels; ``None`` when they are only known at
        runtime. A value-level ``[key]`` access warns when a constant key is
        provably absent from this set; nodes whose keys are dynamic leave it
        ``None`` and are never second-guessed.
        """
        return None

    ### Labels ###

    def set_label(self, label: str) -> Self:
        """Set a label on this node (in place) and return it, for fluent chaining."""
        object.__setattr__(self, "label", label)
        return self

__and__(other) #

a & b — a BoolAnd node.

Source code in capturegraph-lib/capturegraph/procedures/procedure/methods/operators.py
def __and__(
    self: Procedure[Bool],
    other: Procedure[Bool],
) -> Procedure[Bool]:
    """``a & b`` — a ``BoolAnd`` node."""
    from capturegraph.procedures.nodes.process.operators.boolean import BoolAnd

    return BoolAnd(
        left=self,
        right=other,
    )

__eq__(other) #

Two procedures are equal iff they are the same node (same uuid).

Source code in capturegraph-lib/capturegraph/procedures/procedure/methods/identity.py
def __eq__(self: Procedure, other: object) -> bool:
    """Two procedures are equal iff they are the same node (same ``uuid``)."""
    from capturegraph.procedures.procedure import (
        Procedure,
    )

    return isinstance(other, Procedure) and self._uuid == other._uuid

__ge__(other) #

a >= bBoolNot(NumberLessThan(a, b)).

Source code in capturegraph-lib/capturegraph/procedures/procedure/methods/operators.py
def __ge__(
    self: Procedure[Number],
    other: Procedure[Number],
) -> Procedure[Bool]:
    """``a >= b`` — ``BoolNot(NumberLessThan(a, b))``."""
    return ~(self < other)

__getattr__(name) #

Read a struct field; bound as Procedure.__getattr__.

E.g. root.reference, CapturePanorama().preview. Only missing attributes reach here, and dunders never navigate.

Source code in capturegraph-lib/capturegraph/procedures/procedure/methods/navigation.py
def navigate(self: Procedure[Path], name: str) -> Procedure[Path]:
    """Read a struct field; bound as ``Procedure.__getattr__``.

    E.g. ``root.reference``, ``CapturePanorama().preview``. Only missing attributes
    reach here, and dunders never navigate.
    """
    if name.startswith("__") and name.endswith("__"):
        raise AttributeError(name)

    from capturegraph.procedures.procedure.authoring.dispatch import (
        navigable,
    )

    nav = navigable(self)
    if nav is None or not nav.has_field(name):
        raise AttributeError(name)

    if nav.is_path:
        require_no_saved_value(self)
    child = memoized_child(self, ("field", name), lambda: nav.nodes.field(source=self, field=name))
    return cast("Procedure[Path]", child)

__getitem__(key) #

__getitem__(
    self: Procedure[Array],
    key: int
    | Procedure[CGType]
    | JSONScalar
    | bool
    | float
    | str,
) -> Procedure[CGType]
__getitem__(
    self: Procedure[Map],
    key: int
    | Procedure[CGType]
    | JSONScalar
    | bool
    | float
    | str,
) -> Procedure[CGType]
__getitem__(
    self: Procedure[Path],
    key: int
    | Procedure[CGType]
    | JSONScalar
    | bool
    | float
    | str,
) -> Procedure[Path]

Index a navigable position or a value-access array / map.

On a navigated Path it selects an array element / map entry position (another Path). On a map value a node returns it selects the entry's value (MapKey); on the Array that a map's keys()/values() returns it selects the element value — a key scalar, or a Path[V] to navigate. The runtime dispatches all of these through the same navigable view.

Source code in capturegraph-lib/capturegraph/procedures/procedure/methods/navigation.py
def __getitem__(
    self: Procedure[Path] | Procedure[Array] | Procedure[Map],
    key: int | Procedure[CGType] | JSONScalar | bool | float | str,
) -> Procedure[CGType]:
    """Index a navigable position or a value-access array / map.

    On a navigated ``Path`` it selects an array element / map entry position
    (another ``Path``). On a map *value* a node returns it selects the entry's
    value (``MapKey``); on the ``Array`` that a map's ``keys()``/``values()``
    returns it selects the element *value* — a key scalar, or a ``Path[V]`` to
    navigate. The runtime dispatches all of these through the same ``navigable`` view.
    """
    from capturegraph.procedures.procedure.authoring.dispatch import (
        navigable,
    )

    nav = navigable(self)
    if nav is None or not (nav.is_array or nav.is_map):
        raise TypeError(f"{self} is not indexable (it is neither an array nor a map)")

    if nav.is_path:
        require_no_saved_value(cast("Procedure[Path]", self))
    child = subscript_child(cast("Procedure[Path]", self), nav, key, create=True)
    assert child is not None  # create=True always returns a child or raises
    return child

__gt__(other) #

a > bNumberLessThan with the operands swapped.

Source code in capturegraph-lib/capturegraph/procedures/procedure/methods/operators.py
def __gt__(
    self: Procedure[Number],
    other: Procedure[Number],
) -> Procedure[Bool]:
    """``a > b`` — ``NumberLessThan`` with the operands swapped."""
    return other < self

__hash__() #

Hash by node identity (the uuid), consistent with __eq__.

Source code in capturegraph-lib/capturegraph/procedures/procedure/methods/identity.py
def __hash__(self: Procedure) -> int:
    """Hash by node identity (the ``uuid``), consistent with ``__eq__``."""
    return hash(self._uuid)

__iand__(value) #

path &= value — cache the value, overwriting any stored value.

Source code in capturegraph-lib/capturegraph/procedures/procedure/methods/navigation.py
def __iand__[T: CGType](
    self: Procedure[Path[T]],
    value: Procedure[T],
) -> CacheProcedure[T]:
    """``path &= value`` — cache the value, overwriting any stored value."""
    return record_cache(self, value, skip_if_exists=False)

__init_subclass__() #

Record the subclass's return type from its Procedure[X] base.

Source code in capturegraph-lib/capturegraph/procedures/procedure/methods/initialization.py
def __init_subclass__(cls: type[Procedure]) -> None:
    """Record the subclass's return type from its ``Procedure[X]`` base."""
    from capturegraph.procedures.procedure import (
        Procedure,
    )

    super(Procedure, cls).__init_subclass__()
    declared = declared_return_type(cls)
    if declared is not None:
        cls._return_type = declared

__invert__() #

~a — a BoolNot node.

Source code in capturegraph-lib/capturegraph/procedures/procedure/methods/operators.py
def __invert__(self: Procedure[Bool]) -> Procedure[Bool]:
    """``~a`` — a ``BoolNot`` node."""
    from capturegraph.procedures.nodes.process.operators.boolean import BoolNot

    return BoolNot(value=self)

__ior__(value) #

path |= value — cache the value, skipping if already stored.

Source code in capturegraph-lib/capturegraph/procedures/procedure/methods/navigation.py
def __ior__[T: CGType](
    self: Procedure[Path[T]],
    value: Procedure[T],
) -> CacheProcedure[T]:
    """``path |= value`` — cache the value, skipping if already stored."""
    return record_cache(self, value, skip_if_exists=True)

__le__(other) #

a <= bBoolNot(NumberLessThan(b, a)).

Source code in capturegraph-lib/capturegraph/procedures/procedure/methods/operators.py
def __le__(
    self: Procedure[Number],
    other: Procedure[Number],
) -> Procedure[Bool]:
    """``a <= b`` — ``BoolNot(NumberLessThan(b, a))``."""
    return ~(other < self)

__lt__(other) #

a < b — a NumberLessThan node.

Source code in capturegraph-lib/capturegraph/procedures/procedure/methods/operators.py
def __lt__(
    self: Procedure[Number],
    other: Procedure[Number],
) -> Procedure[Bool]:
    """``a < b`` — a ``NumberLessThan`` node."""
    from capturegraph.procedures.nodes.process.operators.number import (
        NumberLessThan,
    )

    return NumberLessThan(
        left=self,
        right=other,
    )

__or__(other) #

a | b — a BoolOr node.

Source code in capturegraph-lib/capturegraph/procedures/procedure/methods/operators.py
def __or__(
    self: Procedure[Bool],
    other: Procedure[Bool],
) -> Procedure[Bool]:
    """``a | b`` — a ``BoolOr`` node."""
    from capturegraph.procedures.nodes.process.operators.boolean import BoolOr

    return BoolOr(
        left=self,
        right=other,
    )

__post_init__() #

Stamp identity, type-check the node, and register it with the context.

Constructing a node registers it with the active recording context (which is how a procedure body is built — see capturegraph.procedures.procedure.authoring.contexts). There must be one: a node built with no active context raises, unless under cg.detached().

Source code in capturegraph-lib/capturegraph/procedures/procedure/methods/initialization.py
def __post_init__(self: Procedure) -> None:
    """Stamp identity, type-check the node, and register it with the context.

    Constructing a node registers it with the active recording context (which
    is how a procedure body is built — see
    [capturegraph.procedures.procedure.authoring.contexts][]). There must be one: a node
    built with no active context raises, unless under ``cg.detached()``.
    """
    object.__setattr__(self, "_uuid", _new_uuid())
    object.__setattr__(self, "_children", {})
    validation.validate(self)
    from capturegraph.procedures.procedure.authoring.contexts.base import (
        ProcedureContext,
    )

    ProcedureContext.current()._collect(self)

__repr__() #

The node's DAG as pretty-printed JSON.

Equivalent to this node's to_json.

Source code in capturegraph-lib/capturegraph/procedures/procedure/methods/export.py
def __repr__(self: Procedure) -> str:
    """The node's DAG as pretty-printed JSON.

    Equivalent to this node's
    [to_json][capturegraph.procedures.procedure.methods.export.to_json].
    """
    return to_json(self)

__setattr__(name, value) #

Type-checker-only stub; make_procedure installs the real version.

Source code in capturegraph-lib/capturegraph/procedures/procedure/__init__.py
def __setattr__(self, name: str, value: CacheProcedure[Any]) -> None:
    """Type-checker-only stub; ``make_procedure`` installs the real version."""
    ...

__setitem__(key, value) #

Absorb the arr[i] |= value / map[k] &= value rebind.

arr[i] |= value desugars to arr.__setitem__(i, arr[i].__ior__(value)); the cache was already recorded by __ior__, so this swallows it — but only when it is this exact slot's own cache (its destination is the navigation child for key), mirroring the strict struct-field __setattr__ guard. A plain arr[i] = value, or a cache built for a different slot, is rejected.

Source code in capturegraph-lib/capturegraph/procedures/procedure/methods/navigation.py
def __setitem__(
    self: Procedure[Path],
    key: int | Procedure[CGType] | JSONScalar | bool | float | str,
    value: CacheProcedure[CGType],
) -> None:
    """Absorb the ``arr[i] |= value`` / ``map[k] &= value`` rebind.

    ``arr[i] |= value`` desugars to ``arr.__setitem__(i, arr[i].__ior__(value))``;
    the cache was already recorded by ``__ior__``, so this swallows it — but only
    when it is *this exact slot's* own cache (its ``destination`` is the navigation
    child for ``key``), mirroring the strict struct-field ``__setattr__`` guard. A
    plain ``arr[i] = value``, or a cache built for a different slot, is rejected.
    """
    from capturegraph.procedures.nodes.destination.cache import (
        CacheProcedure,
    )
    from capturegraph.procedures.procedure.authoring.dispatch import (
        navigable,
    )

    nav = navigable(self)
    if nav is not None and isinstance(value, CacheProcedure):
        if value.destination is subscript_child(self, nav, key, create=False):
            return
    raise TypeError(
        "cache at an indexed slot with '|=' (skip if exists) or '&=' (overwrite), "
        f"not plain assignment (tried to set [{key!r}])"
    )

__str__() #

"label (Kind)", or just "Kind" when unlabeled.

Source code in capturegraph-lib/capturegraph/procedures/procedure/methods/identity.py
def __str__(self: Procedure) -> str:
    """``"label (Kind)"``, or just ``"Kind"`` when unlabeled."""
    return f"{self.label} ({self.kind.__name__})" if self.label else self.kind.__name__

exists() #

path.exists() — a Bool that is true when a value is stored here.

Reads the position from disk (LoadPath) and reports whether that read completes (ProcedureCompleted), so a missing value yields false instead of aborting the run. Pair it with cg.when(...) to run a block only once the position has been written. The read goes to disk regardless of any value cached at this slot this run, so it tests what was already present before the run.

Source code in capturegraph-lib/capturegraph/procedures/procedure/methods/navigation.py
def exists(self: Procedure[Path]) -> Procedure[Bool]:
    """``path.exists()`` — a ``Bool`` that is true when a value is stored here.

    Reads the position from disk (``LoadPath``) and reports whether that read
    completes (``ProcedureCompleted``), so a missing value yields ``false`` instead
    of aborting the run. Pair it with ``cg.when(...)`` to run a block only once the
    position has been written. The read goes to disk regardless of any value cached
    at this slot this run, so it tests what was already present before the run.
    """
    from capturegraph.procedures.nodes.destination.access import (
        LoadPath,
    )
    from capturegraph.procedures.nodes.process.control import (
        ProcedureCompleted,
    )

    return ProcedureCompleted(procedure=LoadPath(source=self))

keys() #

A map position's keys as a read-only Array[K] value (index to select one).

Source code in capturegraph-lib/capturegraph/procedures/procedure/methods/navigation.py
def keys(self: Procedure[Path]) -> Procedure[Array]:
    """A map position's keys as a read-only ``Array[K]`` value (index to select one)."""
    from capturegraph.procedures.procedure.authoring.dispatch import (
        MAP_KEYS,
    )

    require_map_position(self, "keys")
    return cast(
        "Procedure[Array]",
        memoized_child(self, ("keys",), lambda: MAP_KEYS(source=self)),
    )

load() #

path.load() — read the value stored at this position.

Loading is explicit: an unwritten position used as a value never lowers to a disk read on its own. The read waits for a stored value (a prior run's, or one an interceptor fills); while nothing is stored it stays pending, so use [load_or][] when an absent value should produce a fallback instead. The read goes to disk regardless of any value cached at this slot this run, so it sees what was already present before the run.

Source code in capturegraph-lib/capturegraph/procedures/procedure/methods/navigation.py
def load[T: CGType](self: Procedure[Path[T]]) -> Procedure[T]:
    """``path.load()`` — read the value stored at this position.

    Loading is explicit: an unwritten position used as a value never lowers to a
    disk read on its own. The read waits for a stored value (a prior run's, or
    one an interceptor fills); while nothing is stored it stays pending, so use
    [load_or][] when an absent value should produce a fallback instead. The
    read goes to disk regardless of any value cached at this slot this run, so it
    sees what was already present before the run.
    """
    from capturegraph.procedures.nodes.destination.access import (
        LoadPath,
    )

    # The compile-time validation pass checks the node against the schema
    # position, so the declared target is enforced before execution.
    return cast("Procedure[T]", LoadPath(source=self))

load_or(default) #

load_or(
    self: Procedure[Path[T]], default: Procedure[T]
) -> Procedure[T]
load_or(
    self: Procedure[Path], default: object
) -> Procedure[CGType]

path.load_or(default) — the stored value, or default's value.

Returned when nothing is stored here. The fallback runs only on a definitive "nothing stored" answer — never while the value is still loading — so a slow remote read cannot be mistaken for an absent one. default is a procedure (or raw literal) whose type matches the position's — so a typed default names the result's type, and the compile-time validation pass enforces the match.

Source code in capturegraph-lib/capturegraph/procedures/procedure/methods/navigation.py
def load_or(
    self: Procedure[Path],
    default: object,
) -> object:
    """``path.load_or(default)`` — the stored value, or ``default``'s value.

    Returned when nothing is stored here. The fallback runs only on a definitive
    "nothing stored" answer — never while the value is still loading — so a slow
    remote read cannot be mistaken for an absent one. ``default`` is a procedure (or
    raw literal) whose type matches the position's — so a typed default names the
    result's type, and the compile-time validation pass enforces the match.
    """
    from capturegraph.procedures.nodes.destination.access import (
        LoadPath,
    )
    from capturegraph.procedures.procedure.authoring.bridge import (
        coerce_input,
    )

    return LoadPath(source=self, default=coerce_input(default))

preload(default) #

node.preload(default) — pre-answer this node with an adjustable default.

default is a procedure (or a raw literal, lifted to a constant) whose type matches this node's; the runtime preloads it in so the node starts complete and the user may adjust it before capturing::

session.photo &= cg.CaptureImage(
    raw_photo=cg.UserInputBool(label="RAW").preload(False),
)
Source code in capturegraph-lib/capturegraph/procedures/procedure/methods/preloading.py
def preload[T: CGType](
    self: Procedure[T],
    default: object,
) -> Procedure[T]:
    """``node.preload(default)`` — pre-answer this node with an adjustable default.

    ``default`` is a procedure (or a raw literal, lifted to a constant) whose
    type matches this node's; the runtime preloads it in so the node starts
    complete and the user may adjust it before capturing::

        session.photo &= cg.CaptureImage(
            raw_photo=cg.UserInputBool(label="RAW").preload(False),
        )
    """
    from capturegraph.procedures.nodes.process.structural import (
        PreloadProcedure,
    )
    from capturegraph.procedures.procedure.authoring.bridge import (
        coerce_input,
    )

    return cast(
        "Procedure[T]",
        PreloadProcedure(procedure=self, default=coerce_input(default)),
    )

set_label(label) #

Set a label on this node (in place) and return it, for fluent chaining.

Source code in capturegraph-lib/capturegraph/procedures/procedure/__init__.py
def set_label(self, label: str) -> Self:
    """Set a label on this node (in place) and return it, for fluent chaining."""
    object.__setattr__(self, "label", label)
    return self

static_map_keys() #

The keys of the Map this node produces, when known at authoring time.

E.g. CaptureImagesByLabel's labels; None when they are only known at runtime. A value-level [key] access warns when a constant key is provably absent from this set; nodes whose keys are dynamic leave it None and are never second-guessed.

Source code in capturegraph-lib/capturegraph/procedures/procedure/__init__.py
def static_map_keys(self) -> frozenset[str] | None:
    """The keys of the ``Map`` this node produces, when known at authoring time.

    E.g. ``CaptureImagesByLabel``'s labels; ``None`` when they are only known at
    runtime. A value-level ``[key]`` access warns when a constant key is
    provably absent from this set; nodes whose keys are dynamic leave it
    ``None`` and are never second-guessed.
    """
    return None

to_graphviz(group_by_depth=False) #

This node's DAG as a Graphviz diagram for visualization.

See procedure_to_graphviz.

Source code in capturegraph-lib/capturegraph/procedures/procedure/methods/export.py
def to_graphviz(
    self: Procedure,
    group_by_depth: bool = False,
) -> Digraph:
    """This node's DAG as a Graphviz diagram for visualization.

    See
    [procedure_to_graphviz][capturegraph.procedures.procedure.exporting.visualizer.procedure_to_graphviz].
    """
    from capturegraph.procedures.procedure.exporting.visualizer import (
        procedure_to_graphviz,
    )

    return procedure_to_graphviz(self, group_by_depth=group_by_depth)

to_json(indent=4, target_schema=None) #

This node's DAG as a JSON string in the cross-platform wire form.

See procedure_to_json.

Source code in capturegraph-lib/capturegraph/procedures/procedure/methods/export.py
def to_json(
    self: Procedure,
    indent: int = 4,
    target_schema: type[CGType] | None = None,
) -> str:
    """This node's DAG as a JSON string in the cross-platform wire form.

    See
    [procedure_to_json][capturegraph.procedures.procedure.exporting.serializer.procedure_to_json].
    """
    from capturegraph.procedures.procedure.exporting.serializer import (
        procedure_to_json,
    )

    return procedure_to_json(self, indent=indent, target_schema=target_schema)

values() #

A map position's entry positions as Array[Path[V]].

Fan out, or index to select one to navigate.

Source code in capturegraph-lib/capturegraph/procedures/procedure/methods/navigation.py
def values(self: Procedure[Path]) -> Procedure[Array]:
    """A map position's entry positions as ``Array[Path[V]]``.

    Fan out, or index to select one to navigate.
    """
    from capturegraph.procedures.procedure.authoring.dispatch import (
        MAP_VALUES,
    )

    require_map_position(self, "values")
    return cast(
        "Procedure[Array]",
        memoized_child(self, ("values",), lambda: MAP_VALUES(source=self)),
    )