Skip to content

mambo_power.jobs

The stateless job surface. See the manual page for the guarantees, the failure codes and executed examples.

mambo_power.jobs

Stateless, JSON-serialisable job surface: run(SolveRequest) -> SolveResult (ADR-004, W6).

The one function every analysis kind is reachable through, safe to call from a notebook, a CLI, a worker or an HTTP handler — a service adds transport and persistence, never semantics. run is a pure function of its input; every failure is a status = "failed" result with a StructuredError, never an exception; KINDS is the capability list of the installed version. The module-level entry points (pf.solve_ac, pf.solve_dc) remain the notebook-friendly API and are what the registered runners call.

FailureCode module-attribute

FailureCode = Literal[
    "UNKNOWN_KIND",
    "BAD_REQUEST",
    "BAD_OPTIONS",
    "VALIDATION",
    "NO_SLACK_GENERATOR",
    "UNSOLVABLE_NETWORK",
    "INFEASIBLE_LP",
    "UNBOUNDED_LP",
    "INTERNAL",
]

The codes mambo_power.jobs.run / mambo_power.jobs.run_json emit (M2, M3).

ResultModel module-attribute

The closed union of result types a SolveResult can carry (one per registered kind).

KINDS module-attribute

KINDS: dict[str, KindSpec] = {}

Every analysis kind the installed version can run, keyed by name (insertion order).

Runner module-attribute

Runner = Callable[[Scenario, BaseModel | None], BaseModel]

Signature every kind's runner has: (scenario, validated_options_or_None) -> result.

SolveRequest

Bases: BaseModel

One analysis to run: the kind, the subject (inline) and the kind's options.

network/scenario — wave M5 design item D3 (2026-08-25) — exactly one must be given: network is the original, still-supported shape (a bare Network), and every pre-existing SolveRequest(kind=..., network=...) construction and serialized JSON keeps working unchanged. scenario is the new form, for a genuine multi-period Scenario (market.multiperiod) or simply an explicit single-period one. Neither or both given is a ValueError (a pydantic error at construction time; mambo_power.jobs.run_json turns it into BAD_REQUEST). resolved_scenario is what every Runner actually receives — scenario itself, or network wrapped as Scenario(network=network) (single-period, periods=None, exactly market.nodal's and every T=1 kind's existing semantics) — never the raw fields, so widening this model changes no runner's contract.

options is validated by mambo_power.jobs.run against the kind's options model (AcOptions for pf.ac; pf.dc takes none) — unknown keys are a BAD_OPTIONS failure, never silently ignored. job_id is an opaque caller tag echoed on the result.

kind class-attribute instance-attribute

kind: str

A key of jobs.KINDS, e.g. pf.ac.

network class-attribute instance-attribute

network: Network | None

The network to solve; mutually exclusive with scenario.

scenario class-attribute instance-attribute

scenario: Scenario | None

The scenario to solve; mutually exclusive with network.

options class-attribute instance-attribute

options: dict[str, Any]

Kind-specific options, validated by run.

job_id class-attribute instance-attribute

job_id: str | None

Caller's correlation id, echoed back.

resolved_scenario property

resolved_scenario: Scenario

This request as a Scenario: scenario itself when given, or network wrapped as Scenario(network=network) — single-period, periods=None. Recomputed on every access, not cached, so a network mutated in place after construction (request.network.branches[0].to_bus = ...Network does not re-validate on mutation on its own) is reflected here too, exactly as it was when jobs.registry._run_market_nodal did this same wrap internally pre-M5.

Constructing the wrapping Scenario does re-run Network's own after-validator — nested-model construction re-checks every invariant (model/scenario.py's own docstring) — so this can raise NetworkValidationError for a network mutated into an invalid state; mambo_power.jobs.run catches that itself, immediately, precisely so it stays a graceful VALIDATION failure rather than an exception crossing its boundary. A directly-supplied scenario is returned as-is, with no such re-check — mirroring network's own no-revalidation-on-mutation rule.

SolveResult

Bases: BaseModel

Outcome of mambo_power.jobs.run: a typed result or a structured error, never both.

status == "ok" carries result (the kind's result model) and its provenance; status == "failed" carries error and, when the kind was readable, a minimal provenance (kind, version, elapsed time, solver = "none"). warnings holds every warning emitted during the solve as "Category: message" strings — for a network with conflicting generator setpoints that is the SetpointConflictWarning. A power flow that did not converge is status == "ok" with result.converged == False: the partial state is a result, not a failure.

kind class-attribute instance-attribute

kind: str

Echo of the request kind ("" when unreadable).

job_id class-attribute instance-attribute

job_id: str | None

Echo of the request job_id.

status class-attribute instance-attribute

status: Literal['ok', 'failed']

Outcome.

result class-attribute instance-attribute

result: ResultModel | None

The kind's result model; present when status == "ok".

error class-attribute instance-attribute

error: StructuredError | None

Present when status == "failed".

provenance class-attribute instance-attribute

provenance: ResultProvenance | None

The result's stamp, or a minimal one on failure.

warnings class-attribute instance-attribute

warnings: list[str]

Warnings emitted during the solve, as strings.

StructuredError

Bases: BaseModel

A failure as data: stable code, readable message, optional structured detail.

issues is the network's full ValidationIssue list for VALIDATION failures (every problem in one response); details is the pydantic error list (loc, msg, type) for BAD_OPTIONS and BAD_REQUEST. code is a plain string so later kinds can add codes without a schema change; M2's are FailureCode.

code class-attribute instance-attribute

code: str

Stable failure code, e.g. VALIDATION.

message class-attribute instance-attribute

message: str

Human-readable description; the exception text when any.

issues class-attribute instance-attribute

issues: list[ValidationIssue] | None

Every network validation issue, for VALIDATION.

details class-attribute instance-attribute

details: list[dict[str, Any]] | None

pydantic error records (loc, msg, type) for bad options/requests.

InfeasibleLpError

Bases: Exception

opf.dc's, market.nodal's, market.multiperiod's, market.zonal's or market.agents's runner found a non-Optimal, non-Unbounded status (e.g. OpfDcResult.status == "Infeasible") — see _translate_non_optimal_status.

None of mambo_power.opf.solve_dc_opf, mambo_power.market.nodal.solve_nodal, mambo_power.market.multiperiod.solve_multiperiod, mambo_power.market.zonal.solve_zonal or mambo_power.market.agents.solve_agents ever raises on a non-Optimal LP/QP status (their own docstrings, mirroring mambo_power.pf.solve_ac's never-raise-on-non-convergence convention) — each reports the status as data. But an infeasible LP has no dispatch at all, unlike a non-converged AC iterate which still carries a meaningful partial state; wave M3's design (item 7) draws that line deliberately, so every such job kind reports it as a structured job failure (INFEASIBLE_LP) rather than a "successful" result carrying a non-Optimal status. Raised only here, by the job runners — not by solve_dc_opf/solve_nodal/solve_multiperiod/solve_zonal/solve_agents themselves.

KindSpec dataclass

KindSpec(
    kind: str,
    options_model: type[BaseModel] | None,
    result_model: type[BaseModel],
    runner: Runner,
)

One entry of KINDS: the models and runner of an analysis kind.

kind instance-attribute

kind: str

Registry key, e.g. "pf.ac".

options_model instance-attribute

options_model: type[BaseModel] | None

Model SolveRequest.options is validated into; None means the kind takes none.

result_model instance-attribute

result_model: type[BaseModel]

Type the runner returns and SolveResult.result carries for this kind.

runner instance-attribute

runner: Runner

(scenario, options) -> result; options is None when options_model is.

UnboundedLpError

Bases: Exception

opf.dc's, market.nodal's, market.multiperiod's, market.zonal's or market.agents's runner found status "Unbounded"; see InfeasibleLpError for why this is a job failure rather than an "ok" result.

kinds

kinds() -> list[str]

The registered kind names, sorted.

Source code in src/mambo_power/jobs/registry.py
def kinds() -> list[str]:
    """The registered kind names, sorted."""
    return sorted(KINDS)

register

register(spec: KindSpec) -> None

Add spec to KINDS; a kind already registered raises ValueError.

Source code in src/mambo_power/jobs/registry.py
def register(spec: KindSpec) -> None:
    """Add ``spec`` to :data:`KINDS`; a kind already registered raises ``ValueError``."""
    if spec.kind in KINDS:
        raise ValueError(f'kind "{spec.kind}" is already registered')
    KINDS[spec.kind] = spec

run_json

run_json(text: str) -> str

JSON in, JSON out: parse text as a SolveRequest, run it.

Returns SolveResult.model_dump_json(). A request that does not parse is a failed result too: an invalid network → VALIDATION with every issue; malformed JSON or a request of the wrong shape → BAD_REQUEST with the pydantic errors in error.details; a JSON object repeating a key at any depth → BAD_REQUEST naming the key and its path, for every kind, because json would otherwise keep the last value silently. The kind and job_id are echoed when they can be read from the text (kind = "" and no provenance otherwise).

Source code in src/mambo_power/jobs/run.py
def run_json(text: str) -> str:
    """JSON in, JSON out: parse ``text`` as a :class:`~mambo_power.jobs.SolveRequest`, ``run`` it.

    Returns ``SolveResult.model_dump_json()``. A request that does not parse is a failed result
    too: an invalid network → ``VALIDATION`` with every issue; malformed JSON or a request of
    the wrong shape → ``BAD_REQUEST`` with the pydantic errors in ``error.details``; a JSON
    object repeating a key at any depth → ``BAD_REQUEST`` naming the key and its path, for every
    kind, because ``json`` would otherwise keep the last value silently. The kind
    and ``job_id`` are echoed when they can be read from the text (``kind = ""`` and no
    provenance otherwise).
    """
    started_at = datetime.now(UTC)
    clock = time.perf_counter()
    try:
        _reject_duplicate_keys(text)
        request = SolveRequest.model_validate_json(text)
    except _DuplicateKeyError as exc:
        kind, job_id = _peek(text)
        return _failed(
            kind=kind,
            job_id=job_id,
            code="BAD_REQUEST",
            message=f"request is not a valid SolveRequest: {exc}",
            started_at=started_at,
            clock=clock,
        ).model_dump_json()
    except NetworkValidationError as exc:
        kind, job_id = _peek(text)
        return _failed(
            kind=kind,
            job_id=job_id,
            code="VALIDATION",
            message=str(exc),
            issues=exc.issues,
            started_at=started_at,
            clock=clock,
        ).model_dump_json()
    except ValidationError as exc:
        kind, job_id = _peek(text)
        return _failed(
            kind=kind,
            job_id=job_id,
            code="BAD_REQUEST",
            message=f"request is not a valid SolveRequest: {exc}",
            details=_pydantic_details(exc),
            started_at=started_at,
            clock=clock,
        ).model_dump_json()
    except Exception as exc:  # noqa: BLE001 — nothing crosses the boundary
        kind, job_id = _peek(text)
        return _failed(
            kind=kind,
            job_id=job_id,
            code="INTERNAL",
            message=f"{type(exc).__name__}: {exc}",
            started_at=started_at,
            clock=clock,
        ).model_dump_json()
    return run(request).model_dump_json()

Models

mambo_power.jobs.models

Request, result and error models of the job surface (ADR-004; wave M2 W6).

All three are pydantic v2 models with extra="forbid" and exact JSON round-trip, so the body of an HTTP request is a SolveRequest and the body of the response is a SolveResult — no translation layer.

How SolveResult.result is typed. Its annotation is the closed union of the registered kinds' result models (AcPowerFlowResult | DcPowerFlowResult in M2, widened to add OpfDcResult | N1Result in M3, MarketNodalResult in M4, MarketMultiperiodResult in M5, MarketZonalResult in M6, MarketAgentsResult in M7). The type is not inferred from the payload's shape: a model_validator(mode="before") looks the request kind up in KINDS and validates a dict result with exactly that kind's result_model; a second validator (mode="after") then checks that the instance type equals the kind's model, and that status agrees with which of result / error is present. A pydantic discriminated union was not used because the discriminator (kind) lives on the parent, not inside the result, and because the power-flow results do not carry a tag field of their own. A wave that registers a new kind widens the union annotation — the after validator will refuse a result whose class is not the kind's result_model, and the field validation refuses a class outside the union, so the two cannot silently drift apart.

ResultModel module-attribute

The closed union of result types a SolveResult can carry (one per registered kind).

FailureCode module-attribute

FailureCode = Literal[
    "UNKNOWN_KIND",
    "BAD_REQUEST",
    "BAD_OPTIONS",
    "VALIDATION",
    "NO_SLACK_GENERATOR",
    "UNSOLVABLE_NETWORK",
    "INFEASIBLE_LP",
    "UNBOUNDED_LP",
    "INTERNAL",
]

The codes mambo_power.jobs.run / mambo_power.jobs.run_json emit (M2, M3).

StructuredError

Bases: BaseModel

A failure as data: stable code, readable message, optional structured detail.

issues is the network's full ValidationIssue list for VALIDATION failures (every problem in one response); details is the pydantic error list (loc, msg, type) for BAD_OPTIONS and BAD_REQUEST. code is a plain string so later kinds can add codes without a schema change; M2's are FailureCode.

code class-attribute instance-attribute

code: str

Stable failure code, e.g. VALIDATION.

message class-attribute instance-attribute

message: str

Human-readable description; the exception text when any.

issues class-attribute instance-attribute

issues: list[ValidationIssue] | None

Every network validation issue, for VALIDATION.

details class-attribute instance-attribute

details: list[dict[str, Any]] | None

pydantic error records (loc, msg, type) for bad options/requests.

SolveRequest

Bases: BaseModel

One analysis to run: the kind, the subject (inline) and the kind's options.

network/scenario — wave M5 design item D3 (2026-08-25) — exactly one must be given: network is the original, still-supported shape (a bare Network), and every pre-existing SolveRequest(kind=..., network=...) construction and serialized JSON keeps working unchanged. scenario is the new form, for a genuine multi-period Scenario (market.multiperiod) or simply an explicit single-period one. Neither or both given is a ValueError (a pydantic error at construction time; mambo_power.jobs.run_json turns it into BAD_REQUEST). resolved_scenario is what every Runner actually receives — scenario itself, or network wrapped as Scenario(network=network) (single-period, periods=None, exactly market.nodal's and every T=1 kind's existing semantics) — never the raw fields, so widening this model changes no runner's contract.

options is validated by mambo_power.jobs.run against the kind's options model (AcOptions for pf.ac; pf.dc takes none) — unknown keys are a BAD_OPTIONS failure, never silently ignored. job_id is an opaque caller tag echoed on the result.

kind class-attribute instance-attribute

kind: str

A key of jobs.KINDS, e.g. pf.ac.

network class-attribute instance-attribute

network: Network | None

The network to solve; mutually exclusive with scenario.

scenario class-attribute instance-attribute

scenario: Scenario | None

The scenario to solve; mutually exclusive with network.

options class-attribute instance-attribute

options: dict[str, Any]

Kind-specific options, validated by run.

job_id class-attribute instance-attribute

job_id: str | None

Caller's correlation id, echoed back.

resolved_scenario property

resolved_scenario: Scenario

This request as a Scenario: scenario itself when given, or network wrapped as Scenario(network=network) — single-period, periods=None. Recomputed on every access, not cached, so a network mutated in place after construction (request.network.branches[0].to_bus = ...Network does not re-validate on mutation on its own) is reflected here too, exactly as it was when jobs.registry._run_market_nodal did this same wrap internally pre-M5.

Constructing the wrapping Scenario does re-run Network's own after-validator — nested-model construction re-checks every invariant (model/scenario.py's own docstring) — so this can raise NetworkValidationError for a network mutated into an invalid state; mambo_power.jobs.run catches that itself, immediately, precisely so it stays a graceful VALIDATION failure rather than an exception crossing its boundary. A directly-supplied scenario is returned as-is, with no such re-check — mirroring network's own no-revalidation-on-mutation rule.

SolveResult

Bases: BaseModel

Outcome of mambo_power.jobs.run: a typed result or a structured error, never both.

status == "ok" carries result (the kind's result model) and its provenance; status == "failed" carries error and, when the kind was readable, a minimal provenance (kind, version, elapsed time, solver = "none"). warnings holds every warning emitted during the solve as "Category: message" strings — for a network with conflicting generator setpoints that is the SetpointConflictWarning. A power flow that did not converge is status == "ok" with result.converged == False: the partial state is a result, not a failure.

kind class-attribute instance-attribute

kind: str

Echo of the request kind ("" when unreadable).

job_id class-attribute instance-attribute

job_id: str | None

Echo of the request job_id.

status class-attribute instance-attribute

status: Literal['ok', 'failed']

Outcome.

result class-attribute instance-attribute

result: ResultModel | None

The kind's result model; present when status == "ok".

error class-attribute instance-attribute

error: StructuredError | None

Present when status == "failed".

provenance class-attribute instance-attribute

provenance: ResultProvenance | None

The result's stamp, or a minimal one on failure.

warnings class-attribute instance-attribute

warnings: list[str]

Warnings emitted during the solve, as strings.

Registry

mambo_power.jobs.registry

The analysis-kinds registry: what the installed version can run (ADR-004, design item 6).

KINDS maps a kind name ("pf.ac", "pf.dc", "opf.dc", "n1", "market.nodal", "market.multiperiod", "market.zonal", "market.agents") to a KindSpec — the options model the request's options dict is validated against, the result model the runner returns, and the runner itself. The registry is the capability list a service publishes, and the contract test (AC-6/AC-8, wave M4 AC-7, wave M5 AC-7, wave M6 AC-7, wave M7 AC-6) asserts every entry's models are importable and its runner callable. Later waves add kinds with register; nothing else in the package changes.

market.nodal was the first kind whose subject is not a bare Network: mambo_power.market.nodal.solve_nodal takes a Scenario. Wave M4 kept SolveRequest network-shaped and had _run_market_nodal wrap the incoming Network into a Scenario itself, since Scenario was then genuinely just network: Network. Wave M5 (design item D3) widened SolveRequest to accept either network or scenario — now that Scenario also carries periods, a bare Network genuinely cannot supply everything a caller may need — so that wrap moved outward, onto SolveRequest.resolved_scenario (jobs/models.py): every Runner now has the one (Scenario, options) -> result shape, and reads .network off the scenario when that is all it needs (pf.ac, pf.dc, opf.dc, n1); _run_market_nodal no longer wraps anything itself.

Runner module-attribute

Runner = Callable[[Scenario, BaseModel | None], BaseModel]

Signature every kind's runner has: (scenario, validated_options_or_None) -> result.

KINDS module-attribute

KINDS: dict[str, KindSpec] = {}

Every analysis kind the installed version can run, keyed by name (insertion order).

InfeasibleLpError

Bases: Exception

opf.dc's, market.nodal's, market.multiperiod's, market.zonal's or market.agents's runner found a non-Optimal, non-Unbounded status (e.g. OpfDcResult.status == "Infeasible") — see _translate_non_optimal_status.

None of mambo_power.opf.solve_dc_opf, mambo_power.market.nodal.solve_nodal, mambo_power.market.multiperiod.solve_multiperiod, mambo_power.market.zonal.solve_zonal or mambo_power.market.agents.solve_agents ever raises on a non-Optimal LP/QP status (their own docstrings, mirroring mambo_power.pf.solve_ac's never-raise-on-non-convergence convention) — each reports the status as data. But an infeasible LP has no dispatch at all, unlike a non-converged AC iterate which still carries a meaningful partial state; wave M3's design (item 7) draws that line deliberately, so every such job kind reports it as a structured job failure (INFEASIBLE_LP) rather than a "successful" result carrying a non-Optimal status. Raised only here, by the job runners — not by solve_dc_opf/solve_nodal/solve_multiperiod/solve_zonal/solve_agents themselves.

UnboundedLpError

Bases: Exception

opf.dc's, market.nodal's, market.multiperiod's, market.zonal's or market.agents's runner found status "Unbounded"; see InfeasibleLpError for why this is a job failure rather than an "ok" result.

KindSpec dataclass

KindSpec(
    kind: str,
    options_model: type[BaseModel] | None,
    result_model: type[BaseModel],
    runner: Runner,
)

One entry of KINDS: the models and runner of an analysis kind.

kind instance-attribute

kind: str

Registry key, e.g. "pf.ac".

options_model instance-attribute

options_model: type[BaseModel] | None

Model SolveRequest.options is validated into; None means the kind takes none.

result_model instance-attribute

result_model: type[BaseModel]

Type the runner returns and SolveResult.result carries for this kind.

runner instance-attribute

runner: Runner

(scenario, options) -> result; options is None when options_model is.

register

register(spec: KindSpec) -> None

Add spec to KINDS; a kind already registered raises ValueError.

Source code in src/mambo_power/jobs/registry.py
def register(spec: KindSpec) -> None:
    """Add ``spec`` to :data:`KINDS`; a kind already registered raises ``ValueError``."""
    if spec.kind in KINDS:
        raise ValueError(f'kind "{spec.kind}" is already registered')
    KINDS[spec.kind] = spec

kinds

kinds() -> list[str]

The registered kind names, sorted.

Source code in src/mambo_power/jobs/registry.py
def kinds() -> list[str]:
    """The registered kind names, sorted."""
    return sorted(KINDS)

Runner

mambo_power.jobs.run

run and run_json: the one pure entry point every analysis kind is reachable through.

Pipeline of run (design item 6):

  1. look the kind up in KINDS — miss → UNKNOWN_KIND;
  2. validate request.options into the kind's options model — BAD_OPTIONS (pydantic errors in error.details); a kind without an options model rejects any key;
  3. resolve request to a Scenario via request.resolved_scenario (wave M5 D3: scenario as given, or network wrapped) and re-check its network's invariants with mambo_power.model.validate_networkVALIDATION with every issue (a Network validates on construction but not on mutation, so run does not trust its input);
  4. call the runner under warnings.catch_warnings(record=True) and wrap what it raises: NetworkValidationErrorVALIDATION with .issues; NoSlackGeneratorErrorNO_SLACK_GENERATOR; UnsolvableNetworkErrorUNSOLVABLE_NETWORK (a valid network the numerics it was handed to cannot solve, e.g. DC on an x == 0 branch — user data, not a solver bug); InfeasibleLpError/:class:~mambo_power.jobs.registry. UnboundedLpErrorINFEASIBLE_LP/UNBOUNDED_LP (the opf.dc runner's own translation of a non-Optimal status; unlike pf.ac's non-convergence, an infeasible/unbounded LP has no dispatch at all, so it is a structured failure rather than an "ok" result); anything else → INTERNAL with "ExceptionType: message";
  5. check the runner returned the kind's result_model (else INTERNAL), copy its provenance and the captured warnings onto the SolveResult.

No exception crosses run: a runner whose result class is outside the result union is an INTERNAL failure too. A solve that does not converge is not a failure — solve_ac returns converged = False rather than raising, and that result is passed through with status = "ok".

Warnings are captured with warnings.catch_warnings, which swaps the process-global filter list for the duration of the runner; two run calls on different threads can therefore see each other's warnings (Python ≥ 3.14 makes the context thread-local). Pure means "a function of its input", which holds; warning attribution across threads is the one caveat, and a worker process per job (the SaaS's deployment shape) does not hit it.

NO_SOLVER module-attribute

NO_SOLVER = 'none'

provenance.solver on a failed result: no linear-algebra backend ran to completion.

run

run(request: SolveRequest) -> SolveResult

Run request and return a SolveResult; never raises.

See the module docstring for the pipeline and the failure codes. On success result is the kind's result model, provenance is the stamp the solver put on it, and warnings lists every warning the solve emitted (they are captured, not shown).

Source code in src/mambo_power/jobs/run.py
def run(request: SolveRequest) -> SolveResult:
    """Run ``request`` and return a :class:`~mambo_power.jobs.SolveResult`; never raises.

    See the module docstring for the pipeline and the failure codes. On success
    ``result`` is the kind's result model, ``provenance`` is the stamp the solver put on it,
    and ``warnings`` lists every warning the solve emitted (they are captured, not shown).
    """
    started_at = datetime.now(UTC)
    clock = time.perf_counter()
    kind, job_id = request.kind, request.job_id

    def fail(code: str, message: str, **extra: Any) -> SolveResult:
        return _failed(
            kind=kind,
            job_id=job_id,
            code=code,
            message=message,
            started_at=started_at,
            clock=clock,
            **extra,
        )

    spec = KINDS.get(kind)
    if spec is None:
        known = ", ".join(sorted(KINDS))
        return fail("UNKNOWN_KIND", f'unknown kind "{kind}"; registered kinds: {known}')

    options: BaseModel | None = None
    if spec.options_model is not None:
        try:
            options = spec.options_model.model_validate(request.options)
        except ValidationError as exc:
            return fail(
                "BAD_OPTIONS",
                f'options for kind "{kind}" are invalid: {exc}',
                details=_pydantic_details(exc),
            )
    elif request.options:
        keys = ", ".join(sorted(request.options))
        return fail("BAD_OPTIONS", f'kind "{kind}" takes no options; got: {keys}')
    run_options = options.model_dump() if options is not None else {}

    try:
        scenario = request.resolved_scenario
    except NetworkValidationError as exc:
        # request.network was mutated in place into an invalid network after construction
        # (Network does not re-validate on mutation) and resolved_scenario's wrap of it into a
        # fresh Scenario *does* re-run Network's own after-validator (Scenario's own docstring:
        # nested-model construction re-checks every invariant, dangling refs included) -- so the
        # wrap itself can raise where a bare `request.network` never did. Caught here, in the
        # same shape as the explicit validate_network() check just below, so this remains a
        # graceful VALIDATION failure rather than an exception crossing run()'s boundary.
        return fail("VALIDATION", str(exc), issues=exc.issues, options=run_options)
    issues = validate_network(scenario.network)
    if issues:
        error = NetworkValidationError(issues)
        return fail("VALIDATION", str(error), issues=issues, options=run_options)

    raw: BaseModel | None = None
    failure: tuple[str, str, list[ValidationIssue] | None] | None = None
    with warnings.catch_warnings(record=True) as caught:
        warnings.simplefilter("always")
        try:
            raw = spec.runner(scenario, options)
        except NetworkValidationError as exc:
            failure = ("VALIDATION", str(exc), exc.issues)
        except MissingCostError as exc:
            # a generator with no cost under a kind that prices dispatch: the caller's data, not
            # an engine bug (M8 walk, surprise 3). No ValidationIssue: the closed ValidationCode
            # set is the model's own invariants, and a missing cost is legal model data that only
            # the pricing kinds refuse -- the message names every generator.
            failure = ("VALIDATION", str(exc), None)
        except NoSlackGeneratorError as exc:
            failure = ("NO_SLACK_GENERATOR", str(exc), None)
        except UnsolvableNetworkError as exc:
            failure = ("UNSOLVABLE_NETWORK", str(exc), None)
        except InfeasibleLpError as exc:
            failure = ("INFEASIBLE_LP", str(exc), None)
        except UnboundedLpError as exc:
            failure = ("UNBOUNDED_LP", str(exc), None)
        except Exception as exc:  # noqa: BLE001 — the boundary's whole point
            failure = ("INTERNAL", f"{type(exc).__name__}: {exc}", None)
    captured = _messages(caught)

    if failure is not None:
        code, message, failure_issues = failure
        return fail(code, message, issues=failure_issues, options=run_options, captured=captured)
    if type(raw) is not spec.result_model:
        return fail(
            "INTERNAL",
            f'runner for kind "{kind}" returned {type(raw).__name__}, '
            f"expected {spec.result_model.__name__}",
            options=run_options,
            captured=captured,
        )
    if not isinstance(raw, ResultModel):
        return fail(
            "INTERNAL",
            f'result model {type(raw).__name__} of kind "{kind}" is not in SolveResult.result',
            options=run_options,
            captured=captured,
        )
    provenance = getattr(raw, "provenance", None)
    if not isinstance(provenance, ResultProvenance):
        provenance = _minimal_provenance(kind, started_at, clock, run_options)
    return SolveResult(
        kind=kind, job_id=job_id, status="ok", result=raw, provenance=provenance, warnings=captured
    )

run_json

run_json(text: str) -> str

JSON in, JSON out: parse text as a SolveRequest, run it.

Returns SolveResult.model_dump_json(). A request that does not parse is a failed result too: an invalid network → VALIDATION with every issue; malformed JSON or a request of the wrong shape → BAD_REQUEST with the pydantic errors in error.details; a JSON object repeating a key at any depth → BAD_REQUEST naming the key and its path, for every kind, because json would otherwise keep the last value silently. The kind and job_id are echoed when they can be read from the text (kind = "" and no provenance otherwise).

Source code in src/mambo_power/jobs/run.py
def run_json(text: str) -> str:
    """JSON in, JSON out: parse ``text`` as a :class:`~mambo_power.jobs.SolveRequest`, ``run`` it.

    Returns ``SolveResult.model_dump_json()``. A request that does not parse is a failed result
    too: an invalid network → ``VALIDATION`` with every issue; malformed JSON or a request of
    the wrong shape → ``BAD_REQUEST`` with the pydantic errors in ``error.details``; a JSON
    object repeating a key at any depth → ``BAD_REQUEST`` naming the key and its path, for every
    kind, because ``json`` would otherwise keep the last value silently. The kind
    and ``job_id`` are echoed when they can be read from the text (``kind = ""`` and no
    provenance otherwise).
    """
    started_at = datetime.now(UTC)
    clock = time.perf_counter()
    try:
        _reject_duplicate_keys(text)
        request = SolveRequest.model_validate_json(text)
    except _DuplicateKeyError as exc:
        kind, job_id = _peek(text)
        return _failed(
            kind=kind,
            job_id=job_id,
            code="BAD_REQUEST",
            message=f"request is not a valid SolveRequest: {exc}",
            started_at=started_at,
            clock=clock,
        ).model_dump_json()
    except NetworkValidationError as exc:
        kind, job_id = _peek(text)
        return _failed(
            kind=kind,
            job_id=job_id,
            code="VALIDATION",
            message=str(exc),
            issues=exc.issues,
            started_at=started_at,
            clock=clock,
        ).model_dump_json()
    except ValidationError as exc:
        kind, job_id = _peek(text)
        return _failed(
            kind=kind,
            job_id=job_id,
            code="BAD_REQUEST",
            message=f"request is not a valid SolveRequest: {exc}",
            details=_pydantic_details(exc),
            started_at=started_at,
            clock=clock,
        ).model_dump_json()
    except Exception as exc:  # noqa: BLE001 — nothing crosses the boundary
        kind, job_id = _peek(text)
        return _failed(
            kind=kind,
            job_id=job_id,
            code="INTERNAL",
            message=f"{type(exc).__name__}: {exc}",
            started_at=started_at,
            clock=clock,
        ).model_dump_json()
    return run(request).model_dump_json()