We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Config, Logs and Untrusted Input step 4 of 16
Read Settings from Environment Variables
Your service reads five settings from environment variables when it starts.
Every environment variable is a string, so "9000" has to become an int
and "yes" has to become True. Do that once, in one function that returns
a typed Settings record. When several variables are wrong, report them all
at once, so the person deploying the service sees every mistake on the first
try.
load_settings({"SERVICE_NAME": "billing", "PORT": "9000", "DEBUG": "yes"})
# Settings(service_name='billing', port=9000, debug=True,
# environment='dev', timeout_seconds=5.0)
What to write
Fill in load_settings(env: Mapping[str, str]) -> Settings. Read only env,
not os.environ, and do not change it. Taking the mapping as an argument lets
the tests pass in any environment they like. Keep Settings, FieldError and
solve as they are, and add helpers if you like.
| Field | Variable | Conversion | If the variable is absent |
|---|---|---|---|
service_name |
SERVICE_NAME |
keep the string as it is | error, it is required |
port |
PORT |
int(raw) |
8080 |
debug |
DEBUG |
the words below | False |
environment |
ENVIRONMENT |
the names below | "dev" |
timeout_seconds |
TIMEOUT_SECONDS |
float(raw) |
5.0 |
- A default applies only when the variable is absent. A variable set to the
empty string is present. So
SERVICE_NAME=""gives an empty name, andPORT=""is an error, becauseint("")raisesValueError. - Keep
SERVICE_NAMEexactly as given, and add no range or non-empty checks.intandfloatalready accept a sign and spaces around the number. - For
DEBUG, strip spaces and lowercase the text. Then"1","true","yes"and"on"meanTrue, and"0","false","no"and"off"meanFalse. Anything else, including the empty string, is an error. Do not callbool(raw), because every non-empty string is truthy,"false"included. - For
ENVIRONMENT, strip spaces and lowercase the text, then accept only"dev","staging"or"prod". The result must have the typeEnvironment, and mypy checks that. One way is to loop over_ENVIRONMENTSand return the item that matches. - Ignore every other variable, such as
PATH.
Report every error
Check the variables in this order: SERVICE_NAME, PORT, DEBUG,
ENVIRONMENT, TIMEOUT_SECONDS. When one fails, create a
FieldError(name, detail), add it to a list and go on to the next. Make the
detail say what format you accept, such as "expected an integer". After the
last check, if the list is not empty, raise:
ExceptionGroup("invalid settings", errors)
Return Settings only when there were no errors.
Each FieldError records why it happened in __cause__:
- For a failed conversion, the cause is the
ValueErrorthat the conversion raised.intandfloatraise it for you. Make yourDEBUGandENVIRONMENTparsers raiseValueErrortoo. - A missing
SERVICE_NAMEhas no conversion error, so its cause staysNone.
raise FieldError(...) from exc would set the cause, but you are collecting
errors, not raising them yet. Set it yourself:
error = FieldError(name, detail)
error.__cause__ = exc
errors.append(error)
What the driver reports
Keep solve as it is. On success it returns "ok": True, the five fields,
and "frozen", which is true when the object has no __dict__. That holds
because Settings uses slots=True, so keep frozen=True, slots=True on it.
On failure it returns "ok": False, a "count" of the errors in the group,
and two lists. "fields" holds the variable names, and "causes"
holds the type name of each __cause__, so a missing variable adds
"NoneType". The driver sorts each list on its own, so the same position in
the two lists need not belong to the same error. With
SERVICE_NAME="billing", PORT="80x" and TIMEOUT_SECONDS="soon", it
returns:
{
"ok": False,
"fields": ["PORT", "TIMEOUT_SECONDS"],
"causes": ["ValueError", "ValueError"],
"count": 2,
}
Going further
The chained ValueError repeats the bad input, as in
invalid literal for int() with base 10: '80x'. If a setting ever holds a
secret, logging the whole exception group logs the secret too.
Your code, saved here
Sign in to access your accepted solutions and saved draft history. You can keep reading the lesson without an account.
Sign in to your accountStuck?
Python reference solution
Sign in to track your attempts toward unlocking the reference solution.