Job Module — RunContext¶
job
¶
Public job authoring API for functualize.
This package is the primary import point for job authors:
from functualize.job import RunContext, Log, Invoke, Prompt, Perf, State
from functualize.job import JobContext, JobConfigView
from functualize.job import Arg, Option, Stdin
It also exports job decorator utilities for metadata and hooks.
__all__ = ['RunContext', 'RunStatus', 'FromJob', 'Log', 'Invoke', 'Prompt', 'Perf', 'Shell', 'ShellError', 'ShellResult', 'Responder', 'FailingResponder', 'Sources', 'Freshness', 'FreshnessVerdict', 'State', 'Stdout', 'JobContext', 'JobConfigView', 'TTY', 'Live', 'TerminalUnavailable', 'Arg', 'Option', 'Stdin', 'GroupOptions', 'job', 'JobDeclaration', 'Deps', 'Fingerprint', 'Guards', 'Exec', 'Retry', 'Precondition', 'Call', 'call', '_make_global_only_decorator', '_make_hook_decorator', '_make_middleware_decorator', 'suppress_live', 'surface_hint']
module-attribute
¶
FromJob(job, /, *, run=True)
¶
A reference to another job, used in a parameter annotation.
See the module docstring for the two accepted forms. Instances are the
metadata carried inside Annotated; FromJob[...] builds that
Annotated for you.
run=False reads without causing work. The default is that
referencing a value declares the dependency — the idiom every comparable
system follows (doit's getargs "creates an implicit setup-task";
Dagster infers upstreams "from the arguments to the decorated function").
run=False opts out: use the recorded value if there is one, never
trigger execution, and contribute no dependency edge. That is what a
reporting job wants — read the last build's result, do not cause a build.
With run=False and nothing recorded, the parameter falls back to its
default; a parameter with no default raises, because silently injecting
None would make "never ran" indistinguishable from "returned nothing".
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
job
|
str | Callable[..., Any]
|
The upstream job — its registered name, or the decorated function.
Positional-only, matching :class: |
required |
run
|
bool
|
Whether a missing or stale value may trigger the upstream. |
True
|
Source code in src/functualize/_types/from_job.py
__slots__ = ('_job', '_run')
class-attribute
instance-attribute
¶
job
property
¶
The referenced job, as written.
run
property
¶
Whether resolving this reference may execute the upstream.
name
property
¶
The upstream job's name.
__setattr__(name, value)
¶
__class_getitem__(job)
¶
Reject the subscript form with the syntax that actually works.
X[...] is the conventional way to parameterize a Python generic, so
it is the first thing a reader will try. Without this they would get
either a bare "not subscriptable" or — worse — a working runtime object
that their type checker rejects, with nothing pointing at the fix.
Source code in src/functualize/_types/from_job.py
__repr__()
¶
__eq__(other)
¶
Call(target, kwargs=dict())
dataclass
¶
A parameterized dependency reference — a job plus bound keyword args.
Produced by the call() factory. Lets a dependency carry config
overrides, which parameterized deps require: e.g.
call(build, target="wheel") is a distinct dep from
call(build, target="sdist") (proposal §A.4).
Deps(*refs, policy='fail-fast')
dataclass
¶
A job's dependency set and its failure policy (proposal §A.4).
Refs are positional and may each be a registered-job name string, a
callable (resolved to a name at discovery — refactor-safe and
IDE-navigable), or a call(...) for parameterized deps. Unknown names,
unregistered callables, and cycles are boot errors (resolved at discovery).
Source code in src/functualize/_types/job_declaration.py
refs
instance-attribute
¶
policy
instance-attribute
¶
__post_init__()
¶
Source code in src/functualize/_types/job_declaration.py
to_dict()
¶
Source code in src/functualize/_types/job_declaration.py
from_dict(data)
classmethod
¶
Reconstruct from cache form. Opaque callable refs materialize as their recorded name string (the live callable is unavailable off-cache).
Source code in src/functualize/_types/job_declaration.py
Exec(retry=None, platforms=None, run='always', silent=False)
dataclass
¶
Execution policy for a job (proposal §A.5).
run selects dedup behavior: "always", "once" (per session,
ignoring args), or "when_changed" (per session, keyed on identical
resolved args).
There is no job-level timeout, deliberately. Python cannot preempt
a running function, so any such field could only report an overrun while
the work continued — and a caller believing the job stopped may release a
lock or delete a file the still-live job is using. The two mature runners
in this space reached the same conclusion: invoke has no task-level
timeout (only run(cmd, timeout=), a threading.Timer that
SIGKILLs a subprocess), and doit has none at all — its
doit.tools.timeout is an uptodate checker, a freshness TTL, which
is what Fingerprint does here. Bound the work where the OS can enforce
it: sh(..., timeout=N) kills the process group (§B.4).
retry = None
class-attribute
instance-attribute
¶
platforms = None
class-attribute
instance-attribute
¶
run = 'always'
class-attribute
instance-attribute
¶
silent = False
class-attribute
instance-attribute
¶
__post_init__()
¶
Source code in src/functualize/_types/job_declaration.py
to_dict()
¶
Source code in src/functualize/_types/job_declaration.py
from_dict(data)
classmethod
¶
Source code in src/functualize/_types/job_declaration.py
Fingerprint(sources=(), generates=(), method='checksum', decides=False)
dataclass
¶
Up-to-date-checking inputs and outputs for a job (proposal §A.3, §D.3).
sources and generates are both glob patterns —
generates=["dist/*.whl"] is a declaration about a wheel whose version is
not known in advance, not about a file literally named *.whl.
method selects the staleness test.
Neither is required to live under the project (ADR-013). An absolute
pattern, a ../ pattern, and a pattern reaching through a symlinked
directory are all declarable; each path is recorded as written, so an
absolute one does not match on another machine and that machine re-runs the
job once.
A declared output that is not on disk forces a run under every method, and a pattern matching nothing counts as not on disk. Otherwise a job whose inputs were unchanged would report fresh with its promised artifact deleted.
decides moves the skip decision from the framework to the job:
============================ ===============================================
Declaration A fresh verdict means
============================ ===============================================
Fingerprint(sources=[…]) the job is skipped — the default, unchanged
decides=True the body runs and decides what to do
============================ ===============================================
The default is the first row on purpose: opting out is a decision a job
author makes, never one they inherit. A job that opts in reads
:class:~functualize.job.Freshness and returns whatever it likes — its
cached artifact, or real work. It does not get to report
RunStatus.SKIPPED: "the framework skipped me" and "I ran and decided to
do nothing" stay distinguishable in history.
sources = ()
class-attribute
instance-attribute
¶
generates = ()
class-attribute
instance-attribute
¶
method = 'checksum'
class-attribute
instance-attribute
¶
decides = False
class-attribute
instance-attribute
¶
__post_init__()
¶
Source code in src/functualize/_types/job_declaration.py
to_dict()
¶
Source code in src/functualize/_types/job_declaration.py
from_dict(data)
classmethod
¶
Source code in src/functualize/_types/job_declaration.py
Guards(preconditions=(), status=())
dataclass
¶
Pre-flight and up-to-date guards for a job (proposal §A.3, §D.2).
preconditions refuse the run when unmet; status checks report the
job already up-to-date (skip). Precondition items may be shell strings,
callables, or Precondition objects; status items are shell strings or
callables.
preconditions = ()
class-attribute
instance-attribute
¶
status = ()
class-attribute
instance-attribute
¶
__post_init__()
¶
Source code in src/functualize/_types/job_declaration.py
to_dict()
¶
Source code in src/functualize/_types/job_declaration.py
from_dict(data)
classmethod
¶
Reconstruct from cache form. Precondition items materialize as
Precondition objects; status items as their recorded check strings.
Source code in src/functualize/_types/job_declaration.py
JobDeclaration(group=None, extra_description=None, category=None, examples=(), tags=(), visibility='external', config_section=None, deps=None, cache=None, guards=None, exec=None)
dataclass
¶
The frozen, aggregate declaration attached by @job (proposal §A.3).
Identity/description fields stay flat (they are what @job is about);
operational concerns are the grouped value objects. name/group are
stored as declared (None means "fall back to convention"): discovery
resolves group against the module-level JOB_GROUP and name
against __name__ (proposal §A.3). Serializes to/from the discovery cache
via to_dict/from_dict; opaque callables/exception-types do not
round-trip (see module docstring).
group = None
class-attribute
instance-attribute
¶
extra_description = None
class-attribute
instance-attribute
¶
category = None
class-attribute
instance-attribute
¶
examples = ()
class-attribute
instance-attribute
¶
tags = ()
class-attribute
instance-attribute
¶
visibility = 'external'
class-attribute
instance-attribute
¶
config_section = None
class-attribute
instance-attribute
¶
deps = None
class-attribute
instance-attribute
¶
cache = None
class-attribute
instance-attribute
¶
guards = None
class-attribute
instance-attribute
¶
exec = None
class-attribute
instance-attribute
¶
__post_init__()
¶
Source code in src/functualize/_types/job_declaration.py
to_dict()
¶
Source code in src/functualize/_types/job_declaration.py
from_dict(data)
classmethod
¶
Source code in src/functualize/_types/job_declaration.py
Precondition(cmd_or_callable, msg=None)
dataclass
¶
A pre-flight check plus an optional human-facing failure message.
The check is a shell-command string (run, non-zero = refuse) or a callable (falsy return = refuse) — proposal §A.3, §D.2.
cmd_or_callable
instance-attribute
¶
msg = None
class-attribute
instance-attribute
¶
__post_init__()
¶
Source code in src/functualize/_types/job_declaration.py
to_dict()
¶
Retry(attempts, backoff='exponential', on=(), on_exit_codes=())
dataclass
¶
Retry policy for a job (proposal §A.5).
on narrows retries to specific exception types; on_exit_codes
narrows to specific ShellError exit codes. Empty means "retry on any
failure".
attempts
instance-attribute
¶
backoff = 'exponential'
class-attribute
instance-attribute
¶
on = ()
class-attribute
instance-attribute
¶
on_exit_codes = ()
class-attribute
instance-attribute
¶
__post_init__()
¶
Source code in src/functualize/_types/job_declaration.py
to_dict()
¶
from_dict(data)
classmethod
¶
Reconstruct from cache form. on exception types cannot round-trip
(recorded by name only) and materialize empty off-cache.
Source code in src/functualize/_types/job_declaration.py
TTY(*, caps, available, funcapp=None)
¶
Terminal-ownership capability delivered by DI to a job that owns its UI.
You cannot obtain the handle without declaring the parameter, and you cannot declare the parameter without the router seeing it — one artifact serves both the static routing decision and the runtime handle.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
caps
|
dict[type, Any]
|
The per-invocation capability map; |
required |
available
|
bool
|
Whether terminal ownership can be granted in this context. |
required |
Source code in src/functualize/_engine/capabilities/tty.py
ctx
property
¶
The RunContext for this execution — the app's API handle.
Resolved lazily from the per-invocation capability map, so it is
populated by the time the job body runs regardless of parameter order.
Returns None only if the job declared no rc: RunContext parameter
(a fuller guarantee arrives with the Phase 5 orchestrator handoff).
run(app)
¶
Run a job-owned app while it owns the terminal.
Raises:
| Type | Description |
|---|---|
TerminalUnavailable
|
If terminal ownership cannot be granted here (MCP / CI / piped / background). |
TypeError
|
If |
Source code in src/functualize/_engine/capabilities/tty.py
FailingResponder(pattern, response, sentinel)
dataclass
¶
Bases: Responder
A :class:Responder that also aborts on a failure sentinel (§B.4).
Responds like :class:Responder, but if sentinel appears in the output
the command is killed and a :class:ShellError is raised. This is how a
sudo password responder aborts on Sorry, try again instead of
answering the re-prompt forever.
Subclasses :class:Responder so it is accepted anywhere watchers are.
Attributes:
| Name | Type | Description |
|---|---|---|
pattern |
str | Pattern[str]
|
Regex whose match triggers |
response |
str
|
Text written to stdin on each new |
sentinel |
str | Pattern[str]
|
Regex whose appearance aborts the command. |
sentinel
instance-attribute
¶
Freshness(verdict=None)
¶
The verdict this job's own Fingerprint produced, for the job to act on.
Declared as a job parameter, like every other capability::
@job(cache=Fingerprint(sources=["src/**/*.py"], decides=True))
def build(fresh: Freshness) -> str:
verdict = fresh.verdict()
if verdict is not None and verdict.is_fresh:
return "artifact already current"
return rebuild()
decides=True is what makes the branch above reachable, and it is not
decoration. Without it the engine skips the job for you and returns before
the body runs, so is_fresh is the one value that branch can never see —
which is the misconception this capability exists to remove, reproduced in
its own documentation until a review ran the snippet (jof S1). The rule:
ask for the verdict only where you have taken the decision back.
:meth:verdict returns None when this job declares no Fingerprint —
there was no decision, and a fabricated one would be a lie. A job that
declares one gets the verdict whether or not the framework used it to skip.
Source code in src/functualize/_engine/capabilities/freshness.py
__slots__ = ('_verdict',)
class-attribute
instance-attribute
¶
FreshnessVerdict(state, key, recorded_value, declared_sources, declared_generates, source_map)
dataclass
¶
What the pre-flight decided about this run, and the inputs it decided on.
It is the decision itself, not a summary of it: source_map is the very
mapping Sources exposes, and the declared patterns are the ones the
job's own Fingerprint named. A job asking "why am I fresh?" therefore
gets one answer from one capability rather than two answers free to drift.
Attributes:
| Name | Type | Description |
|---|---|---|
state |
GuardState
|
The pipeline outcome — |
key |
str
|
The fingerprint key the decision was computed under, so a job can see which recorded run it is being compared against. |
recorded_value |
Any | None
|
What the previous run recorded, when the pre-flight read a record and the verdict was a skip. |
declared_sources |
tuple[str, ...]
|
The input patterns as the job declared them. |
declared_generates |
tuple[str, ...]
|
The output patterns as the job declared them. |
source_map |
Mapping[str, Mapping[str, Any]]
|
|
state
instance-attribute
¶
key
instance-attribute
¶
recorded_value
instance-attribute
¶
declared_sources
instance-attribute
¶
declared_generates
instance-attribute
¶
source_map
instance-attribute
¶
is_fresh
property
¶
True when the engine decided this job's declared inputs are current.
Only SKIP_FRESH answers True. A satisfied status guard is a
different claim about a different thing ("already done"), and it is
deliberately not folded in here — the two states are distinct for the
same reason they are distinct in the guard pipeline.
Invoke
¶
Job-to-job invocation capability.
Allows a job to invoke other jobs by name or function reference, run jobs in parallel, or introspect job schemas. The actual execution is delegated to the engine — this class raises NotImplementedError until wired.
The engine replaces this stub with a fully-wired instance at runtime.
__call__(job_or_fn, *, config=None, awaits_input=None, available_tools=None, force_gate=False, gate_strategy=None, timeout=None, group_option_values=None, **kwargs)
¶
Invoke a job by name or function reference.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
job_or_fn
|
str | Callable[..., Any]
|
Job name string or registered callable. |
required |
config
|
BaseModel | None
|
Typed config (mutually exclusive with **kwargs). |
None
|
awaits_input
|
type[BaseModel] | None
|
BaseModel subclass describing input the job needs; its JSON schema is attached to the result metadata for the caller (and drives gate resolution when gate params are set). |
None
|
available_tools
|
list[str] | None
|
Restrict tool visibility at the gate (max 64). |
None
|
force_gate
|
bool
|
Dispatch gate strategy even when fully resolved. |
False
|
gate_strategy
|
GateStrategy | str | list[GateStrategy | str] | None
|
Override configured gate strategy for this invocation. |
None
|
timeout
|
float | None
|
Optional timeout in seconds. |
None
|
**kwargs
|
Any
|
Arguments to pass to the job. |
{}
|
Returns:
| Type | Description |
|---|---|
JobResult
|
A JobResult with status, return_value, exception, metadata. |
Raises:
| Type | Description |
|---|---|
JobNotFoundError
|
If callable is not registered. |
ValueError
|
If both config and kwargs are provided. |
ValueError
|
If available_tools contains unregistered tool names. |
ValueError
|
If available_tools has more than 64 entries. |
Source code in src/functualize/_engine/capabilities/invoke.py
parallel(jobs, *, timeout=None)
¶
Invoke 1-32 jobs concurrently, returning results in input order.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
jobs
|
Sequence[tuple[str | Callable[..., Any], dict[str, Any]]]
|
List of (job_or_fn, kwargs) tuples to execute concurrently. |
required |
timeout
|
float | None
|
Seconds the batch may run before unfinished jobs are
reported as timed out. |
None
|
Returns:
| Type | Description |
|---|---|
list[JobResult]
|
List of JobResult objects in the same order as input. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If more than 32 jobs are specified. |
Source code in src/functualize/_engine/capabilities/invoke.py
schema(job_or_fn)
¶
Retrieve the JobDescriptor for a job by name or function reference.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
job_or_fn
|
str | Callable[..., Any]
|
Job name string or registered callable. |
required |
Returns:
| Type | Description |
|---|---|
JobDescriptor
|
A JobDescriptor for the referenced job. |
Raises:
| Type | Description |
|---|---|
JobNotFoundError
|
If the callable/name is not registered. |
Source code in src/functualize/_engine/capabilities/invoke.py
JobConfigView(resolution_chain, default_section_prefix='general')
¶
Scoped, read-write config access for job execution.
Wraps ResolutionChain (parse-once, shared) with: - A section prefix (scoped to job name) - An in-memory override layer for programmatic set() - Same get/set/get_model API that RunContext.config exposed
Initialize with shared resolution chain and optional prefix.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
resolution_chain
|
ResolutionChain
|
The app's ResolutionChain (shared, read-only). |
required |
default_section_prefix
|
str
|
Initial section prefix for key lookups. |
'general'
|
Source code in src/functualize/_config/job_config.py
get(key, default=None, section=None)
¶
Retrieve a configuration value.
Resolution priority: 1. In-memory overrides (from set()) 2. ResolutionChain (env vars → file sources → defaults) 3. Caller-provided default parameter
This method never raises — it always returns a value or None.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
str
|
The configuration key. |
required |
default
|
Any
|
Fallback value if key not found anywhere. |
None
|
section
|
str | None
|
Section override. None uses default_section_prefix. |
None
|
Returns:
| Type | Description |
|---|---|
Any
|
Resolved value or default. |
Source code in src/functualize/_config/job_config.py
resolve_with_source(key, section=None)
¶
(source_type, source_id, value) for key, or None if unset.
The provenance-carrying sibling of :meth:get, for surfaces that must
report where a value came from as well as what it is. It consults the
same layers in the same order — overrides first, then the chain — which
is the whole reason it exists here rather than in the caller: the
display seam previously reached past this view to
config_view._chain and so could not see the override layer at all.
A value set through :meth:set was what the run used and not what any
surface showed.
Never raises: an unresolved key is an answer, not an error.
Source code in src/functualize/_config/job_config.py
set(key, value, section=None)
¶
Store a value in the in-memory override layer.
Does NOT write to disk. Override persists for this instance's lifetime.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
str
|
The configuration key. |
required |
value
|
Any
|
The value to store. |
required |
section
|
str | None
|
Section override. None uses default_section_prefix. |
None
|
Source code in src/functualize/_config/job_config.py
get_model(model_class, section=None)
¶
Resolve a Pydantic model from configuration values.
For each field in the model, resolves the value using the same priority as get(). Passes collected values to Pydantic for validation.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model_class
|
type[T]
|
Pydantic BaseModel subclass. |
required |
section
|
str | None
|
Section override. None uses default_section_prefix. |
None
|
Returns:
| Type | Description |
|---|---|
T
|
Validated model instance. |
Raises:
| Type | Description |
|---|---|
ValidationError
|
If values don't satisfy model schema. |
Source code in src/functualize/_config/job_config.py
set_prefix(prefix)
¶
Update the default section prefix.
Called by RunContext.init to scope lookups to the job name.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
prefix
|
str
|
New default section prefix. |
required |
Source code in src/functualize/_config/job_config.py
JobContext(name, trace_id=None, span_id=None, cwd=None, job_directory=None, invoke_depth=0, scope_id=None, metadata=(lambda: MappingProxyType({}))())
dataclass
¶
Immutable execution context for the current job invocation.
Attributes:
| Name | Type | Description |
|---|---|---|
name |
str
|
The job name being executed. |
trace_id |
str | None
|
Optional distributed trace identifier. |
span_id |
str | None
|
Optional 16 hex-character span identifier from PropagationContext. |
cwd |
Path | None
|
Optional working directory for the job execution. |
job_directory |
Path | None
|
Optional filesystem directory containing the job's source module. |
invoke_depth |
int
|
Nesting depth of invocations (0 at top-level, increments per nest). |
scope_id |
str | None
|
Optional active WorkflowScope ID. |
metadata |
MappingProxyType[str, Any]
|
Read-only mapping of arbitrary key-value metadata. |
name
instance-attribute
¶
trace_id = None
class-attribute
instance-attribute
¶
span_id = None
class-attribute
instance-attribute
¶
cwd = None
class-attribute
instance-attribute
¶
job_directory = None
class-attribute
instance-attribute
¶
invoke_depth = 0
class-attribute
instance-attribute
¶
scope_id = None
class-attribute
instance-attribute
¶
metadata = field(default_factory=(lambda: MappingProxyType({})))
class-attribute
instance-attribute
¶
Live(*, _zone=None)
¶
Per-invocation live-display channel, bound to the active rendering surface.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
_zone
|
Any | None
|
The surface-side live zone that actually mounts constructs, or None for the degraded/kernel case (handles become no-ops). |
None
|
Source code in src/functualize/_engine/capabilities/live.py
suppress(name)
¶
Hide an ambient construct for this invocation.
Ambient constructs are the ones a plugin registered to render by default (a flow-viz tree, say). A job that wants a quiet output for one run drops it here::
def simple(live: Live) -> None:
live.suppress("flow-viz")
The declarative equivalent is @job(suppress_live=["flow-viz"]);
project-wide, it is [live] suppress. No-op in a degraded context.
Source code in src/functualize/_engine/capabilities/live.py
suppress_all()
¶
Hide every ambient construct for this invocation.
add(construct)
¶
Mount a passive construct (a Rich renderable) in the live zone.
Source code in src/functualize/_engine/capabilities/live.py
panel(construct)
¶
Mount an interactive construct as a PanelHost panel (j/k/Enter).
Requires an event loop; where none exists (STDOUT) it degrades to a passive render, and in MCP to event-emission — the same handle, honest degradation. In the kernel it is a no-op.
Source code in src/functualize/_engine/capabilities/live.py
Log(job_name=None)
¶
Structured logging capability for job functions.
Supports both direct call syntax and named level methods
log("message") # defaults to info log("message", level="warning") log.error("something broke")
When constructed with a job_name, log records are emitted to the
per-job logger functualize.job.<job_name>, which allows the TUI and
other handlers to capture output for the specific job. Without a job name
the fallback functualize.job logger is used (backward-compatible).
Source code in src/functualize/_engine/capabilities/log.py
__call__(message, level='info')
¶
Log a message at the specified level.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
message
|
object
|
The message to log (will be converted to str). |
required |
level
|
str
|
One of "debug", "info", "warning", "error", "critical". |
'info'
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If level is not a valid log level. |
Source code in src/functualize/_engine/capabilities/log.py
info(msg)
¶
warning(msg)
¶
error(msg)
¶
Perf
¶
Performance measurement capability.
Provides methods for marking instants and measuring durations during job execution. The actual implementation is backed by the observability layer and wired at runtime.
This class raises NotImplementedError until wired.
mark(name)
¶
Record an instant performance mark.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
The mark name (non-empty, max 256 characters). |
required |
Raises:
| Type | Description |
|---|---|
NotImplementedError
|
Until wired by the observability layer. |
Source code in src/functualize/_engine/capabilities/perf.py
mark_start(name)
¶
Start a named timing phase.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
The phase name (non-empty, max 256 characters). |
required |
Raises:
| Type | Description |
|---|---|
NotImplementedError
|
Until wired by the observability layer. |
Source code in src/functualize/_engine/capabilities/perf.py
mark_end(name)
¶
End a named timing phase.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
The phase name (must match a previous mark_start call). |
required |
Raises:
| Type | Description |
|---|---|
NotImplementedError
|
Until wired by the observability layer. |
Source code in src/functualize/_engine/capabilities/perf.py
phases(include=None, exclude=None)
¶
Retrieve recorded phases with optional filtering.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
include
|
str | None
|
Optional regex pattern — only return phases whose names match. |
None
|
exclude
|
str | None
|
Optional regex pattern — exclude phases whose names match. |
None
|
Returns:
| Type | Description |
|---|---|
list[Phase]
|
List of Phase objects matching the filters. |
Raises:
| Type | Description |
|---|---|
NotImplementedError
|
Until wired by the observability layer. |
Source code in src/functualize/_engine/capabilities/perf.py
Prompt(*, _provider=None, _rc=None, _caps=None)
¶
Ask the person on the other end, or be told there is none.
Reached two ways, and it is the same object either way (ADR-021):
rc.prompts.confirm(...) and a prompt: Prompt parameter.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
_provider
|
PromptCollector | None
|
A collector bound at construction. The kernel's own callers
( |
None
|
_caps
|
dict[type, Any] | None
|
The run's capability map, from which the RunContext — and
through it the live surface stack — is found at call time.
Lazy on purpose: DI resolution runs before the RunContext exists,
so capturing it eagerly captures |
None
|
Source code in src/functualize/_engine/capabilities/prompt.py
__slots__ = ('_caps', '_explicit_rc', '_provider')
class-attribute
instance-attribute
¶
ask(request)
¶
Send a fully-constructed request to the surface that can answer it.
The low-level form every convenience method routes through. Use it when
you need control over the PromptRequest fields.
Raises:
| Type | Description |
|---|---|
InputNotAvailable
|
Nothing can collect and the request is required with no default — the case where returning a default would fabricate an answer nobody gave. |
Source code in src/functualize/_engine/capabilities/prompt.py
confirm(question, *, destructive=False, default=None, context_message=None, context_data=None)
¶
Ask a yes/no question.
With no default the question is required: off a terminal it
raises rather than assuming an answer. Pass default to make the
non-interactive answer explicit.
Source code in src/functualize/_engine/capabilities/prompt.py
choice(question, choices, *, default=None, context_message=None)
¶
Present options and return the selected value.
Source code in src/functualize/_engine/capabilities/prompt.py
text(question, *, default=None, secret=False, placeholder=None, validator=None, context_message=None)
¶
Ask for free-form text. secret=True asks without echoing.
Source code in src/functualize/_engine/capabilities/prompt.py
Responder(pattern, response)
dataclass
¶
Answers an interactive prompt in a command's live output (§B.4).
When pattern (a regex) appears in new output, response is written to
the child's stdin. Use for scripted interactions — a tool asking
Continue? [y/N], a password prompt, etc. response should include its
own trailing newline if the program expects Enter.
Attributes:
| Name | Type | Description |
|---|---|---|
pattern |
str | Pattern[str]
|
Regex searched against live output (str or compiled pattern). |
response |
str
|
Text written to stdin on each new match. |
Shell
¶
Bases: Protocol
DI-injectable shell-command capability (proposal Part B).
Three command forms (§B.1), in order of preference:
- list —
sh(["docker", "build", "-t", tag, "."])— no shell, no quoting problem. The documented default idiom. - template —
sh("docker build -t {tag} .", tag=version)— each substituted value isshlex.quote-d before interpolation, then run without a shell. - raw —
sh("a | b", shell=True)— explicit shell interpretation; the caller owns quoting.
A raw string with neither shell=True nor template params is an error
(never a silent shlex.split) — that ambiguity is where injection bugs
live.
__call__(command, *, capture=True, stream=None, check=True, cwd=None, env=None, replace_env=False, in_stream=None, timeout=None, retry=None, shell=False, pty=False, watchers=None, background=False, label=None, silent=False, **template_params)
¶
Run command and return a :class:ShellResult.
stream=True routes live output to the surface's channel —
stdout when piped, the panel in the TUI (§C.1); a callable is an
explicit sink. silent=True suppresses the command echo, which is
otherwise written to the surface's diagnostic channel (stderr when
piped) so it can never corrupt piped data.
pty allocates a pseudo-terminal (POSIX; degrades to pipes on
Windows) so programs that check isatty() behave interactively.
watchers are :class:Responder/:class:FailingResponder objects
that answer prompts in the live output by writing to stdin.
Raises:
| Type | Description |
|---|---|
ShellError
|
If the command exits non-zero and |
ValueError
|
If a raw string is passed without |
Source code in src/functualize/_types/shell.py
cd(path)
¶
Run commands in path for the duration of the block (§B.3).
Nestable — a nested cd resolves relative to the enclosing one.
Overridden by an explicit per-call cwd=.
Source code in src/functualize/_types/shell.py
prefix(command)
¶
Prepend command to every command in the block (§B.3).
with sh.prefix(["poetry", "run"]): sh(["pytest"]) runs
poetry run pytest. Nestable; outer prefixes apply before inner.
Source code in src/functualize/_types/shell.py
defer(command, **kwargs)
¶
Register a cleanup command to run when the job exits (§B.5).
Deferred commands run LIFO on the engine's job-exit unwind — on
success, on failure, on Ctrl+C, and after a timeout — not through a
user try/finally, which a killed subprocess tree or a hard
timeout skips. kwargs are the options :meth:__call__ takes.
Source code in src/functualize/_types/shell.py
run_deferred()
¶
Run and clear all deferred commands, LIFO (engine-owned, §B.5).
Best-effort: a cleanup that fails must not mask the job's own outcome and must not stop the remaining cleanups.
sudo(command, *, preserve_env=False, password=None, watchers=None, **kwargs)
¶
Run command under sudo -S with an auto password responder (§B.4).
The password comes from password= or the injected
[shell] sudo_password secret, and is fed to sudo's stdin rather
than placed in the echoed command. Requires the list command form.
Raises:
| Type | Description |
|---|---|
ValueError
|
No password is available, or |
ShellError
|
The command itself failed. |
Source code in src/functualize/_types/shell.py
ShellError(result)
¶
Bases: Exception
Raised when a command exits non-zero under check=True (§B.2).
Carries the full :class:ShellResult so callers can inspect output.
Attributes:
| Name | Type | Description |
|---|---|---|
result |
The ShellResult for the failed command. |
Source code in src/functualize/_types/shell.py
result = result
instance-attribute
¶
ShellResult(returncode, stdout, stderr, command, duration_ms, pid=None)
dataclass
¶
The outcome of a single shell command (proposal §B.2).
Attributes:
| Name | Type | Description |
|---|---|---|
returncode |
int
|
Process exit code. |
stdout |
str
|
Captured standard output (empty string when not captured). |
stderr |
str
|
Captured standard error (empty string when not captured). |
command |
str
|
Display form of the command (secrets masked — §B.6). |
duration_ms |
float
|
Wall-clock duration in milliseconds. |
pid |
int | None
|
The child process id (or None if it never started). |
Sources(source_map=None, *, declared=False, generates=())
¶
The files this job's Fingerprint(sources=...) resolved to.
Reads as a mapping of project-relative POSIX path → {"mtime", "size",
"sha256"}::
@job(cache=Fingerprint(sources=["src/**/*.yaml"]))
def parse(sources: Sources) -> Parsed:
files = {path: Path(path).read_text() for path in sources.keys()}
declared is not the same question as emptiness, and conflating them is
the bug this exists next to:
============================================ ========== =========
Declaration declared items()
============================================ ========== =========
sources=["src/*.yaml"], files present True populated
sources=["absent/*.yaml"], no match True empty
no Fingerprint, or no sources False empty
============================================ ========== =========
The middle row is the R3 refusal's trigger, read here through the same mechanism rather than a second one.
Source code in src/functualize/_engine/capabilities/sources.py
__slots__ = ('_declared', '_generates', '_map')
class-attribute
instance-attribute
¶
declared
property
¶
True when the job declares Fingerprint(sources=...) at all.
Tells "declared, nothing matched" (True, empty) apart from "declared no sources" (False, empty) — which an empty mapping alone cannot.
generates
property
¶
Declared outputs, as project-relative POSIX paths.
items()
¶
keys()
¶
values()
¶
get(path, default=None)
¶
__len__()
¶
__iter__()
¶
__contains__(path)
¶
__getitem__(path)
¶
__bool__()
¶
Truthy when at least one input resolved.
Note this is emptiness, not :attr:declared — a job that declared
sources which matched nothing is falsy here and declared. That
pair is the whole point; see the class docstring.
Source code in src/functualize/_engine/capabilities/sources.py
State(backend)
¶
A run's key-value store, shared by every job in the run.
Reached as rc.state, or by declaring state: State. Both are the same
object (ADR-021).
Keys are flat and the whole run shares one namespace, so two jobs can pick
the same name. The answer is a naming convention rather than a framework
namespace — write state.set("fetch.rows", n) and read the namespace back
with state.keys("fetch.*"). One concept (a string) instead of two.
Values must be JSON-serializable; that is checked at write time, where the offending call is still on the stack, rather than at the write of the file.
Every write is one lock-read-write cycle against scopes.json. A job
writing a handful of keys will not notice; one writing hundreds in a loop
should hold :meth:batch.
Source code in src/functualize/_engine/capabilities/state.py
__slots__ = ('_backend',)
class-attribute
instance-attribute
¶
get(key, default=None)
¶
set(key, value)
¶
Store value under key.
Raises:
| Type | Description |
|---|---|
TypeError
|
|
Source code in src/functualize/_engine/capabilities/state.py
delete(key)
¶
keys(pattern='')
¶
Stored key names, optionally filtered by a glob pattern.
* stops at a . and ** crosses it::
state.keys("fetch.*") # one level under `fetch`
state.keys("fetch.**") # every depth under `fetch`
state.keys("*.rows") # the same leaf in any namespace
state.keys() # everything
So "fetch.*" cannot match "fetchmeta.x": the literal dot has to
match a real dot. A bare prefix is a startswith and does reach the
neighbouring namespace, which is why the pattern form is documented. A
pattern containing no * keeps the prefix meaning.
The matcher is the one the rest of the codebase uses —
rc.events.on_event("job.*") and perf-phase filtering call the same
function. A second glob implementation that agreed with this one today
is the divergence ADR-021 exists to prevent.
Raises:
| Type | Description |
|---|---|
TypeError
|
|
Source code in src/functualize/_engine/capabilities/state.py
to_dict()
¶
clear()
¶
batch()
¶
Hold the file lock across many writes, writing once at the end.
Every :meth:set is otherwise its own lock-read-write cycle against
scopes.json::
with state.batch():
for name, n in counts.items():
state.set(f"fetch.{name}", n)
An exception inside the block discards the block's writes rather than persisting some of them. A backend that cannot batch yields a plain null context, so this is always safe to write.
Source code in src/functualize/_engine/capabilities/state.py
Stdout
¶
Bases: Protocol
DI-injectable explicit stdout data channel (proposal Part C, revised).
Two methods, two intents:
emit(value)— serializevalueto stdout per the resolved--emit-formatformat, one logical document per call, flushed per call.valuemay bestr/bytes,dict/list, a pydantic model, a dataclass, or an iterable of those.--emit-formatdecides list handling:emit([a, b, c])is one JSON array underjsonand one line per item underndjson. To stream rows explicitly, loopfor r in rows: out.emit(r).write(data)— raw verbatim passthrough (strorbytes), no serialization. Forcat-like filters and binary output.
Unlike the old implicit return-value emission, an explicit emit/write
is never surface-suppressed: it always writes (on a TTY too), routed so
it coordinates with an active TUI, with secrets masked.
emit(value)
¶
TerminalUnavailable(message=None, *, job_name=None)
¶
Bases: Exception
Raised when a job needs an interactive terminal but none is available.
A job that declares tty: TTY (a hard requirement) owns the terminal for
the duration of tty.run(app). In contexts that cannot grant terminal
ownership — MCP, CI, piped/redirected I/O, background execution — the job is
refused with this error (pre-flight where the router can see the
requires_tty marker; at tty.run time otherwise), naming the fix.
Attributes:
| Name | Type | Description |
|---|---|---|
job_name |
The job that required a terminal, if known. |
Source code in src/functualize/_types/errors.py
job_name = job_name
instance-attribute
¶
RunContext(name, config, logger, metadata=None, *, plugin_configs=None, resources=None, perf_timeline=None, _workflow_scope=None, _invoke_depth=0, _parent_request=None, _run_id=None, _max_invoke_depth=10, _execution_engine=None, cwd=None, job_directory=None, _di_registry=None, _caps=None)
¶
Execution context injected into each job — thin facade delegating to capabilities.
Source code in src/functualize/_engine/capabilities/runcontext.py
MAX_INVOKE_DEPTH = 10
class-attribute
¶
prompts
property
¶
rc.prompts — ask the person on the other end, if there is one.
The same object a prompt: Prompt parameter receives (ADR-021).
Resolved through the capability map so the two doors cannot drift; the
fallback below builds one only for a context with no map at all.
events
property
¶
rc.events — events, phases, run status and the perf timeline.
wiring
property
¶
rc.wiring — the plugin configs and resources this app provides.
discovery
property
¶
rc.discovery — read-only questions about the registered jobs.
config
property
¶
name
property
¶
metadata
property
¶
result_metadata
property
¶
job_config
property
writable
¶
workflow_scope
property
¶
cwd
property
¶
This run's working directory: the one it named, or the project's.
The run's own cwd wins when the request carried one. Otherwise the
answer is the project root the engine's host knows, not the process's
working directory — which is what this used to return, and which is a
different directory whenever the two disagree.
job_directory
property
¶
state
property
¶
rc.state — the run's shared, durable key-value store.
The same object a state: State parameter receives (ADR-021).
Resolved through the capability map so the two doors cannot drift; the
scope is consulted only to build one when the job declared no state:
parameter and nothing has therefore materialised it yet.
on_log(callback)
¶
set_result_metadata(key, value)
¶
__getitem__(key)
¶
rc[T] — the run's instance of T.
The capability map first, the DI registry second (ADR-021). A
per-invocation capability lives in the map and is never in the
registry, so consulting only the registry reported a capability the job
was holding in its own hand as missing: rc[Log] raised
MissingProviderError and Log in rc was False while a log: Log
parameter had the object.
A qualified or named lookup skips the map deliberately. There is no
"the" Conn when two are registered under different qualifiers, so
those forms are exactly the ones that must reach the registry and let
it answer — see ADR-021 for the classes that cannot share one object.
Source code in src/functualize/_engine/capabilities/runcontext.py
__contains__(key)
¶
T in rc — is T reachable from this run?
Same order as :meth:__getitem__, and for the same reason: a
capability in the map is reachable even though the registry has never
heard of it.
Source code in src/functualize/_engine/capabilities/runcontext.py
invoke(job_name, *, _propagate_scope=True, timeout=None, **kwargs)
¶
Invoke another registered job. Delegates to Invoke capability.
Source code in src/functualize/_engine/capabilities/runcontext.py
invoke_parallel(jobs)
¶
Invoke multiple jobs concurrently. Delegates to Invoke.parallel().
log(message, level='info')
¶
Source code in src/functualize/_engine/capabilities/runcontext.py
RunStatus
¶
Bases: Enum
Status of a run context execution.
SUCCESS = 'Success'
class-attribute
instance-attribute
¶
FAILURE = 'Failure'
class-attribute
instance-attribute
¶
BLOCKED = 'Blocked'
class-attribute
instance-attribute
¶
SKIPPED = 'Skipped'
class-attribute
instance-attribute
¶
RUNNING = 'Running'
class-attribute
instance-attribute
¶
CANCELLED = 'Cancelled'
class-attribute
instance-attribute
¶
TIMEOUT = 'Timeout'
class-attribute
instance-attribute
¶
UNKNOWN = 'Unknown'
class-attribute
instance-attribute
¶
REFUSED = 'Refused'
class-attribute
instance-attribute
¶
resumable
property
¶
True when the run stopped at a declared pause point, not an error.
A workflow that reaches a Gate with no input is BLOCKED, not FAILURE:
it did everything it was asked to, and re-invoking it with the input
deposited continues from where it stopped. Callers that treat "not
SUCCESS" as "broken" — CI gates, Deps satisfaction, TUI colouring —
need to tell the two apart.
terminal
property
¶
True when the phase is over and cannot be transitioned from.
One definition, because there were two and they disagreed.
_engine/capabilities/workflow.py and
_engine/capabilities/runcontext.py each carried a _TERMINAL_STATES
frozenset; one included REFUSED and one did not. The first one's own
comment explains why the omission matters — "a refused step simply
never gets end_time or duration, so it reads as still running in
every consumer of this record" — and its sibling had exactly that
defect, one module away. The runcontext copy's test mirrored the
omission and asserted agreement with it, so the two could never
converge by failing.
This is the state machine's question — may a phase transition out
of here — and not the delivery question functualize.types.is_failure
answers. The membership is the one workflow.py reasoned about, kept
exactly: BLOCKED is a declared pause and resumable is the property
that says so; SKIPPED is a phase a run can still move on from; RUNNING
is the non-terminal case by definition; and UNKNOWN is deliberately
left transitionable, because a status nobody could determine should not
also be a state nobody can leave.
ran
property
¶
True when the job's body actually executed.
SKIPPED is not a failure — a guard or an up-to-date fingerprint
answered "no work to do", which is the point of declaring them. But it
is not SUCCESS either: a caller that treats them alike cannot tell a
build that ran from one that was already current, and func why
exists precisely to answer that.
Arg(help=None, metavar=None, show_default=True)
dataclass
¶
Mark a parameter as a positional CLI argument.
Non-CLI adapters ignore this marker — they see only the base type.
Usage::
from typing import Annotated
from functualize.job import Arg
def deploy(target: Annotated[str, Arg(help="Deploy target")]):
...
Option(*args, help=None, hidden=False, envvar=None)
dataclass
¶
Mark a parameter as a named CLI option with optional short flag.
Non-CLI adapters ignore this marker — they see only the base type.
Usage::
from typing import Annotated
from functualize.job import Option
def deploy(target: Annotated[str, Option("-t", "--target", help="Deploy target")]):
...
# Short flag only — long flag derived from param name:
def deploy(verbose: Annotated[bool, Option("-v")]):
...
Accept positional strings as flag names: Option("-t", "--target").
Positional arg detection:
- Single dash + one char (e.g. -t) → short flag
- Double dash + name (e.g. --target) → long flag
- Two positional args: detect by dash count, assign accordingly
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
*args
|
str
|
Flag name strings (short like |
()
|
help
|
str | None
|
Help text for the option. |
None
|
hidden
|
bool
|
Whether to hide this option from --help output. |
False
|
envvar
|
str | None
|
Environment variable name to read as fallback. |
None
|
Source code in src/functualize/_types/cli_markers.py
Stdin(flag=None, help=None, encoding='utf-8')
dataclass
¶
Mark a parameter as stdin-aware.
When stdin is piped and no explicit CLI value provided, reads from stdin. Flag value always wins over stdin (explicit > implicit).
Usage::
from typing import Annotated
from functualize.job import Stdin
def transform(data: Annotated[str, Stdin(flag="--data", help="Input data")]):
...
# Pipe usage: echo "hello" | func transform
GroupOptions
¶
Bases: BaseModel
Base class for per-group option declarations.
Subclass with a group= class keyword to bind the declaration to a
dotted group path. Fields carry the same Option markers used on job
parameters and are introspectable via model_fields.
__group_path__ = ''
class-attribute
¶
call(fn_or_name, **kwargs)
¶
Build a parameterized dependency reference (proposal §A.4).
Example::
deps=Deps(call(build, target="wheel"))
Source code in src/functualize/_types/job_declaration.py
job(_func=None, *, group=None, extra_description=None, category=None, examples=(), tags=(), visibility='external', config_section=None, deps=None, cache=None, guards=None, exec=None)
¶
Declare a job's identity and operational contract (proposal §A.3–A.6).
Identity and description are flat kwargs (that is what @job is about);
operational concerns are grouped, self-validating value objects
(Deps/Fingerprint/Guards/Exec). Usable bare (@job) for
plain opt-in or with any subset of kwargs::
@job
def build(sh: Shell): ...
@job(
group="infra",
deps=Deps("lint", "test"),
cache=Fingerprint(sources=["src/**/*.py"], generates=["dist/*.whl"]),
exec=Exec(retry=Retry(attempts=2)),
)
def deploy(sh: Shell, config: DeployConfig): ...
The frozen :class:JobDeclaration is stored on the function as
__functualize_job__; discovery reads it (falling back to convention).
group overrides the module-level JOB_GROUP; the addressable name
is always derived from __name__ by normalization, so there is exactly
one spelling of a job. Identity-preserving: decorated is original always holds,
so @job composes with other job decorators in any order.
Raises:
| Type | Description |
|---|---|
ValueError
|
If any field or value object violates its invariants (validated eagerly at decoration time). |
Source code in src/functualize/job/decorators.py
suppress_live(*names)
¶
Opt a job out of one or more ambient live constructs.
Ambient constructs are the ones a plugin registered to render by default (a flow-viz execution tree, say). A job whose output reads better without one says so declaratively::
@suppress_live("flow-viz")
def simple_task(log: Log) -> None:
log("No tree needed here")
The imperative equivalent is live.suppress("flow-viz") inside the body;
project-wide, it is [live] suppress in config.
Identity-preserving, like every decorator here: decorated is original.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
*names
|
str
|
Ambient construct names to suppress. Passing none is a no-op. |
()
|
Returns:
| Type | Description |
|---|---|
Callable[[F], F]
|
A decorator attaching the declaration to the function. |
Source code in src/functualize/job/decorators.py
surface_hint(surface)
¶
Declare a job's preferred render surface.
The surface-resolution ladder consults this before the
tui.default_surface setting and the framework default::
@surface_hint("stdout")
def report(log: Log) -> None:
log("Renders on the released terminal, even from the TUI")
A hint is a preference, not a requirement — a HARD constraint like a
bare tty: TTY capability still wins, and a "panel" hint is ignored
on a direct CLI run (there is no TUI to render a panel).
Identity-preserving, like every decorator here: decorated is original.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
surface
|
str
|
"stdout" or "panel". |
required |
Returns:
| Type | Description |
|---|---|
Callable[[F], F]
|
A decorator attaching the declaration to the function. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in src/functualize/job/decorators.py
__getattr__(name)
¶
Lazily materialize GroupOptions on first access (PEP 562).
Defining a Pydantic BaseModel subclass runs pydantic's plugin loader,
which imports every installed pydantic plugin — logfire pulls in
rich, for instance. functualize/__init__.py reaches this package on
every functualize import, so defining the class eagerly would drag those
dependencies into every process and break the CLI-dependency isolation
guarantee (tests/test_typer_isolation.py, now a CLI-dependency absence test).
Deferring costs nothing: only a project that actually declares group
options ever touches the class, and from functualize.job import
GroupOptions still works — Python falls back to this hook.
Source code in src/functualize/job/__init__.py
RunContext API Reference¶
functualize.job.RunContext is a thin facade (a 32-line re-export module) over
the ~780-LOC capability class in functualize._engine.capabilities.runcontext,
which delegates work to per-capability classes (Log, Invoke, Perf,
WorkflowTracker, ...). It provides configuration access, logging, metadata tracking, phase tracking, job invocation, event emission, and prompting.
Module location: src/functualize/job/context.py
from functualize.job import (
RunContext, Log, Invoke, Prompt, Perf, State, Sources, JobContext, JobConfigView,
)
Core Properties¶
| Property | Type | Description |
|---|---|---|
name |
str |
The job name for this execution. |
metadata |
dict[str, Any] |
Execution metadata (run_type, run_status, start_time, end_time, duration). |
result_metadata |
dict[str, Any] |
Mutable metadata dict carried to JobResult (64-key max). |
config |
JobConfigView |
The resolved configuration view for this job. |
job_config |
Any |
The Pydantic config model instance (settable). |
phases |
list[JobPhase] |
All tracked job phases in this execution. |
cwd |
Path |
The working directory for this execution. |
job_directory |
Path \| None |
The directory containing the job module. |
run_status |
RunStatus |
Current execution status. |
run_duration |
float |
Elapsed time in seconds since execution started. |
Logging¶
rc.log(message, level="info")¶
Emit a log message through the job's own Log
capability when the job declares one, so both routes share a sink; otherwise it
writes to the job's functualize.job.<name> logger, which is where Log would
have written too.
level must be one of debug, info, warning, error, critical — anything
else raises ValueError.
from functualize.job import Log, RunContext
def my_job(rc: RunContext) -> None:
rc.log("Starting processing") # info (default)
rc.log("Connecting to DB", level="debug")
rc.log("Retrying request", level="warning")
def either_way(rc: RunContext, log: Log) -> None:
log("same sink") # the injected capability
rc.log("same sink") # routed to that same instance
Status Tracking¶
rc.events.set_run_status(status, message="")¶
Transition the execution status. Terminal states cannot be transitioned from.
from functualize.types import RunStatus
rc.events.set_run_status(RunStatus.SUCCESS, "All records processed")
Job Phases¶
rc.events.track_phase(phase_name, message, status=None)¶
Create or update a named job phase. Delegates to the WorkflowTracker capability class.
from functualize.types import RunStatus
rc.events.track_phase("extract", "Fetching from API")
rc.events.track_phase("extract", "Got 1000 records", RunStatus.SUCCESS)
Job Invocation¶
rc.invoke(job_name, *, timeout=None, **kwargs)¶
Invoke a sibling job as a child execution. Delegates to the Invoke capability class.
result = rc.invoke("validate-data", source="api")
result = rc.invoke("slow-job", timeout=30.0, batch_size=100)
Returns: JobResult with status, duration, return_value, and exception.
rc.invoke_parallel(jobs)¶
Invoke multiple jobs concurrently. Delegates to Invoke.parallel().
jobs = [
("process-shard", {"shard_id": 0}),
("process-shard", {"shard_id": 1}),
("process-shard", {"shard_id": 2}),
]
results = rc.invoke_parallel(jobs)
Returns: list[JobResult] in the same positional order as the input.
Event Emission¶
rc.events.emit(event_name, resource="", **payload)¶
Emit a custom structured event. Delegates directly to EventBus.emit().
Prompting¶
rc.prompts.ask(request)¶
Present a structured prompt to the user via the active PromptCollector.
from functualize.plugin import PromptRequest
response = rc.prompts.ask(PromptRequest(
question="Select environment",
choices=[...],
))
Convenience Methods¶
rc.prompts.confirm(question, *, destructive=False, default=None)— Yes/no confirmationrc.prompts.choice(question, choices, *, default=None)— Single selectionrc.prompts.text(question, *, default=None, secret=False)— Text input
Performance Instrumentation¶
rc.events.perf_mark(name) / rc.events.perf_mark_start(name) / rc.events.perf_mark_end(name)¶
Record performance marks. Delegates to the Perf capability class.
DI Access¶
rc[SomeType] / SomeType in rc¶
Access dependency-injected services via subscript notation.
Shell Capability — Errors and Responders¶
The Shell capability (sh: Shell) and its ShellResult/Responder types are covered in the Shell Capability guide. Two of its vocabulary types are documented here.
ShellError¶
Raised when a command exits non-zero under check=True — also on timeout, and when a FailingResponder sentinel appears in the live output.
| Attribute | Type | Description |
|---|---|---|
result |
ShellResult |
The full result of the failed command, so callers can inspect stdout/stderr/returncode. |
from functualize.job import Shell, ShellError
def deploy(sh: Shell) -> None:
try:
sh(["docker", "build", "-t", "app", "."])
except ShellError as e:
print(e.result.stderr)
FailingResponder¶
A Responder that also aborts on a failure sentinel. Responds like Responder — writing response to the child's stdin whenever pattern matches new output — but if sentinel appears the command is killed and a ShellError is raised. This is how a sudo password responder aborts on Sorry, try again instead of answering the re-prompt forever.
from functualize.job import FailingResponder
FailingResponder(
pattern=r"\[sudo\] password.*:",
response="s3cr3t\n",
sentinel="Sorry, try again",
)
| Attribute | Type | Description |
|---|---|---|
pattern |
str \| re.Pattern[str] |
Regex whose match in live output triggers response (inherited from Responder). |
response |
str |
Text written to the child's stdin on each match (inherited from Responder). |
sentinel |
str \| re.Pattern[str] |
Regex whose appearance aborts the command with ShellError. |
Subclasses Responder, so it is accepted anywhere a watchers= sequence is.
Freshness Capability¶
FreshnessVerdict¶
The verdict functualize.job.Freshness exposes to a job whose @job(cache=Fingerprint(..., decides=True)) opted in to seeing its own freshness decision, instead of the engine silently returning before the body ran.
@dataclass(frozen=True)
class FreshnessVerdict:
state: GuardState
key: str
recorded_value: Any | None
declared_sources: tuple[str, ...]
declared_generates: tuple[str, ...]
source_map: Mapping[str, Mapping[str, Any]]
| Attribute | Type | Description |
|---|---|---|
state |
GuardState |
The pipeline outcome — SKIP_FRESH when the job's declared inputs are current, RUN otherwise. |
key |
str |
The fingerprint key the decision was computed under. |
recorded_value |
Any \| None |
What the previous run recorded, when the pre-flight read a record and the verdict was a skip. |
declared_sources |
tuple[str, ...] |
The input patterns as the job's own Fingerprint declared them. |
declared_generates |
tuple[str, ...] |
The output patterns as the job's own Fingerprint declared them. |
source_map |
Mapping[str, Mapping[str, Any]] |
{path: {mtime, size, sha256}} for every match of the declared inputs. |
is_fresh (property) |
bool |
True only when state is GuardState.SKIP_FRESH. Deliberately does not fold in a satisfied status guard — "already done" is a different claim from "declared inputs are current". |
from functualize.job import Freshness
from functualize.job.decorators import job, Fingerprint
@job(cache=Fingerprint(sources=["src/**/*.py"], decides=True))
def build(fresh: Freshness) -> str:
verdict = fresh.verdict()
if verdict is not None and verdict.is_fresh:
return "artifact already current"
return rebuild()
decides=True is what makes the branch above reachable — without it the engine skips the job for you and returns before the body runs, so is_fresh is the one value that branch could never see.