Architecture#
sktime-cli is a thin, stateless layer over sktime: a Typer application, six
command groups, and a set of small internal modules that each own exactly one
concern. This page maps the repository, the modules, and the path a command
takes from argv to output.
Repository layout#
sktime_cli/
├── pyproject.toml # hatchling build, uv-managed, ruff config
├── src/sktime_cli/
│ ├── app.py # Typer root: global options, command registration
│ ├── __main__.py # `python -m sktime_cli`
│ ├── _errors.py # CliError + error-code → exit-code table
│ ├── _guard.py # the one exception handler; shared --format/--json options
│ ├── _output.py # format dispatch: human/agent/json/quiet emitters
│ ├── _cache.py # workspace dirs + registry disk cache
│ ├── _specs.py # "NaiveForecaster(sp=12)" → estimator instance
│ ├── _scitypes.py # which sktime scitypes `run` dispatches on, classified once
│ ├── _io.py # csv/parquet/json/.ts/.tsf/.arff read & write, --fh parsing
│ ├── _input.py # --data (path or dataset id) → the y/X a workflow needs
│ ├── _frames.py # flattens MultiIndex forecasts and segments into long form
│ ├── _datasets.py # dataset name resolution and loaders (builtin/ucr/tsf/fpp3)
│ ├── _models.py # model .zip save/load
│ └── commands/
│ ├── registry.py # search · describe · tags · types
│ ├── datasets.py # list · describe · load
│ ├── catalogues.py # list · get
│ ├── data.py # inspect · convert · split
│ ├── run.py # fit · predict · fit-predict · transform · detect · evaluate
│ ├── model.py # inspect
│ ├── metrics.py # list · score
│ ├── check.py # validate an object against sktime's API contract
│ └── env.py # version · env · doctor · cache info/clear
├── skills/sktime-cli/SKILL.md # agent-facing contract, copied into the wheel
├── tests/ # pytest + Typer CliRunner, network tests marked
├── scripts/scitype_coverage.py # regenerates the scitype coverage breakdown
└── docs/ # this documentation site
├── conf.py # Sphinx configuration
├── _ext/typer_cli.py # generates the CLI reference from the live app
└── assets/generate.py # renders the terminal captures
Dependency layering#
Modules import strictly downwards; _errors.py and _output.py are the
leaves. There are no import cycles.
graph TD
MAIN["__main__.py"] --> APP["app.py"]
APP --> CMD["commands/*"]
CMD --> GUARD["_guard.py"]
CMD --> SPECS["_specs.py"]
CMD --> SCI["_scitypes.py"]
CMD --> INPUT["_input.py"]
CMD --> FRAMES["_frames.py"]
CMD --> IO["_io.py"]
CMD --> DS["_datasets.py"]
CMD --> MODELS["_models.py"]
INPUT --> IO
INPUT --> DS
SPECS --> CACHE["_cache.py"]
DS --> CACHE
MODELS --> CACHE
CMD --> OUT["_output.py"]
GUARD --> OUT
GUARD --> ERR["_errors.py"]
CACHE --> ERR
IO --> ERR
INPUT --> ERR
SCI --> ERR
OUT --> ERR
_frames.py imports nothing from the package: it is pure pandas reshaping,
and a leaf like _errors.py and _output.py.
A deliberate import discipline keeps startup fast: the only third-party
module imported at module level is typer. Every sktime, pandas, numpy,
rich, and platformdirs import is function-local, so sktime-cli --help and
--version never pay sktime’s import cost.
Module responsibilities#
Module |
Owns |
Key entry points |
|---|---|---|
|
Typer root, global options ( |
|
|
The error vocabulary. Codes are append-only and part of the published contract |
|
|
The single exception choke point; every leaf command is decorated with it |
|
|
Rendering for all five formats and the stdout/stderr split |
|
|
Workspace resolution and the registry disk cache |
|
|
Spec strings → estimator instances; |
|
|
Which sktime scitypes |
|
|
File formats sktime doesn’t handle itself; index conventions; |
|
|
|
|
|
Reshaping sktime results that don’t survive a CSV round trip: MultiIndex probabilistic forecasts, |
|
|
Dataset ID grammar ( |
|
|
Model artifact |
|
|
Option surfaces and orchestration only, no domain logic |
one |
The life of a command#
Taking sktime-cli run fit "NaiveForecaster(sp=12)" --data airline.csv --model-out model.zip as the example:
Root callback (
app.py:_root) stores the global--format/--jsonchoice in_outputand the cache flags in_cache. Global state is fine here because every invocation is one short-lived process.Guard: the leaf command is wrapped by
_guard.handle_errors, so from this point every failure becomes a structured error on stderr plus a meaningful exit code (see the error model in Design).Spec engine:
_specs.build_estimatorparses the spec withast, resolvesNaiveForecasteragainst the cached registry, imports just that module, and evaluates the expression in a builtins-free namespace.Data loading:
_input.loadtreats--datapolymorphically: an existing path is read via_io.read_any(index conventions applied), anything else resolves as a dataset name via_datasets.resolve. It returns a neutral container;_input.as_endogenousthen decides which slot each object fills, from the estimator’s scitype rather than from the file.Fit dispatch: forecasters get
fit(y, X, fh); panel scitypes (classifier/regressor/clusterer) getfit(X, y).Artifact:
_models.save_modelwrites the.zip(defaulting into the workspacemodels/dir when--model-outis omitted).Output: a result record goes through
_output.emit_recordin whichever format was resolved: rich table (human), TSV (agent), one JSON document (json), or just the essential value (quiet).
Testing#
Tests run the CLI in-process through typer.testing.CliRunner (invoke
fixture in tests/conftest.py). A session-scoped autouse fixture points
SKTIME_CLI_HOME at a temp directory, so tests never touch the real cache.
This works because _cache.cli_home() reads the environment at call time,
not import time. Tests that download data are marked network and excluded
by default (addopts = "-m 'not network'" in pyproject.toml).