Stop the start with a clear message for a bad setting
A bad UPDATE_INTERVAL_HOURS or LOG_LEVEL no longer ends in a traceback. Warn about a CWA_PASSWORD without a CWA_USERNAME.
This commit is contained in:
+20
-2
@@ -27,6 +27,22 @@ def _optional_path(name: str) -> Path | None:
|
|||||||
return Path(value).absolute() if value else None
|
return Path(value).absolute() if value else None
|
||||||
|
|
||||||
|
|
||||||
|
# A bad value stops the start with one clear line, not with a traceback.
|
||||||
|
def _number(name: str, default: str) -> float:
|
||||||
|
value = os.getenv(name, default).strip()
|
||||||
|
try:
|
||||||
|
return float(value)
|
||||||
|
except ValueError:
|
||||||
|
raise SystemExit(f"{name} must be a number, not {value!r}") from None
|
||||||
|
|
||||||
|
|
||||||
|
def _choice(name: str, default: str, choices: tuple[str, ...]) -> str:
|
||||||
|
value = os.getenv(name, default).strip().upper()
|
||||||
|
if value not in choices:
|
||||||
|
raise SystemExit(f"{name} must be one of {', '.join(choices)}, not {value!r}")
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
def _base_path(value: str) -> str:
|
def _base_path(value: str) -> str:
|
||||||
path = value.strip().strip("/")
|
path = value.strip().strip("/")
|
||||||
return f"/{path}" if path else ""
|
return f"/{path}" if path else ""
|
||||||
@@ -37,7 +53,7 @@ PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
|||||||
CONFIG_DIR = _path("CONFIG_DIR", "/config")
|
CONFIG_DIR = _path("CONFIG_DIR", "/config")
|
||||||
INGEST_DIR = _optional_path("INGEST_DIR")
|
INGEST_DIR = _optional_path("INGEST_DIR")
|
||||||
TMP_DIR = _path("TMP_DIR", "/tmp/bindery")
|
TMP_DIR = _path("TMP_DIR", "/tmp/bindery")
|
||||||
LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO").strip().upper()
|
LOG_LEVEL = _choice("LOG_LEVEL", "INFO", ("DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"))
|
||||||
# A path prefix, such as /bindery, for a reverse proxy that forwards the full path.
|
# A path prefix, such as /bindery, for a reverse proxy that forwards the full path.
|
||||||
URL_BASE = _base_path(os.getenv("URL_BASE", ""))
|
URL_BASE = _base_path(os.getenv("URL_BASE", ""))
|
||||||
|
|
||||||
@@ -49,7 +65,7 @@ CWA_PASSWORD = os.getenv("CWA_PASSWORD", "")
|
|||||||
CWA_DB_PATH = _optional_path("CWA_DB_PATH") # Calibre-Web app.db, for logins
|
CWA_DB_PATH = _optional_path("CWA_DB_PATH") # Calibre-Web app.db, for logins
|
||||||
CALIBRE_DB_PATH = _optional_path("CALIBRE_DB_PATH") # calibre metadata.db, for book IDs
|
CALIBRE_DB_PATH = _optional_path("CALIBRE_DB_PATH") # calibre metadata.db, for book IDs
|
||||||
|
|
||||||
UPDATE_INTERVAL_HOURS = float(os.getenv("UPDATE_INTERVAL_HOURS", "24"))
|
UPDATE_INTERVAL_HOURS = _number("UPDATE_INTERVAL_HOURS", "24")
|
||||||
DEFAULT_LOCKED_FIELDS = _list("DEFAULT_LOCKED_FIELDS", "title")
|
DEFAULT_LOCKED_FIELDS = _list("DEFAULT_LOCKED_FIELDS", "title")
|
||||||
|
|
||||||
# Logins: none, local, cwa or proxy. An API key works in every mode.
|
# Logins: none, local, cwa or proxy. An API key works in every mode.
|
||||||
@@ -89,6 +105,8 @@ def prepare() -> None:
|
|||||||
if not PERSONAL_INI.exists():
|
if not PERSONAL_INI.exists():
|
||||||
shutil.copy(DEFAULT_PERSONAL_INI, PERSONAL_INI)
|
shutil.copy(DEFAULT_PERSONAL_INI, PERSONAL_INI)
|
||||||
logger.info(f"Created the default FanFicFare configuration at {PERSONAL_INI}")
|
logger.info(f"Created the default FanFicFare configuration at {PERSONAL_INI}")
|
||||||
|
if CWA_PASSWORD and not CWA_USERNAME:
|
||||||
|
logger.warning("CWA_PASSWORD is set, but CWA_USERNAME is not. Bindery sends no login to CWA.")
|
||||||
if INGEST_DIR and not (CWA_URL or CALIBRE_DB_PATH):
|
if INGEST_DIR and not (CWA_URL or CALIBRE_DB_PATH):
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"INGEST_DIR is set, but CWA_URL and CALIBRE_DB_PATH are not. Bindery cannot find the library book "
|
"INGEST_DIR is set, but CWA_URL and CALIBRE_DB_PATH are not. Bindery cannot find the library book "
|
||||||
|
|||||||
@@ -155,3 +155,36 @@ def test_prepare_warns_about_an_ingest_folder_without_a_library(monkeypatch, tmp
|
|||||||
with caplog.at_level("WARNING"):
|
with caplog.at_level("WARNING"):
|
||||||
config.prepare()
|
config.prepare()
|
||||||
assert ("each update adds a new book" in caplog.text) is warns
|
assert ("each update adds a new book" in caplog.text) is warns
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("name", "value", "error"),
|
||||||
|
[
|
||||||
|
("UPDATE_INTERVAL_HOURS", "daily", "UPDATE_INTERVAL_HOURS must be a number, not 'daily'"),
|
||||||
|
("LOG_LEVEL", "loud", "LOG_LEVEL must be one of DEBUG, INFO, WARNING, ERROR, CRITICAL, not 'LOUD'"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_a_bad_value_stops_the_start_with_a_clear_message(monkeypatch, reload_config, name, value, error):
|
||||||
|
monkeypatch.setenv(name, value)
|
||||||
|
with pytest.raises(SystemExit) as stop:
|
||||||
|
reload_config()
|
||||||
|
assert str(stop.value) == error
|
||||||
|
|
||||||
|
|
||||||
|
def test_log_level_in_lower_case(monkeypatch, reload_config):
|
||||||
|
monkeypatch.setenv("LOG_LEVEL", " debug ")
|
||||||
|
reload_config()
|
||||||
|
assert config.LOG_LEVEL == "DEBUG"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(("username", "warns"), [("", True), ("bindery", False)])
|
||||||
|
def test_prepare_warns_about_a_cwa_password_without_a_user_name(monkeypatch, tmp_path, caplog, username, warns):
|
||||||
|
monkeypatch.setattr(config, "CWA_URL", "https://cwa.invalid")
|
||||||
|
monkeypatch.setattr(config, "CWA_USERNAME", username)
|
||||||
|
monkeypatch.setattr(config, "CWA_PASSWORD", "secret")
|
||||||
|
for name, folder in [("CONFIG_DIR", "config"), ("UPLOAD_DIR", "uploads"), ("TMP_DIR", "tmp"), ("INGEST_DIR", "in")]:
|
||||||
|
monkeypatch.setattr(config, name, tmp_path / folder)
|
||||||
|
monkeypatch.setattr(config, "PERSONAL_INI", tmp_path / "config" / "personal.ini")
|
||||||
|
with caplog.at_level("WARNING"):
|
||||||
|
config.prepare()
|
||||||
|
assert ("CWA_PASSWORD is set" in caplog.text) is warns
|
||||||
|
|||||||
Reference in New Issue
Block a user