diff --git a/apps/backend/src/core/config.py b/apps/backend/src/core/config.py index 08ec1af5..f3b0e3a1 100644 --- a/apps/backend/src/core/config.py +++ b/apps/backend/src/core/config.py @@ -1,10 +1,10 @@ -import logging import os import secrets import warnings from functools import lru_cache from string import Template from typing import Annotated, Any, Literal +from urllib.parse import quote from data_manipulation.logging import configure_logging from pydantic import ( @@ -198,8 +198,8 @@ def all_cors_origins(self) -> list[str]: def POSTGRES_DATAFEEDER_URI(self) -> PostgresDsn: return PostgresDsn.build( scheme="postgresql+psycopg", - username=self.POSTGRES_DATAFEEDER_USER, - password=self.POSTGRES_DATAFEEDER_PASSWORD, + username=quote(self.POSTGRES_DATAFEEDER_USER, safe=""), + password=quote(self.POSTGRES_DATAFEEDER_PASSWORD, safe=""), host=self.POSTGRES_DATAFEEDER_HOST, port=self.POSTGRES_DATAFEEDER_PORT, path=self.POSTGRES_DATAFEEDER_DB, @@ -208,10 +208,13 @@ def POSTGRES_DATAFEEDER_URI(self) -> PostgresDsn: @computed_field # type: ignore[prop-decorator] @property def POSTGRES_DATA_URI(self) -> PostgresDsn: + # _set_data_db_defaults guarantees these are set by the time this is accessed. + assert self.POSTGRES_DATA_USER is not None + assert self.POSTGRES_DATA_PASSWORD is not None return PostgresDsn.build( scheme="postgresql+psycopg", - username=self.POSTGRES_DATA_USER, - password=self.POSTGRES_DATA_PASSWORD, + username=quote(self.POSTGRES_DATA_USER, safe=""), + password=quote(self.POSTGRES_DATA_PASSWORD, safe=""), host=self.POSTGRES_DATA_HOST, port=self.POSTGRES_DATA_PORT, path=self.POSTGRES_DATA_DB, @@ -261,11 +264,10 @@ def _enforce_non_default_secrets(self) -> Self: @field_validator("*", mode="after") @classmethod def expand_env_vars(cls, v: Any) -> Any: + # safe_substitute leaves invalid or unresolved placeholders (e.g. a literal + # "$" in a password) untouched instead of raising, unlike substitute(). if isinstance(v, str): - try: - v = Template(v).substitute(os.environ) - except KeyError as e: - logging.error(f"Environment variable {e} not set for value: {v}") + v = Template(v).safe_substitute(os.environ) return v diff --git a/pyproject.toml b/pyproject.toml index fc56136e..ee1447ab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,7 +32,7 @@ dev = [ [tool.uv] # Source of truth for the uv version. Keep in sync with the UV_VERSION ARG # default in apps/backend/Dockerfile and docker/Dockerfile.airflow. -required-version = "==0.9.15" +required-version = ">=0.9.15" [tool.uv.sources] geoservercloud = { git = "https://github.com/camptocamp/python-geoservercloud.git" }