Source code for dosho._builder

"""Thin builder for authoring a native `dosho` cab: wraps `shinobi.Cab` +
`shinobi.loaders.build_model` + `shinobi.loaders.sanitize_unique` to remove
the boilerplate of hand-transcribing a real tool's parameter list, without
inventing any new schema surface.

Generalises the `cab_from_defaults` helper first proven in
stimela-ninja's `examples/ninja_selfcal.py`. Deliberately not a
signature-introspecting decorator (`@cab`) -- that shape was tried once in
shinobi itself (`ba2bd48`) and removed (`605b6f8`): it can't express
`nom_de_guerre` (hyphenated CLI names aren't valid Python parameter
names), `input_patterns`, `Policies`, `positional`, or `implicit`. Raw
`Cab(...)` stays fully available for anything this helper doesn't cover --
it's sugar, not a wall.
"""

from __future__ import annotations

from typing import Any

from shinobi import Cab
from shinobi.loaders import build_model, sanitize_unique
from shinobi.steps.schema import ParamMeta, ParamPattern, Policies


[docs] def define_cab( name: str, command: str, image: str, fields: dict[str, tuple[str, bool, Any]], *, outputs: dict[str, tuple[str, bool, Any]] | None = None, field_meta: dict[str, ParamMeta] | None = None, policies: Policies | None = None, input_patterns: list[ParamPattern] | None = None, output_patterns: list[ParamPattern] | None = None, flavour: str = "binary", wranglers: dict[str, list[str]] | None = None, info: str | None = None, ) -> Cab: """Build a `Cab` from a flat `{raw_param_name: (dtype, required, default)}` dict -- the shape a tool's own `--help`/docs naturally give you (e.g. `"data-ms": ("MS", True, None)`). Hyphenated/dotted raw names are sanitised to valid pydantic field names, with the original kept as a `nom_de_guerre` so the built cab's `build_argv` still emits the tool's real flag. `field_meta` entries the caller passes explicitly (keyed by the *sanitised* field name -- e.g. an output field's `implicit` template, or a `positional`/`repeat_as_tokens` override) are merged in on top of the auto-derived `nom_de_guerre`s; an explicit entry that doesn't set its own `nom_de_guerre` keeps the auto-derived one rather than losing it. """ resolved_fields: dict[str, tuple[str, bool, Any]] = {} resolved_meta: dict[str, ParamMeta] = {} seen: dict[str, str] = {} for raw_name, spec in fields.items(): field = sanitize_unique(raw_name, seen) resolved_fields[field] = spec if field != raw_name: resolved_meta[field] = ParamMeta(nom_de_guerre=raw_name) for field, meta in (field_meta or {}).items(): auto = resolved_meta.get(field) if auto is not None and meta.nom_de_guerre is None: meta = meta.model_copy(update={"nom_de_guerre": auto.nom_de_guerre}) resolved_meta[field] = meta return Cab( name=name, command=command, image=image, info=info, flavour=flavour, inputs_model=build_model( f"{name}_Inputs", resolved_fields, allow_extra=bool(input_patterns) ), outputs_model=build_model(f"{name}_Outputs", outputs or {}), field_meta=resolved_meta, policies=policies or Policies(), input_patterns=input_patterns or [], output_patterns=output_patterns or [], wranglers=wranglers or {}, )