Skip to content

validate

tit.server.routes.validate

POST /api/validate/{kind} -- field-level config validation, no side effects.

Deserializes config through :func:tit.config_io.deserialize_config for the dataclass that kind maps to (see :func:cls_for), catching exactly what a config dataclass's own __post_init__ (or a bad _type/field shape) can raise, and reports it as {ok, errors: [{path, message}]} -- the UI never re-implements these rules (design rule R1); this route only surfaces them. Exhaustive-search mask paths are also checked for accessibility before a plan or costly leadfield load can proceed.

Kind -> config-class resolution

Most kinds map to exactly one dataclass in :data:tit.config_io.CONFIG_CLASS_REGISTRY (:data:SIMPLE_KIND_CLASS). Two kinds do not: stats (GroupComparisonConfig or CorrelationConfig) and blender (MontageConfig/VectorConfig/RegionConfig/SubcorticalConfig) -- the frozen PipelineConfig union in contracts/openapi.yaml lists all of them under the same kind/path parameter with no other discriminant on the object itself. This module resolves the ambiguity with a config["_type"] key (see :data:AMBIGUOUS_KIND_CLASSES), the same discriminator convention :mod:tit.config_io already uses for union-typed fields (ROIs, electrodes, montages) -- just applied here to the top-level kind/class choice. _type is not a field of any of these dataclasses, so :func:~tit.config_io.deserialize_config silently ignores it (documented behavior: "keys in data that are not fields of cls ... are ignored"), making this additive rather than a contract change. NO_SCHEMA_KINDS is empty as of the runners lane registering NiftiAverageConfig/NilearnConfig in :data:~tit.config_io.CONFIG_CLASS_REGISTRY -- kept as a (currently-empty) frozenset rather than removed outright since contracts/generated/config.schema.json has not been regenerated with these two $defs yet (a B4/F1b follow-up: docker exec tit-v3-spike simnibs_python dev/build_schema.py), so the desktop's generated types don't know about them even though this route already validates/plans them for real.

This is a documented design decision by this track (B3), not something contracts/openapi.yaml specifies -- flagged in the B3 report for F1a/the orchestrator in case a future contract revision wants an explicit discriminant instead.

KindNotConfigurable

Bases: ValueError

Raised by :func:cls_for for an unknown _type on an ambiguous kind.

cls_for

cls_for(kind: str, config_body: dict[str, Any]) -> type | None

Resolve kind (and, for an ambiguous kind, config_body["_type"]) to a dataclass.

Parameters

kind : str A :data:ALL_KINDS member (callers should 404 first for anything else). config_body : dict The request's raw config object (read for _type only when kind is ambiguous; never mutated).

Returns

type or None None for a :data:NO_SCHEMA_KINDS member (nothing to validate against yet).

Raises

KindNotConfigurable If kind is ambiguous and _type (or its default) does not name one of its known classes.

Source code in tit/server/routes/validate.py
def cls_for(kind: str, config_body: dict[str, Any]) -> type | None:
    """Resolve *kind* (and, for an ambiguous kind, ``config_body["_type"]``) to a dataclass.

    Parameters
    ----------
    kind : str
        A :data:`ALL_KINDS` member (callers should 404 first for anything else).
    config_body : dict
        The request's raw ``config`` object (read for ``_type`` only when *kind* is
        ambiguous; never mutated).

    Returns
    -------
    type or None
        ``None`` for a :data:`NO_SCHEMA_KINDS` member (nothing to validate against yet).

    Raises
    ------
    KindNotConfigurable
        If *kind* is ambiguous and ``_type`` (or its default) does not name one of its
        known classes.
    """
    from tit.config_io import resolve_config_class

    if kind in NO_SCHEMA_KINDS:
        return None
    if kind in SIMPLE_KIND_CLASS:
        return resolve_config_class(SIMPLE_KIND_CLASS[kind])

    choices = AMBIGUOUS_KIND_CLASSES.get(kind)
    if choices is not None:
        type_tag = config_body.get("_type") or AMBIGUOUS_KIND_DEFAULT[kind]
        registry_key = choices.get(type_tag)
        if registry_key is None:
            raise KindNotConfigurable(
                f"kind={kind!r} config._type must be one of {sorted(choices)}, "
                f"got {type_tag!r}"
            )
        return resolve_config_class(registry_key)

    raise KindNotConfigurable(f"unknown kind: {kind!r}")