Review round 1 of the refusal contract: it claimed more than it held. - The other reason carriers get their Literal too (BlockedUrl, ClipError, FolderError, the playlist and sign-in refusal helpers), so the type checker refuses a reason passed by variable or an unknown literal, and the contract reads one shape. - A literal reason in a detail dict built anywhere counts as a reason of its code; a code the scan cannot read, a refused() whose reason it cannot read, and a new reason passed by variable each fail the contract until listed with what holds them. - A sign-in refusal key the backend no longer sends is caught too. - localizeDetail's numbers (download cap in GB, upload cap in bytes) and the shared-sentence fallbacks are tested in both languages. - One Literal parser in the contract; a stale comment fixed.
41 lines
2.2 KiB
Python
41 lines
2.2 KiB
Python
"""A request the app refuses, said in a way the UI can put in the reader's language.
|
|
|
|
Every refusal the API answers carries a structured `detail` — `{code, reason, …params}` — and the client
|
|
translates it (`lib/api/refusalText.ts` `localizeDetail`). A bare English string reached the Hungarian UI
|
|
verbatim: the blocking error dialog, and every inline surface that printed `HttpError.detail`, showed
|
|
whatever sentence the route happened to hand-write. `refused` is the generic family
|
|
(`errors.refused.<reason>`, the params interpolated into the sentence); the older specific codes
|
|
(`not_found`, `youtube`, `playlist`, the admin refusals, …) stay, since each is already translated.
|
|
|
|
A test (`test_http_details.py`) reads every `HTTPException` in the app and fails on a string detail,
|
|
and the frontend's contract test (`i18n/backendIds.contract.test.ts`) requires every `code` a detail
|
|
carries to be in one record of `lib/api/refusalCodes.ts`, and every reason it can be sent with — a
|
|
literal, or a `Literal` type when it travels in a variable — to have its sentence in both languages.
|
|
A code or reason that scan cannot read fails it until it is listed with what holds it. So the rule is
|
|
enforced, not described.
|
|
"""
|
|
|
|
from fastapi import HTTPException
|
|
|
|
|
|
def refused(status: int, reason: str, **params: object) -> HTTPException:
|
|
"""The refusal to raise: `raise refused(409, "view_name_taken", name=name)`. `params` travel in
|
|
the detail for the sentence to interpolate (`{{name}}`); keep them to values the reader may see."""
|
|
return HTTPException(status_code=status, detail={"code": "refused", "reason": reason, **params})
|
|
|
|
|
|
# `sysconfig.InvalidSetting.reason` → the refusal it answers as. A dict of literals, so the frontend's
|
|
# contract test can read every reason this can produce.
|
|
_SETTING_REASONS = {
|
|
"not_a_number": "setting_not_a_number",
|
|
"too_low": "setting_too_low",
|
|
"too_high": "setting_too_high",
|
|
"not_a_choice": "setting_not_a_choice",
|
|
"no_encryption_key": "no_encryption_key",
|
|
}
|
|
|
|
|
|
def setting_refused(exc) -> HTTPException:
|
|
"""A value the admin entered that `sysconfig.set_value` refused (`exc`: its `InvalidSetting`)."""
|
|
return refused(400, _SETTING_REASONS[exc.reason], key=exc.key, **exc.params)
|