API reference¶
This reference is generated from the source docstrings. The most useful names
are re-exported from the top-level shinobi package and documented here;
supporting types live in their home modules below.
Top-level package¶
- class shinobi.Cab(*, name, info=None, inputs_model, outputs_model, backend=None, image=None, venv=None, input_mutability=<factory>, field_meta=<factory>, cache=None, cache_dir=None, sandbox=None, harvest=<factory>, scratch=<factory>, resources=None, command, flavour='binary', policies=<factory>, input_patterns=<factory>, output_patterns=<factory>, wranglers=<factory>)[source]¶
Bases:
ScopeAn atomic step backed by a single command.
- Parameters:
name (str)
info (str | None)
backend (str | None)
image (str | None)
venv (str | None)
input_mutability (dict[str, Mutability])
cache (bool | None)
cache_dir (str | None)
sandbox (bool | None)
resources (Resources | None)
command (str)
flavour (str)
policies (Policies)
input_patterns (list[ParamPattern])
output_patterns (list[ParamPattern])
- input_patterns: list[ParamPattern]¶
- output_patterns: list[ParamPattern]¶
- model_config = {}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class shinobi.ExecContext(scope, raw_inputs, *, backend_override=None, recipe_backend=None, config=None, cache_enabled=False, cache_dir='', cache_path='', stream=True, pin=False, sandbox_root=None, clear_outputs=True, input_keys=None, budget=None, run_id='')[source]¶
Bases:
objectLive execution state, created by _dispatch. inputs is a validated snapshot for inspection; the raw caller kwargs are kept separately because MUTABLE fields must reach the backend as the caller’s original objects.
- Parameters:
- prepare_inputs()[source]¶
Validated + mutability-processed inputs, with no overrides applied – for a plain-function step’s own function to call the underlying function with (see steps/pyfunc.py’s adapter, and the manual bare-Scope pattern documented on Scope/StepRef). Reuses the already-validated self.inputs snapshot rather than re-validating.
- resolve_backend_name(override=None)[source]¶
Resolve the effective backend name using the standard priority chain. Exposed so orchestration functions (e.g. the pystep adapter) can inspect which backend is active without duplicating the precedence logic.
- import_func(func, module=None)[source]¶
Import and return a callable by name.
If module is None, looks up func in builtins (e.g.
print,len). Otherwise imports module and returns getattr(module, func).Useful for pysteps that invoke container-only functions (e.g. CASA tasks) without triggering linter warnings about missing imports on the host.
- run(*, backend=None, **overrides)[source]¶
Run the underlying Cab or Recipe with optional input overrides.
- Parameters:
- Returns:
The step’s StepResult. Also stored on self.outputs.
- Raises:
TypeError – If self.scope is neither a Cab nor a Recipe (a plain-function step must return its result directly instead of calling ctx.run()).
- Return type:
- class shinobi.InputRef(*, field)[source]¶
Bases:
BaseModelWiring source: this sub-step’s input comes from the enclosing Recipe’s own input field field.
- Parameters:
field (str)
- model_config = {}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class shinobi.LoopIteration(*, loop, index, prev_step=None, sentinel_step=None, sentinel_field)[source]¶
Bases:
BaseModelMarks a step as belonging to iteration index of the unrolled loop loop (see Recipe.add_loop). Carried by every step the unrolling produces; it is bookkeeping for the skip decision only – the dependency edges that make the unrolled chain a real DAG are ordinary wiring plus after, never this.
prev_step is the same body step one iteration earlier (so selfcal.4.image passes through selfcal.3.image), and sentinel_step/sentinel_field name the previous iteration’s output whose existence on disk means “the loop has already converged, do no work”. prev_step and sentinel_step are both None for the first iteration, which can never skip – there is nothing before it to pass through or to have converged. sentinel_field is always set: which output carries the signal is a property of the loop, not of one iteration.
- Parameters:
- model_config = {}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class shinobi.LoopRef(name, steps, final)[source]¶
Bases:
objectWhat Recipe.add_loop returns: the loop’s step names, and an outputs proxy resolving to the final iteration’s producers.
Not a StepRef – a loop is not a step. It has already been unrolled into the recipe’s steps by the time this is returned; this is just a handle for wiring whatever comes next.
- property outputs: _LoopOutputsProxy¶
Wiring proxy (definition layer) – NOT runtime values.
- class shinobi.Mutability(*values)[source]¶
-
Whether a step’s input may be changed in place by the step’s own orchestration function without that change propagating back to the caller’s object.
- IMMUTABLE = 'immutable'¶
- MUTABLE = 'mutable'¶
- class shinobi.OutputRef(*, step, field)[source]¶
Bases:
BaseModelWiring source: this input (or, in Recipe.output_wiring, the recipe’s own output) comes from step step’s output field field.
- model_config = {}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class shinobi.Recipe(*, name, info=None, inputs_model, outputs_model, backend=None, image=None, venv=None, input_mutability=<factory>, field_meta=<factory>, cache=None, cache_dir=None, sandbox=None, harvest=<factory>, scratch=<factory>, resources=None, steps=<factory>, output_wiring=<factory>, max_workers=None)[source]¶
Bases:
ScopeA composite step: declared sub-steps with explicit wiring.
The one deliberately mutable Scope: builder methods (add_step, step, set_output) extend steps/output_wiring before first run.
- Parameters:
- property inputs: _InputsProxy¶
Wiring proxy (definition layer) – NOT runtime values.
- property outputs: _OutputsProxy¶
Wiring proxy (definition layer) – NOT runtime values.
- add_step(name, scope, *, scatter=None, resources=None, **kwargs)[source]¶
Add a step. scope is usually a bare Scope/Cab/Recipe, but can also be an already-built StepRef (e.g. from @shinobi.pystep or @shinobi.step) – its func is carried over so the step keeps its orchestration function, not just its schema.
- Parameters:
scatter (list[str] | ScatterSpec | None) – Fields to fan out over. Each must be a field of the step’s inputs_model; at runtime the corresponding value must be a list, and every scattered field must have the same length.
resources (Resources | None) – What this step costs to run, for the scheduler’s admission control. An explicit keyword rather than a kwarg, since **kwargs here is split into wiring and step params and would otherwise swallow it as a constant input value.
name (str)
kwargs (Any)
- Return type:
- add_loop(name, body, *, max_iter, until, carry, index_input=None, **kwargs)[source]¶
Unroll body max_iter times into this recipe, as a declared chain.
The loop is bounded unrolling with short-circuit pass-through: the body’s steps are flattened into this recipe once per iteration and chained by real carry wiring, so the whole thing is an ordinary statically-inspectable DAG – –dryrun renders every iteration and the offload compiler emits a plain dependency chain. The only runtime decision is whether an already-declared step does any work: once an iteration writes the until sentinel file, every later step passes the corresponding previous iteration’s outputs straight through.
A Recipe body is flattened (selfcal.3.image); any other Scope is one step per iteration (selfcal.3).
- Parameters:
name (str) – The loop’s name; every step it creates is prefixed with it.
body (Scope | StepRef) – The loop body. Its outputs must be re-consumable as its inputs (see carry) – a loop body is a fixed point.
max_iter (int) – How many times to unroll. The upper bound on iterations, not the exact count.
until (str) – Name of a path-typed output of body. Once that file exists, the loop has converged. A path (rather than a bool) is what lets the identical predicate serve both local execution and an offloaded sbatch script.
carry (dict[str, str]) – Body output field -> body input field, the loop-carried dependency. Required and explicit: these pairs are the edges between iterations, and inferring them from matching names would make the graph’s shape depend on name coincidence.
index_input (str | None) – Name of a body input to bind to the 1-based iteration number. Without it every iteration resolves to identical values, so a body cannot name its outputs per cycle (a per-cycle image prefix, say) – which real self-calibration does. Steps consuming it must tolerate a changing value: it is the one input that deliberately differs between iterations.
**kwargs (Any) – Iteration 1’s inputs, split into wiring and constants exactly as add_step does. Constants apply to every iteration.
- Returns:
A LoopRef whose outputs proxy resolves to the final iteration.
- Return type:
- step(scope, *, backend=None, scatter=None, resources=None, **kwargs)[source]¶
Decorate a function as a new step appended to this recipe.
- Parameters:
scope (Scope) – The Cab, Recipe, or bare Scope to bind as this step.
backend (str | None) – Backend override for this step.
scatter (list[str] | ScatterSpec | None) – Fields to fan out over (see add_step).
resources (Resources | None) – What this step costs to run (see add_step).
**kwargs (Any) – Split into wiring (InputRef/OutputRef values) and per-step constant params via _split_kwargs.
- Returns:
A decorator that binds the given function, appends the resulting StepRef to self.steps, and returns it.
- model_config = {}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class shinobi.ScatterSpec(*, fields)[source]¶
Bases:
BaseModelDeclaration that a step should fan out over one or more list-typed input fields. Each listed field must be a list at runtime and all listed fields must have the same length. The step is executed once per index, with slice i receiving element i of every scattered field.
The step’s own inputs_model/outputs_model describe one slice. A downstream step sees the scattered step’s outputs gathered into lists (one element per slice), so it can scatter over them in turn or consume the whole list as a gathered result.
- model_config = {}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class shinobi.Scope(*, name, info=None, inputs_model, outputs_model, backend=None, image=None, venv=None, input_mutability=<factory>, field_meta=<factory>, cache=None, cache_dir=None, sandbox=None, harvest=<factory>, scratch=<factory>, resources=None)[source]¶
Bases:
BaseModelDefinition: schema, metadata, backend config. Never carries inputs/outputs/func fields – those live in ExecContext/StepRef.
Cab/Recipe are the two execution-aware subclasses ExecContext.run() knows how to run. A bare Scope is also valid – it’s the manual building block for a plain-Python-function step whose own function returns its StepResult directly rather than calling ctx.run(); see StepRef’s docstring and steps/pyfunc.py’s @shinobi.pystep (which automates this pattern from a function’s own signature).
image is optional: when set on a bare Scope (typically via @shinobi.pystep(image=…)), the step’s Python function can be executed inside a container instead of in-process. Cab inherits this field for the same purpose (container backends need it to wrap argv in a runtime invocation).
- Parameters:
- input_mutability: dict[str, Mutability]¶
- mutability_of(field)[source]¶
Look up the declared mutability of an input field.
- Parameters:
field (str) – Name of the input field.
- Returns:
The field’s Mutability, defaulting to Mutability.IMMUTABLE if not explicitly declared.
- Return type:
- with_backend(backend)[source]¶
A copy bound to backend, or self unchanged if backend is None. Shared by @shinobi.step and Recipe.step, which both bind a per-step backend override onto a Scope before wrapping it in a StepRef.
- with_resources(resources)[source]¶
A copy bound to resources, or self unchanged if None. Same shape as with_backend, and used the same way: Recipe.add_step, Recipe.step and @shinobi.step bind a per-step footprint onto a Scope before wrapping it in a StepRef, so one shared Cab can be declared cheap in one recipe position and expensive in another without either mutating the Cab itself.
- Parameters:
resources (Resources | None)
- Return type:
- model_config = {}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class shinobi.StepRef(*, name, step, func=None, wiring=<factory>, params=<factory>, scatter=None, after=<factory>, loop=None)[source]¶
Bases:
BaseModelA named, executable binding of a Scope: orchestration function, wiring (meaningful only inside a Recipe), and per-step constants. Returned by @shinobi.step (free-standing) and @recipe.step (appended to recipe.steps). arbitrary_types_allowed is needed only for func.
step is typed as the general Scope (not Cab | Recipe) so it can also hold a bare Scope – the manual, no-magic way to write a plain-Python-function step: build Scope(name=, inputs_model=, outputs_model=) yourself, write a function that always returns its own StepResult (never calls ctx.run(), which only knows how to execute a Cab or Recipe), and wrap it in a StepRef directly. @shinobi.pystep (steps/pyfunc.py) automates exactly this pattern by deriving the Scope’s schema from the function’s own signature. Passing a Cab/Recipe instance here is unaffected – pydantic’s default revalidate_instances=”never” keeps an already-constructed instance’s real subtype, it does not downcast to bare Scope.
- Parameters:
- model_config = {'arbitrary_types_allowed': True}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- scatter: ScatterSpec | None¶
- loop: LoopIteration | None¶
- shinobi.pystep(*, name=None, info=None, image=None, venv=None, backend=None, sandbox=None, harvest=None, write_paths=None, **params)[source]¶
Decorate (or directly call on an existing function, matching @shinobi.step’s precedent: pystep()(existing_func)) a plain, type-hinted function to turn it into a StepRef. See the module docstring for the schema-derivation and outputs rules.
image enables container execution: when set and a container backend is resolved, the function runs inside the specified container image instead of in-process. The function’s source module is mounted into the container so it can be imported by the runner script.
venv enables virtualenv execution: when set (a path or a key into backend.venv.envs) and the venv backend is resolved, the function runs under that venv’s own interpreter, importing the venv’s real packages (unlike the container path, the target’s own package is not stubbed). A scope may carry both image and venv; the resolved backend name decides which is used.
backend sets the default backend for this step (same as on any Scope). With image, this is typically a container backend name like
"docker"or"apptainer"; with venv,"venv".sandbox/harvest opt this step into sandboxed execution and declare extra keep-globs (see shinobi.sandbox and the fields on Scope). Sandboxing applies to the out-of-process paths (container and venv) – an in-process run ignores it (os.chdir is process-global, and recipes run steps on a thread pool), executing in the caller’s cwd as always.
write_paths names the parameters that are destinations this function creates rather than data it reads (ParamMeta.write_path) – the one thing no signature can express, since outputvis: Path returned as the output and vis: Path rewritten in place are the same annotation. Only a named parameter has its stale product cleared before a re-run (sandbox.clear_stale_outputs); everything else is treated as the caller’s data and left alone.
**params are per-call constants, same as @shinobi.step.
Schema helpers¶
Supporting types used when defining cabs, not re-exported at the top level.
- class shinobi.steps.schema.ParamMeta(*, nom_de_guerre=None, implicit=None, info=None, positional=False, positional_head=False, repeat_as_tokens=False, dtype=None, choices=None, abbreviation=None, write_path=False, writable=None)[source]¶
Bases:
BaseModelPer-field metadata a plain pydantic model can’t express: the name the underlying tool actually expects (nom_de_guerre), a value always supplied by the cab itself rather than the caller (implicit), human-facing help (info), the cab dtype string (dtype, e.g. “File”/”MS”) for a ParamPattern attr – since a dynamically-named param has no declared field/type annotation for path_fields to inspect, this is how backends know to bind-mount its directory – positional: emitted as a bare value (no –flag), in field-declaration order, after every flagged/pattern-matched arg. positional_head: the same, but emitted before every flagged/ pattern-matched arg instead of after – real cult-cargo’s own cubical.yml names this exact policy (parset: {policies: {positional_head: true}}) for a tool whose own CLI only recognises a leading bare token as a parset file to seed defaults from (CubiCal’s main.py: if len(sys.argv) > 1 and not sys.argv[1][0].startswith(‘-‘): custom_parset_file = sys.argv[1]); killMS’s kMS.py has the identical sys.argv[1]-only check. A plain positional field there would always land after every –flag, which these two tools’ own argv[1]-anchored parset detection can’t see – either raising (“Unexpected number of arguments”, CubiCal) or silently not reading the parset at all (killMS, which never validates leftover-arg count). Setting both positional and positional_head on the same field is nonsensical; positional_head wins if both are set. Head positionals, like tail ones, are emitted in field-declaration order. – and repeat_as_tokens: a list/tuple value is emitted as separate bare argv tokens (after the one flag occurrence, or as separate positional tokens) instead of joined into one comma-separated token – real cult-cargo cabs express this as a per-field policies: {repeat: list} (see e.g. wsclean’s -size <w> <h>/-weight briggs <n>, which need two separate argv tokens, not “4096,4096” as one).
write_path: this string-typed input names a filesystem path the tool writes to. Two shapes, both covered: a stem products are built from (wsclean’s prefix, ddfacet’s Output-Name, which never exists as a file itself), and a complete path written directly (a cache directory, a logfile). It is a declaration of intent only, and deliberately changes nothing about how the value is handled. In particular it does not make the field a path: a path dtype here would be rewritten by sandbox.absolutize_path_inputs and the tool would then write outside the sandbox, which is precisely why the convention types these as strings (see declared_output_dirs). Mounting and pre-creation still come from the output side – a path-typed output whose implicit template names this field, a harvest glob, or a scratch glob.
What it buys is enforcement. That convention previously existed only as prose and authorial discipline: a write path nobody declared a target for produces no error, just products that stay inside the container or outside the harvest. Marking the field lets Cab check that something does declare it (see Cab._write_path_declares_a_write_target).
A path-typed input may be marked too, and there it is the only thing that distinguishes the two identical-looking spellings of an output field that echoes a same-named input.
mstransformtakesoutputvisand creates it;flagdatatakesvisand rewrites the caller’s own data; both declare that one name on inputs_model and on outputs_model, so mutated_path_fields – and any other structural test – sees one shape. Markingoutputvissays which it is, and sandbox.clear_stale_outputs then clears the stale product before a re-run instead of leaving the tool to trip over it. The default (unmarked) is the safe reading: an echoed path is the caller’s data and is never deleted. Note the marker still changes nothing about how the value is handled – a path-typed one is anchored at the workspace by sandbox.absolutize_path_inputs exactly as before, which is what a complete destination path (as opposed to a stem) wants.writable: this path-typed input is one the tool must not modify (writable: false, cult-cargo/scabha’s own key). The container backends bind-mount the directories a read-only input contributes :ro, and re-assert the input itself :ro at its own path when something writable shares that directory (backends.container.bind_dir_modes). None – the default, and every Python-typed pystep input – means unmarked, which is read as writable. Carried onto a declared field’s json_schema_extra by the loaders (readonly_path_fields reads it from there, the same channel abbreviation rides); kept here as well because a ParamPattern attr has no declared model field for readonly_path_fields to inspect, and a dynamically-named input is exactly the case that wants the marker – QuartiCal’s <term>.load_from names a previous run’s gain store, which the step reads and must not write back into.
choices: the field’s allowed values (cult-cargo/classic’s choices key). A loader that sets this also narrows the field’s real annotation on inputs_model/outputs_model to typing.Literal[*choices] (see loaders._modelgen.narrow_choices), so an out-of-set value fails pydantic validation the same way a wrong dtype would – not merely documented in info. Kept here too (rather than only inferred from the model’s own annotation) so a ParamPattern attr – which has no declared model field for a dynamically-matched name – can still carry it.
abbreviation: a short single-dash CLI alias for the field’s generated –long-flag (cult-cargo/classic’s abbreviation key, e.g. simms’ ascii-sky -> -as). Purely a ninja run convenience – carried onto the field’s json_schema_extra by the loaders so clickutil.build_options can emit the alias; it never affects the argv the tool actually receives (that still uses nom_de_guerre).
On an output field, a string implicit containing {name} placeholders is resolved by steps.dispatch._fill_outputs as a str.format template against the step’s prepared (validated) input values – e.g. implicit=”{prefix}-MFS-image.fits” derives a tool’s output path from its own prefix input, without shinobi ever importing/executing the tool’s own schema-generation code. A plain string with no {…} is used as a literal constant, same as on an input field.
- Parameters:
- implicit: Any¶
- model_config = {}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class shinobi.steps.schema.ParamPattern(*, separator='.', segments)[source]¶
Bases:
BaseModelA family of inputs whose names are <segment><separator><segment>…, e.g. QuartiCal’s K.type/G.time_interval or cubical’s g1-solvable/ g-time-int. Matched as one anchored regex assembled from segments: exactly one segment is attrs (the known, enumerable part, each value with its own ParamMeta – dtype/nom_de_guerre/info); every other segment is a regex (soft shape-validation of a level that can’t be enumerated ahead of time). See AGENTS.md for the motivating tools.
The attrs segment is usually last (cubical/QuartiCal: an unenumerable term name followed by a known attribute, g1.solvable), but doesn’t have to be – wsclean’s dynamic output names are the opposite shape, a known/enumerable image type followed by an open-ended qualifier tail (dirty.per-band, restored.i.per-interval.mfs), so attrs there is the first segment. Only one segment may carry attrs; the rest must all be regex.
A segment regex that should behave as an unconstrained “match anything” level (the old design’s prefix) should be written lazily (.+?, not .+): with more than one registered attr, an eager .+ prefers the shortest attr that completes an overall match, which is wrong when one attr is itself a suffix of another (e.g. “int” vs “time-int” with separator “-”) – .+? tries the shortest prefix first, which is exactly “prefer the longest/most specific attr”.
- Parameters:
separator (str)
segments (list[ParamSegment])
- segments: list[ParamSegment]¶
- model_config = {}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- model_post_init(context, /)¶
This function is meant to behave like a BaseModel method to initialize private attributes.
It takes context as an argument since that’s what pydantic-core passes when calling it.
- Parameters:
self (BaseModel) – The BaseModel instance.
context (Any) – The context.
- Return type:
None
- class shinobi.steps.schema.ParamSegment(*, regex=None, attrs=None)[source]¶
Bases:
BaseModelOne level of a dotted/dashed dynamic-parameter name. A “shape” segment carries only regex – soft validation, no metadata, for a level whose actual values can’t be enumerated at cab-authoring time (e.g. a solver term name like QuartiCal’s K/G). The “meta” segment – always the last one in a ParamPattern – carries attrs: the known, enumerable part, each value with its own ParamMeta.
- model_config = {}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class shinobi.steps.schema.Policies(*, prefix='--', replace=<factory>, list_sep=', ', repeat_list=False, key_value=False, repeat=None, explicit_true=False, explicit_false=False, true_token='true', false_token='false')[source]¶
Bases:
BaseModelHow a cab’s parameters are turned into command-line arguments.
key_value/repeat mirror real cult-cargo cab-level policy keys verbatim (e.g. QuartiCal’s policies: {key_value: true, repeat: ‘[]’, prefix: ‘’}): key_value=True means a hydra-style single name=value argv token instead of two tokens (–name, value); repeat=”[]” means a list value formats as one bracketed-literal token (solver.terms=[K,G]) instead of list_sep-joining. Distinct from a per-field ParamMeta.repeat_as_tokens (real per-field policies: {repeat: list}, e.g. wsclean’s bare -size 4096 4096), which is a field-level override and takes precedence when set.
explicit_true/explicit_false also mirror real cult-cargo cab-level policy keys verbatim (e.g. CubiCal’s policies: {explicit_true: true, explicit_false: false}): by default a True boolean value emits as a bare flag (–flag, argparse store_true-style) and False is omitted entirely. Some real CLIs (CubiCal’s own optparse-derived parser among them) instead expect every boolean option to always take an explicit value token – passing a bare flag with no value corrupts parsing of everything after it, since the parser consumes the next token as that flag’s value. explicit_true=True emits –flag true (two tokens, “true”/”false” lowercase) instead of a bare flag when the value is True; explicit_false=True does the same instead of omitting the flag when the value is False. Each direction is independent (CubiCal only needs explicit_true, never explicit_false), and this applies uniformly to declared fields and ParamPattern-matched dynamic ones (e.g. CubiCal’s own per-Jones-term g-solvable).
true_token/false_token are what those two tokens actually say. Lowercase “true”/”false” (the default) is what CubiCal’s parser reads, but it is not universal: DDFacet and killMS share a parset reader (DDFacet.Parset.ReadCFG) that parses “0”/”1” as ints and “True”/”False” as bools, and leaves an unrecognised “false” as a string – which, being non-empty, is truthy. –Mask-Auto false there switches the mask on. So the pair of tokens is a cab-level policy of its own (policies: {explicit_true: true, explicit_false: true, true_token: ‘1’, false_token: ‘0’} for those two), alongside the explicit_* switches rather than instead of them: explicit_* decides whether a value token is emitted, these decide what it reads.
They spell a boolean in every value position, not only after a flag: a key_value cab’s single name=value token, a positional, an element of a joined or bracketed list, one occurrence of a repeated flag. Python’s own str(True) is “True”, so a path that missed this emitted a spelling no policy ever asked for (policies._scalar_token is the one place that decides).
- Parameters:
- model_config = {}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class shinobi.steps.schema.ScatterSpec(*, fields)[source]¶
Bases:
BaseModelDeclaration that a step should fan out over one or more list-typed input fields. Each listed field must be a list at runtime and all listed fields must have the same length. The step is executed once per index, with slice i receiving element i of every scattered field.
The step’s own inputs_model/outputs_model describe one slice. A downstream step sees the scattered step’s outputs gathered into lists (one element per slice), so it can scatter over them in turn or consume the whole list as a gathered result.
- model_config = {}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- shinobi.steps.schema.path_fields(model)[source]¶
Names of every field of model whose (Optional/list-unwrapped) type is a filesystem path (
pathlib.Path). File-like cab dtypes (File/MS/Directory/URI) map to Path, so this drives both container bind-mounting and the CLI’sclick.Path()mapping.
- shinobi.steps.schema.declared_output_dirs(scope, prepared)[source]¶
(directory, source)for every directory the step is declared to write into, resolved before the run from the declarations alone – never from the shape of an input’s value.This is the answer to “where does this tool put its products”, which a field’s dtype cannot express: the established way to declare an output stem is a string-typed input (wsclean’s
prefix, ddfacet’sOutput-Name), deliberately so, because a path dtype would be rewritten by sandbox.absolutize_path_inputs and the tool would then write outside the sandbox. Such an input can say so withParamMeta.write_path, which changes nothing here – this function reads the declarations either way – but lets Cab reject a stem that no declaration names, which is the authoring mistake this convention is otherwise silent about. The write target is instead declared by the output side: a path-typed output field whoseimplicittemplate names the input ("{prefix}-MFS-image.fits"), aharvestglob, or ascratchglob for a write target that is not a product.Knowable pre-run means the parent of each declared_output_paths entry (per path-typed output field, its value taken from a same-named input, its resolved
implicittemplate, or its field default – the same priority as _fill_outputs), then the literal (glob-free) directory prefix of eachharvestandscratchpattern. source describes the declaration (output 'restored_image',harvest pattern '{prefix}-*.fits',scratch pattern '{cache_dir}/*') so callers can name it in an error.A pattern that references a field whose value is None is skipped, not resolved: substituting it would produce a literal
Nonepath segment ("{cache_dir}/*"->"None/*"), which is a real directory name and would have the sandbox create one. An unset optional write target declares nothing, which is the same answer a path-typed output whose value is None already gives.Both relative and absolute directories are returned, order-preserving and de-duplicated;
.and anything containing..is dropped. The two consumers filter complementary halves: sandbox pre-creates the relative ones inside the scratch dir, the container backend bind-mounts the absolute ones so a product declared outside the workdir still reaches the host. Both consumers treat ascratchdirectory exactly like a product’s: it is mounted so the tool’s write lands on the host, and pre-created so the tool doesn’t have to. Only harvest differs – it never rescues ascratchpath out of a sandbox, which is the whole point of the distinction. Best-effort by design – a template that fails to resolve is skipped here, not raised; output filling and harvest report those errors with full context.
Execution¶
- class shinobi.results.BackendRun(returncode, stdout='', stderr='', image_digest=None, containerized=False, venv=None, venv_digest=None, stdout_dropped=0, stderr_dropped=0, wrangler_lines_dropped=False)[source]¶
Bases:
objectWhat a backend returns after running a command – just the raw run outcome. Wrangling stdout/stderr into structured outputs and filling an outputs_model is the dispatch layer’s job, not the backend’s.
- Parameters:
- class shinobi.results.StepResult(name, returncode, outputs, inputs, stdout='', stderr='', cached=False, skipped=False, kind='cab', backend=None, image=None, image_digest=None, containerized=False, venv=None, venv_digest=None, sandboxed=False, resources=None, sub_results=None, cache_key=None, output_keys=None)[source]¶
Bases:
objectThe outcome of running a step (a Cab or a Recipe).
outputs is a validated instance of the step’s outputs_model; inputs is a validated instance of the effective (post-override) inputs the step actually ran with. For a Recipe these aggregate from its sub-steps.
- Parameters:
name (str)
returncode (int)
outputs (BaseModel)
inputs (BaseModel)
stdout (str)
stderr (str)
cached (bool)
skipped (bool)
kind (str)
backend (str | None)
image (str | None)
image_digest (str | None)
containerized (bool)
venv (str | None)
venv_digest (str | None)
sandboxed (bool)
resources (Resources | None)
sub_results (dict[str, StepResult] | None)
cache_key (str | None)
- outputs: BaseModel¶
- inputs: BaseModel¶
- sub_results: dict[str, StepResult] | None = None¶
- provenance_key(field)[source]¶
The cache key identifying whatever produced output field.
A leaf step produces all its outputs in one run, so every field resolves to that step’s own cache_key. A Recipe fans out to output_keys instead – each declared output comes from a distinct sub-step, and invalidating a downstream consumer because some unrelated sub-step re-ran would throw away most of the cache’s value.
None means “no provenance available” (caching disabled, or a step that isn’t cacheable) – callers must treat that as “contribute nothing”, not as a key in its own right.
- shinobi.steps.dispatch.register_step_backend(name, backend)[source]¶
Register a backend instance under name, overriding the real class-based registry. Mainly for tests.
- shinobi.steps.dispatch.get_step_backend(name)[source]¶
Resolve a backend instance by name, checking test overrides first.
Short-circuit semantics for an unrolled loop (see Recipe.add_loop).
A loop is declared, not interpreted: add_loop flattens its body into the recipe max_iter times and chains the copies with real wiring, so the graph is fully inspectable before anything runs. What stays a run-time decision is narrow – whether an already-declared step does any work. This module is that decision, and nothing else.
The rule lives here rather than in the scheduler because two tiers evaluate it: _run_recipe calls should_skip in-process, and the Slurm offload compiler emits the same predicate as a shell [ -f … ] test at the top of each iteration’s script. A convergence signal that is a path is what makes that possible – a bool would have no way to cross a node boundary, and the two tiers would need separate definitions that could drift into running a different number of cycles for the same recipe.
- shinobi.steps.loops.sentinel_exists(value)[source]¶
Whether a convergence sentinel has actually been produced.
- shinobi.steps.loops.sentinel_value(ref, results)[source]¶
The sentinel value this step’s skip decision reads, or None if it cannot skip (the first iteration, or a step outside any loop).
- Parameters:
ref (StepRef) – The step about to be scheduled.
results (dict[str, StepResult]) – Completed steps by name. The sentinel producer is guaranteed to be present: add_loop gives every iteration an edge to it, so the scheduler cannot reach this step first.
- Returns:
The previous iteration’s sentinel output value, or None.
- Return type:
- shinobi.steps.loops.should_skip(ref, results)[source]¶
Whether ref should pass its predecessor’s outputs through instead of running, because an earlier iteration already converged.
- Parameters:
ref (StepRef) – The step about to be scheduled.
results (dict[str, StepResult]) – Completed steps by name.
- Returns:
True if the previous iteration’s sentinel exists on disk.
- Return type:
- shinobi.steps.loops.passthrough_result(ref, prev, inputs)[source]¶
The result of a step that was skipped: the same body step’s outputs from one iteration earlier, handed on unchanged.
Passing the previous outputs object through (rather than re-deriving field by field) is what makes convergence sticky without extra bookkeeping: the sentinel is itself one of those outputs, so every later iteration sees it and skips in turn.
cache_key/output_keys are carried over too. A skipped step produced no new data, so contributing nothing would drop the upstream term from every downstream cache key (see shinobi.cache) and needlessly invalidate work that is genuinely unchanged.
kind deliberately keeps the scope’s real kind – skipped is a separate flag. shinobi.provenance.apply_manifest_pins asserts that a record’s kind still matches the scope’s type, so inventing a “skipped” kind would make any early-converging run unreplayable.
- Parameters:
ref (StepRef) – The skipped step.
prev (StepResult) – The corresponding step’s result from the previous iteration.
inputs (Any) – The validated inputs this step would have run with.
- Returns:
A successful StepResult marked skipped.
- Return type:
Backends¶
Backend abstraction: a backend takes a cab and a resolved argv and runs it somewhere – natively, in a container, on Slurm, on Kubernetes, …
A backend knows nothing about recipes or output schemas beyond the argv
it’s handed and the cab’s image/command metadata; it only knows
how to execute and how to capture output. Wrangling that output into
structured results is the dispatch layer’s job, so a backend returns a
raw BackendRun (returncode/stdout/stderr), nothing schema-aware.
- class shinobi.backends.Backend[source]¶
Bases:
ABCAbstract base class for execution backends.
- Variables:
name (str) – Registry key used to look up this backend (e.g.
"native","container","slurm").
- abstractmethod run(cab, argv, inputs, *, label='', stream=True, pin=False, cwd=None)[source]¶
Execute argv (as built by shinobi.policies.build_argv) and return a BackendRun. Must not raise on a non-zero exit – that’s reported via BackendRun.returncode / BackendRun.success.
inputsis the prepared inputs dict argv was built from (the one _prepare_inputs produces, so MUTABLE fields are the caller’s own objects by reference). Most backends ignore it, but container backends need it to know which File/MS-valued params to bind-mount.label/streamcontrol live stdout/stderr echo (see shinobi.backends._stream.run_streaming) – only native and container act on them today; slurm/kubernetes accept and ignore both (neither has any log-tailing infrastructure yet, so they keep reading output once after the job/pod finishes).cwdis the working directory to run in (shinobi.sandbox passes the step’s sandbox here), defaulting to the process cwd. slurm/ kubernetes accept and ignore it too: they run in a remote/pod cwd the dispatch layer can’t scope, so a sandboxed step on those backends degrades gracefully to an unsandboxed run (harvest finds the outputs already in the workspace and moves nothing).
- shinobi.backends.register(backend_cls)[source]¶
Register a backend class under its
nameattribute.Intended for use as a class decorator on Backend subclasses.
- shinobi.backends.get_backend(name, **opts)[source]¶
Instantiate a registered backend by name.
- Parameters:
name (str) – Registry key of the backend (e.g.
"native","slurm").**opts – Keyword arguments forwarded to the backend’s constructor.
- Returns:
A new instance of the requested backend.
- Raises:
ValueError – If no backend is registered under
name.- Return type:
- shinobi.backends.registered_backend_names()[source]¶
Every name a backend is registered under, sorted.
The authoritative answer to “is this a real backend?”, for callers that need to check a name rather than instantiate it – the CLI validating –backend before a run starts (shinobi.cli). Not every consumer of a backend name reaches get_backend: the pystep adapter matches the resolved name against CONTAINER_RUNTIMES and otherwise runs the function in-process, so a typo there would go unnoticed rather than raise.
Building argv¶
Turn a cab’s schema + resolved parameter values into a command line.
Operates on the step-model Cab: the parameter values come from an already-validated inputs_model instance (or the prepared dict dispatch builds from it), while per-field naming/implicit metadata comes from the cab’s field_meta, dynamically-named params from input_patterns, and arg formatting from policies.
- shinobi.policies.build_argv(cab, resolved)[source]¶
Build a full argv (starting with the cab’s command) from a resolved parameter dict, according to the cab’s policies and field metadata.
Rejects any non-“binary” flavour before building argv – so a non-executable command can never reach subprocess as argv[0] (see SECURITY.md).
Loaders¶
Load YAML cab definitions in the scabha dialect into shinobi Cab objects.
Lineage. shinobi’s cab schema is borrowed from scabha, the schema library underneath Stimela 2.0, and the vocabulary here is deliberately scabha’s: inputs/outputs with dtype/required/default/info/choices, policies, management.wranglers, image, flavour, command. Reusing it was a design decision, not an accident of history – the cab schema is the part of stimela2 that got it right, and shinobi’s own Cab mirrors it closely enough that loading a scabha cab is a translation rather than an interpretation. What shinobi drops is the layer above the cab: stimela2’s recipe, alias and expression machinery (see stimela-ninja’s AGENTS.md).
cult-cargo is the largest published library of cabs written in this dialect and is what this loader is usually pointed at, but the dialect is scabha’s and nothing here is specific to that project. shinobi.loaders.worker_schema reads a scabha-derived config dialect through the same shared helpers.
shinobi-native keys. shinobi’s own Cab carries a few things scabha has no vocabulary for, so the dialect accepts them as an extension rather than inventing a second format for cabs authored against shinobi directly. A document using none of them is a plain scabha document, and cult-cargo’s own files remain a readable subset.
Per field, alongside the scabha keys: write_path: true marks a
string-typed input naming a filesystem path the tool writes to – a stem
products are built from, or a complete path written directly (see
ParamMeta.write_path) – and mutable: true marks an input the step may
change in place (Mutability.MUTABLE). Both are registered in
_LEAF_SPEC_KEYS, which matters more than it looks: _is_section tells a
leaf param from a nested CLI section by whether the mapping has any known
param-spec key, so a spec carrying only an unregistered key is read as a
section and the field disappears without a word.
Per cab: sandbox, harvest and scratch, which mirror the Scope
fields of the same names.
image: may also name a key rather than a reference, resolved through the
caller-supplied images mapping (see loads) – the same shape as
package_roots, and for the same reason: shinobi has no manifest and does
not go looking for one. It lets a document say image: WSCLEAN and leave
which reference that is to the deployment that loads it.
Also per cab: input_patterns/output_patterns, families of
dynamically-named params (ParamPattern). A pattern is a separator plus
ordered segments; each segment is either a regex (a level that cannot
be enumerated ahead of time) or attrs (the known level, each attr a param
spec in its own right). ParamPattern’s own validator enforces the real
rule – exactly one segment carries attrs – so the loader only produces
the shape and lets it object, naming the cab and key when it does.
An attr spec is read by the same _param_meta as a declared field, with one
asymmetry: an attr keeps its dtype, a field does not. A declared field’s
dtype is already its model annotation, and repeating it on field_meta would
make every field of every cab differ from its Python-authored equivalent; a
pattern attr has no model field at all, which is the reason
ParamMeta.dtype exists.
Support is deliberately partial. This reads the static, declarative subset and refuses the parts that are a programming language wearing YAML. The boundary is drawn once, here and in SECURITY.md, and the sections below say exactly where it falls: composition mechanisms this implements, then the scabha features it does not.
Composition mechanisms, implemented in a deliberately minimal form – real scabha cab files are not self-contained and rely on stimela2’s config system for these:
_include: [file, ...]– merges other YAML files in (relative to the including file), most often to pull in a sharedvars:/lib:namespace. Merging is a plain deep-merge; the including file’s own keys win over included ones. Resolved wherever it appears in the document (top level, or nested underinputs:/outputs:, as real cult-cargo’scubical.yml/quartical.ymldo) via the same tree-walkingresolve_directivehelper_usealready relies on._use: dotted.path– deep-merges a dict looked up by dotted path in the fully-merged document (post-_include) into the dict it appears in, with that dict’s own sibling keys taking precedence. Used both for small things (image: {_use: vars.cult-cargo.images, name: breizorro}) and to inherit a cab’s entire command/flavour block.The package-scoped include form (
_include: (pkg.dotted.path)file.yamlor_include: [{(pkg.dotted.path): [file, ...]}]) – searches an installed package’s data directory rather than a relative path. Resolving a dotted package name to a filesystem directory would normally mean importing the package (importlib), but that risks executing arbitrary code from any__init__.pyon the path – shinobi never imports a cab package for any reason (see SECURITY.md’s “never eval()/exec() a cab’s command” boundary, which this extends to “never import a cab package”). Instead, callers passpackage_roots={"cultcargo": Path(...)}toload_file()/loads(): an explicit, caller-supplied mapping from a dotted package prefix to its filesystem directory. A dotted name is resolved against the longest registered prefix, descending the remainder as subdirectories (cultcargo.genesis.cubicalagainst{"cultcargo": Path("/.../cultcargo")}->Path("/.../cultcargo/genesis/cubical")) – the normal package/subpackage-is-a-subdirectory convention, without ever asking Python’s import machinery to confirm it. A package-scoped_includenaming a package with no registered root raises a clearCabLoadError.
Deliberately NOT implemented (this is the boundary – see SECURITY.md). Each of these is a place where scabha stops describing a tool and starts computing something, which is the line shinobi does not cross in a cab:
Expressions and substitutions. The
=config.x.y/=recipe.ms/${...}/=IFSET(...)language scabha values can contain. Left as literal strings, so a cab carrying one loads with that value verbatim rather than resolved – visible in the built Cab, not silently dropped. ParamMeta.implicit is the one templating shinobi does resolve, and it is plainstr.formatagainst the step’s own validated inputs: no name resolution across steps, no function calls, no conditionals.Conditionals and control flow. Anything whose value depends on evaluating a predicate at load or run time. A cab is a parameter table; branching over it belongs in the Python that calls the step, where it is visible to the reader and to the DAG.
Aliases and propagation. stimela2 propagates parameter values up and down between recipe and step level, which is what forces its expression language to exist. shinobi wires steps with typed InputRef/OutputRef objects instead, so there is nothing to propagate.
dynamic_schema: dotted.path– a reference to a Python function that would need importing and calling to get a cab’s real schema (real cult-cargo’swsclean.yml/cubical.yml/quartical.ymluse this). Resolving it for real is not just a parsing gap like the above: it means executing arbitrary code named by a cab file at load time. Not implemented, and not worked around here either: a cab usingdynamic_schemaalways loads with a warning and whatever staticinputs:/outputs:are present – silently incomplete unless you notice the warning. The hand-authored, cross-checked static schemas for the three real cabs that need this (wsclean, cubical, quartical) live indosho(the native shinobi cab repository, a sibling project) instead of as a stopgap table in this loader – this loader used to carry one (a small per-cabParamPatterntable read from each cab’s own static data files, e.g. cubical’sschema_JONES_TEMPLATE.yaml), removed once dosho’s real ports superseded it. Seedosho/cabs/wsclean.py/cubical.py/quartical.pyfor that knowledge now, and prefer porting a cab there over reintroducing a table here.
Building the expression language out, or actually executing a cab’s own
dynamic_schema, would mean re-deriving stimela2’s config engine (or
its code-execution trust model) – exactly what this project exists to
avoid unless a real cab actually needs it.
- shinobi.loaders.yaml_cab.load_file(path, *, package_roots=None, images=None)[source]¶
Load a YAML cab definition file into Cab instances.
- Parameters:
- Returns:
A dict mapping cab name to its built Cab instance.
- Return type:
- shinobi.loaders.yaml_cab.loads(text, *, package_roots=None, images=None)[source]¶
Parse cab defs from a YAML string. Supports
_use(resolved against the document itself) and package-scoped_include(resolved against package_roots), but not a plain relative-path_include, since there’s no base directory to resolve a relative file path against.imagesmaps an image key to its full reference, for a document that names images symbolically (image: WSCLEAN) rather than by a baked-in reference. Caller-supplied for the same reasonpackage_rootsis: shinobi has no manifest of its own and will not go looking for one. A cab repository passes its own – dosho’s images.yaml is exactly this – so a deployment’s overrides still decide the reference at load time instead of it being fixed when the document was written.An image string absent from the mapping is left alone, because a literal reference is the older and still-valid form (cult-cargo’s files carry
quay.io/stimela2/...directly). A key that is simply misspelled therefore reaches the runtime as an image name and fails there – loudly, at pull time, which is the safe direction: the alternative rejects every legitimate bare name (ubuntu) to catch a typo.
Load stimela-classic style parameters.json cab definitions into shinobi Cab objects. This is a different cab schema format from scabha-dialect YAML (see shinobi.loaders.yaml_cab) – useful for exactly the tools cult-cargo doesn’t have a loadable definition for (several CASA tasks, msutils – see AGENTS.md/examples/ninja_selfcal.py for which ones and why).
Classic’s format: one JSON file per cab (e.g. stimela/cargo/cab/casa_mstransform/parameters.json), a top-level task/binary/base/prefix/msdir plus a flat parameters list – unlike cult-cargo, there’s no _include/_use composition to resolve; each file is fully self-contained.
Field mapping (into a generated inputs_model + field_meta):
name -> the model field name, sanitised to a valid identifier if needed (the original kept as a nom_de_guerre).
dtype -> a Python type on the generated model. A param can declare dtype as a list of alternatives (e.g. [“int”, “str”]) for a genuine type union; the first alternative is used and the rest are dropped – narrowing a real union to shinobi’s simpler model, not a bug.
io: “msfile” forces dtype to “MS” (matching shinobi/cult-cargo convention for the main measurement-set parameter), regardless of whatever the raw dtype said (almost always “file” anyway). io: “input”/”output” have no separate shinobi concept – a file-like type alone already drives bind-mounting via path_fields – so they’re otherwise dropped.
required, default, info -> the model field / its ParamMeta.
mapping -> ParamMeta.nom_de_guerre (classic’s own name for the same concept: what the underlying tool actually calls this parameter).
choices -> ParamMeta.choices, and the generated model field’s real annotation is narrowed to typing.Literal[*choices] (see _modelgen.narrow_choices) – an out-of-set value fails pydantic validation, not just a note in info. Also still appended to info as a human-readable parenthetical, for callers that only look at info.
flavour: classic’s CASA-task cabs (base containing “casa”) are not real standalone executables – binary there is a CASA task name (mstransform/listobs/flagdata/…), invoked by wrapping it in a CASA script, not subprocess.run([“mstransform”, …]). These load with flavour=”casa-task” (shinobi’s existing non-executable flavour, UnsupportedFlavourError-guarded in shinobi.policies – see SECURITY.md’s “Never eval()/exec() a cab’s command” section), not “binary”, so they can’t be silently misrun as if they were real binaries. Cabs with any other base (msutils, wsclean, cubical, …) are real CLI tools and load as flavour=”binary”.
image: classic’s base (e.g. “stimela/casa”) is a base-image family name, not a concrete pullable reference – the real tag/version lives in separate tag/version fields (arrays of compatible versions, no single “the” version). base is used as a best-effort image default; override it on the loaded Cab if you need a specific real image.
- shinobi.loaders.stimela_classic.load_file(path)[source]¶
Load a stimela-classic cab definition (JSON) file into a Cab.
- shinobi.loaders.stimela_classic.loads(text)[source]¶
Parse a stimela-classic cab definition from a JSON string.
Cab loaders, plus the public helpers for building a cab’s schema by hand.
The shinobi.loaders.yaml_cab and shinobi.loaders.stimela_classic
submodules load cab definitions from their respective on-disk formats.
build_model (and sanitize_unique) are the same helpers those loaders use
to turn a flat {name: (dtype, required, default)} spec into the pydantic
inputs_model/outputs_model a Cab needs – re-exported here as the
supported way to build those models directly, without hand-writing a pydantic
class. The implementation lives in the internal _modelgen module.
- shinobi.loaders.build_model(name, fields, *, allow_extra=False, choices=None, extras=None)[source]¶
Create a pydantic model class named name.
fields maps a field name to (dtype, required, default). See required_field_spec for the required/default rule applied to each. choices maps a field name to its allowed values (see narrow_choices) – omitted or absent for a field means its plain dtype-derived type applies unchanged. extras maps a field name to a json_schema_extra dict carried onto that field (e.g. abbreviation for the CLI); a field absent from extras gets none. Mirrors what worker_schema._leaf_field builds per field, so both scabha-dialect loaders attach field-level hints the same way.
- shinobi.loaders.merge_field_meta(base, override)[source]¶
base updated with override, merging (not replacing) the entry of any field name both declare – see merge_param_meta. The one way a loader is allowed to compose a cab’s input-side and output-side metas, so a cab built from a document and the same cab built in Python cannot disagree about what its fields declare.
- shinobi.loaders.merge_param_meta(base, override)[source]¶
Merge two ParamMeta`s describing one field name, attribute by attribute: `override wins wherever it says something, and base survives wherever override left the default.
This exists for the dual declaration – one name on inputs and on outputs, the echo-the-input-back idiom every MS-producing cab uses. Each side declares what it knows: the output side carries implicit (how the product’s path is derived), the input side carries write_path (whether the tool creates that path or rewrites the caller’s data). Replacing the whole object, as the loaders used to, keeps only the last one collected – so the input’s write_path was silently dropped for precisely the fields it exists to mark, and sandbox.clear_stale_outputs then read every such cab as “the caller’s data, leave it alone”. Silent, and on the half of the schema that decides whether a re-run works at all.
Attribute-wise rather than “the input wins” so the output’s implicit still overrides – that is what the whole-object merge got right, and what every loaded cab already depends on.
Configuration¶
Application configuration: layered defaults < config file < env vars < CLI overrides, all validated by the same pydantic models used everywhere else in shinobi. No OmegaConf/scabha/munch/benedict stack – just pydantic-settings, reusing the validation library the cab schemas already depend on.
- class shinobi.config.VenvConfig(*, default=None, envs=<factory>)[source]¶
Bases:
BaseModelSettings for the venv backend (shinobi.backends.venv).
default is the venv used when a step declares none of its own (a path, or a key into envs); None means “no default”, so a venv-backend step with nothing declared falls back to native. envs maps short names to venv paths so recipes/config can refer to a venv by name rather than a machine-specific absolute path. A venv path is a deployment concern, so these live here (or on a Scope in Python), never in a shared cab repo.
- model_config = {}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class shinobi.config.BackendConfig(*, default='native', run_as_host_user=True, venv=<factory>)[source]¶
Bases:
BaseModelSettings controlling which execution backend cabs run under.
- Parameters:
default (str)
run_as_host_user (bool)
venv (VenvConfig)
- venv: VenvConfig¶
- model_config = {}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class shinobi.config.ExecutionConfig(*, max_workers=1, resources=<factory>, enforce_resources='auto', clear_stale_outputs=True)[source]¶
Bases:
BaseModelSettings controlling recipe step scheduling.
- Parameters:
- resources: ResourceBudget¶
- enforce_resources: Literal['auto', 'always', 'never']¶
- model_config = {}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class shinobi.config.SnapshotConfig(*, mode='auto')[source]¶
Bases:
BaseModelSettings controlling mutation-chain snapshots (shinobi.snapshots).
- Parameters:
mode (Literal['auto', 'copy', 'off'])
- mode: Literal['auto', 'copy', 'off']¶
- model_config = {}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class shinobi.config.CacheConfig(*, enabled=False, dir='.shinobi/cache', snapshots=<factory>, content_sample=False)[source]¶
Bases:
BaseModelSettings controlling step-level skip-if-unchanged caching.
- Parameters:
enabled (bool)
dir (str)
snapshots (SnapshotConfig)
content_sample (bool)
- snapshots: SnapshotConfig¶
- model_config = {}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class shinobi.config.LogConfig(*, dir='.', file=None, level='INFO', stream=True, capture_head_lines=5000, capture_tail_lines=5000)[source]¶
Bases:
BaseModelSettings controlling logging and live output streaming.
- Parameters:
- model_config = {}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class shinobi.config.SandboxConfig(*, enabled=False, dir='.shinobi/work')[source]¶
Bases:
BaseModelSettings controlling per-step sandbox execution (shinobi.sandbox).
- model_config = {}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class shinobi.config.ProvenanceConfig(*, enabled=False, dir='.shinobi/runs')[source]¶
Bases:
BaseModelSettings controlling reproducible-run provenance (shinobi.provenance).
- model_config = {}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class shinobi.config.AppConfig(_case_sensitive=None, _nested_model_default_partial_update=None, _env_prefix=None, _env_prefix_target=None, _env_file=PosixPath('.'), _env_file_encoding=None, _env_ignore_empty=None, _env_nested_delimiter=None, _env_nested_max_split=None, _env_parse_none_str=None, _env_parse_enums=None, _cli_prog_name=None, _cli_parse_args=None, _cli_settings_source=None, _cli_parse_none_str=None, _cli_hide_none_type=None, _cli_avoid_json=None, _cli_enforce_required=None, _cli_use_class_docs_for_groups=None, _cli_show_env_vars=None, _cli_exit_on_error=None, _cli_prefix=None, _cli_flag_prefix_char=None, _cli_implicit_flags=None, _cli_ignore_unknown_args=None, _cli_kebab_case=None, _cli_shortcuts=None, _secrets_dir=None, _build_sources=None, *, backend=<factory>, execution=<factory>, log=<factory>, cache=<factory>, provenance=<factory>, sandbox=<factory>)[source]¶
Bases:
BaseSettingsPrecedence, highest to lowest: CLI overrides > env vars (SHINOBI_*) > config file > built-in defaults.
- Parameters:
_case_sensitive (bool | None)
_nested_model_default_partial_update (bool | None)
_env_prefix (str | None)
_env_prefix_target (EnvPrefixTarget | None)
_env_file (DotenvType | None)
_env_file_encoding (str | None)
_env_ignore_empty (bool | None)
_env_nested_delimiter (str | None)
_env_nested_max_split (int | None)
_env_parse_none_str (str | None)
_env_parse_enums (bool | None)
_cli_prog_name (str | None)
_cli_settings_source (CliSettingsSource[Any] | None)
_cli_parse_none_str (str | None)
_cli_hide_none_type (bool | None)
_cli_avoid_json (bool | None)
_cli_enforce_required (bool | None)
_cli_use_class_docs_for_groups (bool | None)
_cli_show_env_vars (bool | None)
_cli_exit_on_error (bool | None)
_cli_prefix (str | None)
_cli_flag_prefix_char (str | None)
_cli_implicit_flags (bool | Literal['dual', 'toggle'] | None)
_cli_ignore_unknown_args (bool | None)
_cli_kebab_case (bool | Literal['all', 'no_enums'] | None)
_secrets_dir (PathType | None)
_build_sources (tuple[tuple[PydanticBaseSettingsSource, ...], dict[str, Any]] | None)
backend (BackendConfig)
execution (ExecutionConfig)
log (LogConfig)
cache (CacheConfig)
provenance (ProvenanceConfig)
sandbox (SandboxConfig)
- model_config = {'arbitrary_types_allowed': True, 'case_sensitive': False, 'cli_avoid_json': False, 'cli_enforce_required': False, 'cli_exit_on_error': True, 'cli_flag_prefix_char': '-', 'cli_hide_none_type': False, 'cli_ignore_unknown_args': False, 'cli_implicit_flags': False, 'cli_kebab_case': False, 'cli_parse_args': None, 'cli_parse_none_str': None, 'cli_prefix': '', 'cli_prog_name': None, 'cli_shortcuts': None, 'cli_show_env_vars': False, 'cli_use_class_docs_for_groups': False, 'enable_decoding': True, 'env_file': None, 'env_file_encoding': None, 'env_ignore_empty': False, 'env_nested_delimiter': '__', 'env_nested_max_split': None, 'env_parse_enums': None, 'env_parse_none_str': None, 'env_prefix': 'SHINOBI_', 'env_prefix_target': 'variable', 'extra': 'forbid', 'json_file': None, 'json_file_encoding': None, 'nested_model_default_partial_update': False, 'protected_namespaces': ('model_validate', 'model_dump', 'settings_customise_sources'), 'secrets_dir': None, 'toml_file': None, 'validate_default': True, 'yaml_config_section': None, 'yaml_file': None, 'yaml_file_encoding': None}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- backend: BackendConfig¶
- execution: ExecutionConfig¶
- cache: CacheConfig¶
- provenance: ProvenanceConfig¶
- sandbox: SandboxConfig¶
- classmethod settings_customise_sources(settings_cls, init_settings, env_settings, dotenv_settings, file_secret_settings)[source]¶
Set the settings source precedence: init > env vars > YAML file.
- Parameters:
settings_cls – The BaseSettings subclass being configured.
init_settings – Source for values passed directly to __init__.
env_settings – Source for SHINOBI_* environment variables.
dotenv_settings – Unused; .env files are not supported.
file_secret_settings – Unused; Docker/Kubernetes secret files are not supported.
- Returns:
The ordered tuple of settings sources pydantic-settings should consult, highest precedence first.