* fix(settings): tell a malformed WD_* setting by its variable, without its valu…e
pydantic's account of a settings error names the field (`cache_disable`) rather than the
variable an operator set (`WD_CACHE_DISABLE`, `WD_AUTH__CEDA`), and repeats the value given,
which for `WD_AUTH__*` is a credential and for `WD_FSSPEC_CLIENT_KWARGS` may hold request
headers. Neither the REST API nor the CLI had anything better to show for it than that
traceback.
`describe_settings_error` turns a settings `ValidationError` into one line per problem: `WD_`
plus the error's location joined by `__`, upper-cased, and pydantic's message without its
"Value error, " prefix and without the input. A `SettingsError`, which pydantic-settings raises
when a dict, pair or nested setting is not valid JSON, is told by its variable as well.
`check_settings` builds the settings from the environment and `.env` alone and returns those
lines, empty when the settings are valid: the shared helper the REST API's startup and the CLI
use in the following commits.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(restapi): refuse to start on a malformed WD_* setting, naming the variable
The REST API did refuse a malformed setting at start, but only as a side effect: its module
builds `Info()` on import, and `Info.__init__` read `Settings().cache_dir`, so the import ended
in pydantic's traceback, naming the field and repeating the value. Nothing checked the settings
on purpose.
`Info.cache_dir` is now read when asked for, so the module imports whatever the settings, and
the app checks them as the server starts its lifespan. That holds however the app is started --
`wetterdienst restapi`, `uvicorn wetterdienst.ui.restapi:app`, `--reload` -- and covers `/mcp`,
which this app serves; the MCP server is not run on its own anywhere.
The check is a small ASGI middleware that answers the lifespan startup itself rather than a
failure raised from the app's lifespan context: Starlette reports such a failure to the server
as the formatted traceback, which uvicorn logs. Answering `lifespan.startup.failed` with the
lines from `check_settings` leaves the log with the variables and what is wrong with them,
without the values, followed by uvicorn's "Application startup failed. Exiting.", and the
server exits before it takes a connection.
A `.env` edited after the start still gives a bare 500 per request; a readable answer for that
is the issue's second item and not part of this change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(cli): tell a malformed WD_* setting by its variable and exit 1
Every command that builds settings ended in pydantic's traceback when the environment's were
malformed, naming the field and repeating the value; `history`, `issues` and `stripes values`
built them inside a catch-all, which logged that traceback or, for the stripes, printed the
whole dump as the error.
The command group now catches a settings error from any command, rebuilds the settings from
the environment alone, and if those fail tells them by `check_settings`: a line per variable
and what is wrong with it, without the value, exit status 1 -- the environment is not the
command line's to fix, so not a usage error. An error the environment alone does not
reproduce is the options' and is left as it is; `_build_settings` already tells those by their
option. `history`, `issues` and `stripes values` build or check the settings before their
catch-all, as the REST API's stripes endpoints do. `alerts` builds its settings inside a
`ValueError` handler too; moving them out is part of #2337 (GH-2322), and the group then tells
them the same way.
Commands that never read the settings, such as `radar`, still run with a malformed one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(settings): name a .env key that is no setting as written, not with a second WD_
pydantic-settings reports a `.env` key that names no setting as `extra_forbidden`, located at
the key as written there, prefix included. `describe_settings_error` put `WD_` in front of that
again, so a misspelt `WD_CACHE_DIABLE` was told as `WD_WD_CACHE_DIABLE`, and an unrelated
`OTHER_SECRET` as `WD_OTHER_SECRET`, names that exist nowhere. Such a key is now named as
written, as one that is not a wetterdienst setting.
That such a key breaks the settings at all, even without the prefix, is older and filed as
#2349.
The docstring now says what the lines leave out: the input pydantic echoes. A validator's own
message may still name what it refuses, a unit or a parameter name, which none of those on the
credentials or the headers does.
The new tests clear every ambient `WD_*` variable rather than only `WD_CACHE_DISABLE`, so one
set in the shell of whoever runs them does not decide the outcome.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(cli): tell only a settings error as the environment's
The command group took any `ValidationError` for the environment's whenever the environment's
settings failed on their own. An option can override a malformed variable (`--shape wide`
beside `WD_TS_SHAPE=bogus`), and the command then runs on; an error of another model it met
later was replaced by the variable's line and hidden. The group now tells only an error of the
settings themselves, or a `SettingsError`, as the environment's.
The changelog entry no longer claims more than the change does: the REST API's check runs when
the server starts the app's lifespan, so `--lifespan off` skips it, and `alerts` still builds
its settings inside a `ValueError` handler until #2337.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs(settings): say a .env key that is no setting is named upper-cased, not as written
pydantic-settings lower-cases the `.env` key it reports, so the line names the whole key,
prefix and all, upper-cased as variables are, not as it was written: `wd_cache_diable` in
`.env` is told as `WD_CACHE_DIABLE`. The comment and the test said "as written", and the
previous commit's message did too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs(restapi): say what the startup check does under --reload and what status it exits with
The middleware's docstring said the refusal holds under `--reload` and that the server exits.
Under `--reload` only the worker exits; uvicorn's reloader stays and starts a worker again on
the next change to a source file, not to `.env`. The changelog entry put `wetterdienst restapi`
among the commands that exit with status 1, where it exits with uvicorn's startup failure,
status 3, now that the module imports and uvicorn runs the check.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(restapi): refuse to start when the settings fail other than by a validation error
The startup check caught only a `ValidationError` or `SettingsError`. A validator that fails in
its own code raises something else: `WD_TS_GEO_STATION_DISTANCE=5` makes a key validator raise
`TypeError` (#2353). That escaped the middleware, and uvicorn, in its default lifespan mode
`auto`, took the exception for a lifespan the app does not support, logged "Application startup
complete" and served, without the app's own or the MCP session manager's lifespan. Before the
check, the `Info()` on import failed on it and the server never started, so this was a
regression of the change that made `Info.cache_dir` lazy.
Any other failure is now refused the same way, with its type and message. The lifespan tests
run in mode `auto`, which `wetterdienst restapi` uses, rather than `on`.
The changelog entry no longer says every other CLI command exits 1: those that never read the
settings, such as `radar`, still run.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(restapi): tell a settings failure that is not pydantic's by its type alone
The previous commit refused any failure of the settings build at startup with its type and
message. That message is not pydantic's, so nothing keeps the value a validator was given out
of it, a key or a value in the environment; it is now told by its type alone.
`describe_settings_error` becomes private: it assumes the settings were built from the
environment and `.env` alone, where a key no setting has can only have come from `.env`, and
`check_settings` is the one caller that builds them so. The changelog entry says what the lines
leave out, pydantic's echo of the value, as a validator's own message may still name the unit
or parameter it refuses.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(cli): expect alerts to tell a malformed WD_* setting by its variable
#2337 (GH-2322) moved `alerts`' settings out of its `ValueError` handler and let their error
propagate as it is, as `values` did. With the command group from this branch, that error is now
told by the variable with exit status 1, so `alerts` joins the commands the variable test covers,
and its own environment test expects that line rather than the raw `ValidationError`. The
changelog entry, resolved in the merge, no longer sets `alerts` aside.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>