Errors¶
errors
¶
Error taxonomy + a single logger factory.
One base class so callers can catch everything from this package with
except ScaleForecastError. Subclasses are intentionally few and boring — add one
only when a caller would branch on it, not for decoration.
ConfigError
¶
Bases: ScaleForecastError
The run config is missing, malformed, or internally inconsistent.
DataError
¶
Bases: ScaleForecastError
The input series data violates the contract the config declares.
Raised by the pre-flight validator (validation.py) before any compute fans
out, so a shape problem in the source (missing column, gap in a series, wrong
freq) surfaces as one clear message naming the offender instead of thousands of
failed cells.
EngineError
¶
Bases: ScaleForecastError
A compute engine (Spark/Ray/BigQuery) failed to launch or collect results.
JobIdTaken
¶
Bases: EngineError
The platform already holds the job id this submit asked for.
Raised by every runtime submitter that names its own job, in place of the platform's raw
ALREADY_EXISTS. Two callers branch on it, which is why it exists rather than being a bare
message: registry.lifecycle.run_job stamps a distinct failure_reason token so the clash is
legible in the registry rather than only in the launcher's stdout, and the operator needs to be
told which of the two ways out applies.
Why it happens at all. The attempt counter is derived from the registry
(registry.jobs.next_job_attempt → MAX(attempt)), while uniqueness is enforced by the
platform. Those agree only as long as every job that reached a platform also wrote a
run_jobs row — and the emitted-command path (launch_plan.stage_run, the Airflow emitter)
hands a runnable command to something that is not the launcher. Observed live 2026-09-22 on a
--force re-run whose id had been created four hours earlier by a pasted command; see
docs/validation.md. launch_plan.stage_run now files an EMITTED row precisely so the
counter can see that launch, and job_launch walks forward past a taken id on the force path,
so reaching this error means both of those missed — a job created by something with no access
to this registry at all.
MissingExtraError
¶
Bases: ScaleForecastError, ImportError
A feature needs an optional-dependency extra that is not installed.
Both a ScaleForecastError and an ImportError, so except ScaleForecastError and the
idiomatic except ImportError each catch it. The message is the fix: the pip install
line naming the extra. Raised by require_extra and require_modules.
ModelError
¶
Bases: ScaleForecastError
A model failed to fit or predict.
Note: inside a worker cell this is captured into the CellResult, never raised
out of run_cell. It is raised only in direct/unit use.
RegistryError
¶
Bases: ScaleForecastError
A BigQuery/GCS registry operation failed (table, write, or artifact).
ScaleForecastError
¶
Bases: Exception
Base class for every error raised by this package.
configure_cli_logging
¶
Attach a root handler for a command-line entry point, unless the host already has one.
Every python -m scale_forecasting.<module> verb reports through _log.info — the resolved
run_id, the fanout, "submitted" — and the root logger ships with no handler and a WARNING
threshold, so as a library that is right and as a CLI it means the documented commands print
nothing at all (--dry-run, whose whole job is to say what a run would do, once exited 0 in
silence). Guarded on the root's handlers so a process that has already configured logging
(Airflow, a notebook) does not get a second copy of every line. SF_LOG_LEVEL overrides the
level.
Source code in src/scale_forecasting/errors.py
get_logger
¶
Return the package logger for name — __name__ at every call site.
No handler is attached here and propagation is left on, which is what lets an application
route this package's records with its own configuration. Until 1.0 each logger carried its own
StreamHandler with propagation off, and an Airflow task log never saw a line this package
wrote; the CLIs that relied on that handler now call configure_cli_logging instead.
Source code in src/scale_forecasting/errors.py
is_importable
¶
Is module installed? Probed without importing it, so this costs microseconds.
find_spec on a dotted name imports the parents, and raises ModuleNotFoundError when a
parent is absent (google.cloud.bigquery with no google at all) — which is just another
way of saying "not installed".
Source code in src/scale_forecasting/errors.py
require_extra
¶
Raise MissingExtraError naming the install command unless extra is installed.
The package has zero top-level google.* imports and a bare pip install scale-forecasting
is the pure forecasting layer (models, metrics, backtests, the playground). Everything that
reaches Google Cloud sits behind the gcp extra, so a core-only install that calls
main.run would otherwise die on ModuleNotFoundError: No module named 'google' somewhere
below the registry. This is called once at each entry point that needs the extra — main.run,
the Forecaster, the CLI mains, the plotting helpers — with purpose saying what the caller
was trying to do, so the failure is one sentence with the fix in it. It probes every module the
extra installs; a path that needs only some of them uses require_modules.
Source code in src/scale_forecasting/errors.py
require_modules
¶
Raise MissingExtraError naming extra's install line unless every module imports.
The narrow form of require_extra: a code path that imports two of an extra's modules asks for
those two, not for the whole extra. The distinction matters inside a running job — a Dataproc
or Vertex worker reaches BigQuery for the registry without ever holding the launch clients
(google.cloud.dataproc_v1, google.cloud.aiplatform) that the same [gcp] extra installs
on the launcher, and a whole-extra probe there would fail a healthy worker for a package it
never imports. registry.tables.bigquery_client is that caller.