diff --git a/mddocs/docs/changelog/next_release/413.improvement.md b/mddocs/docs/changelog/next_release/413.improvement.md new file mode 100644 index 00000000..0c099f9f --- /dev/null +++ b/mddocs/docs/changelog/next_release/413.improvement.md @@ -0,0 +1 @@ +Hide auth provider secrets and database password from logs. diff --git a/pyproject.toml b/pyproject.toml index cad5fae2..132772c5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,6 +43,7 @@ dependencies = [ "asgi-correlation-id~=5.0.0", "uuid6~=2025.0.1", "celery~=5.6.0", + "devtools~=0.12.2", "psycopg2-binary~=2.9.11", ] dynamic = ["version"] diff --git a/syncmaster/db/factory.py b/syncmaster/db/factory.py index e0bf2542..9bcf54f5 100644 --- a/syncmaster/db/factory.py +++ b/syncmaster/db/factory.py @@ -3,15 +3,15 @@ from sqlalchemy.ext.asyncio import ( AsyncSession, - async_engine_from_config, async_sessionmaker, + create_async_engine, ) from syncmaster.server.settings import DatabaseSettings def create_session_factory(settings: DatabaseSettings) -> async_sessionmaker[AsyncSession]: - engine = async_engine_from_config(settings.model_dump(), prefix="") + engine = create_async_engine(url=str(settings.url), **settings.model_dump(exclude={"url"})) return async_sessionmaker( bind=engine, class_=AsyncSession, diff --git a/syncmaster/db/migrations/env.py b/syncmaster/db/migrations/env.py index 99518ba5..4999df35 100644 --- a/syncmaster/db/migrations/env.py +++ b/syncmaster/db/migrations/env.py @@ -34,7 +34,7 @@ class MigrationAppSettings(BaseSettings): if not config.get_main_option("sqlalchemy.url"): # read application settings only if sqlalchemy.url is not being passed via cli arguments # TODO: remove settings object creating during import - config.set_main_option("sqlalchemy.url", MigrationAppSettings().database.url) + config.set_main_option("sqlalchemy.url", str(MigrationAppSettings().database.url)) target_metadata = ( Base.metadata, diff --git a/syncmaster/scheduler/__init__.py b/syncmaster/scheduler/__init__.py index 0e9d24bf..38a17672 100644 --- a/syncmaster/scheduler/__init__.py +++ b/syncmaster/scheduler/__init__.py @@ -8,6 +8,6 @@ def celery_factory(settings: SchedulerAppSettings) -> Celery: return Celery( __name__, - broker=settings.broker.url, + broker=str(settings.broker.url), backend="db+" + settings.database.sync_url, ) diff --git a/syncmaster/scheduler/utils.py b/syncmaster/scheduler/utils.py index f4979770..0f2ad796 100644 --- a/syncmaster/scheduler/utils.py +++ b/syncmaster/scheduler/utils.py @@ -15,7 +15,7 @@ @contextlib.asynccontextmanager async def get_async_engine(settings: Settings) -> AsyncGenerator[AsyncEngine, None]: - engine = create_async_engine(url=settings.database.url) + engine = create_async_engine(url=str(settings.database.url), **settings.database.model_dump(exclude={"url"})) try: yield engine finally: diff --git a/syncmaster/server/__init__.py b/syncmaster/server/__init__.py index 90ae71da..01bc6fe5 100644 --- a/syncmaster/server/__init__.py +++ b/syncmaster/server/__init__.py @@ -30,7 +30,7 @@ def celery_factory(settings: Settings) -> Celery: return Celery( __name__, - broker=settings.broker.url, + broker=str(settings.broker.url), backend="db+" + settings.database.sync_url, ) diff --git a/syncmaster/server/providers/auth/dummy_provider.py b/syncmaster/server/providers/auth/dummy_provider.py index b60be9b2..429d9123 100644 --- a/syncmaster/server/providers/auth/dummy_provider.py +++ b/syncmaster/server/providers/auth/dummy_provider.py @@ -2,10 +2,10 @@ # SPDX-License-Identifier: Apache-2.0 import logging -from pprint import pformat from time import time from typing import Any +from devtools import pformat from fastapi import FastAPI, Request from syncmaster.db.models import User diff --git a/syncmaster/server/providers/auth/keycloak_provider.py b/syncmaster/server/providers/auth/keycloak_provider.py index 84be040f..757da598 100644 --- a/syncmaster/server/providers/auth/keycloak_provider.py +++ b/syncmaster/server/providers/auth/keycloak_provider.py @@ -4,6 +4,7 @@ import time from typing import Any, NoReturn +from devtools import pformat from fastapi import FastAPI, Request from jwcrypto import jwk from jwcrypto.common import JWException @@ -37,7 +38,7 @@ def __init__(self, settings: KeycloakAuthProviderSettings) -> None: @classmethod def setup(cls, app: FastAPI) -> FastAPI: settings = KeycloakAuthProviderSettings.model_validate(app.state.settings.auth.model_dump(exclude={"provider"})) - log.info("Using %s provider with settings:\n%s", cls.__name__, settings) + log.info("Using %s provider with settings:\n%s", cls.__name__, pformat(settings)) app.state.auth_provider = cls(settings=settings) app.add_middleware( diff --git a/syncmaster/server/providers/auth/oauth2_gateway_provider.py b/syncmaster/server/providers/auth/oauth2_gateway_provider.py index 7cf22d76..941ff612 100644 --- a/syncmaster/server/providers/auth/oauth2_gateway_provider.py +++ b/syncmaster/server/providers/auth/oauth2_gateway_provider.py @@ -3,6 +3,7 @@ import logging from typing import Any +from devtools import pformat from fastapi import FastAPI, Request from keycloak import KeycloakOpenID, KeycloakOperationError @@ -32,7 +33,7 @@ def setup(cls, app: FastAPI) -> FastAPI: settings = OAuth2GatewayProviderSettings.model_validate( app.state.settings.auth.model_dump(exclude={"provider"}), ) - log.info("Using %s provider with settings:\n%s", cls.__name__, settings) + log.info("Using %s provider with settings:\n%s", cls.__name__, pformat(settings)) app.state.auth_provider = cls(settings=settings) return app diff --git a/syncmaster/server/scripts/manage_superusers.py b/syncmaster/server/scripts/manage_superusers.py index b3567569..717043c8 100755 --- a/syncmaster/server/scripts/manage_superusers.py +++ b/syncmaster/server/scripts/manage_superusers.py @@ -119,7 +119,7 @@ async def main(args: argparse.Namespace, session: AsyncSession) -> None: settings = SuperuserAppSettings() setup_logging(settings.logging) - engine = create_async_engine(settings.database.url) + engine = create_async_engine(*settings.database.model_dump(mode="json")) SessionLocal = async_sessionmaker(autocommit=False, autoflush=False, bind=engine, class_=AsyncSession) parser = create_parser() args = parser.parse_args() diff --git a/syncmaster/server/settings/auth/__init__.py b/syncmaster/server/settings/auth/__init__.py index c4a97f73..ec1d6143 100644 --- a/syncmaster/server/settings/auth/__init__.py +++ b/syncmaster/server/settings/auth/__init__.py @@ -1,7 +1,12 @@ # SPDX-FileCopyrightText: 2023-present MTS PJSC # SPDX-License-Identifier: Apache-2.0 -from pydantic import BaseModel, ConfigDict, Field, ImportString +from typing import TYPE_CHECKING + +from pydantic import BaseModel, ConfigDict, Field, ImportString, field_validator + +if TYPE_CHECKING: + from syncmaster.server.providers.auth.base_provider import AuthProvider class AuthSettings(BaseModel): @@ -28,3 +33,17 @@ class AuthSettings(BaseModel): ) model_config = ConfigDict(extra="allow") + + @field_validator("provider", mode="after") + @classmethod + def _validate_provider(cls, value: type) -> "type[AuthProvider]": + from syncmaster.server.providers.auth.base_provider import AuthProvider # noqa: PLC0415 + + if not issubclass(value, AuthProvider): + msg = f"Class {value} is not a subclass of {AuthProvider}" + raise TypeError(msg) + return value + + # prevent leaking provider secrets + def __repr_args__(self): + return [("provider", self.provider)] diff --git a/syncmaster/settings/broker.py b/syncmaster/settings/broker.py index c5481268..adef5889 100644 --- a/syncmaster/settings/broker.py +++ b/syncmaster/settings/broker.py @@ -1,6 +1,26 @@ # SPDX-FileCopyrightText: 2023-present MTS PJSC # SPDX-License-Identifier: Apache-2.0 -from pydantic import BaseModel, ConfigDict, Field +from typing import Annotated +from urllib.parse import urlsplit + +from pydantic import AfterValidator, AnyUrl, BaseModel, ConfigDict, Field, UrlConstraints +from sqlalchemy import make_url + + +def validate_url(value: AnyUrl): + split = urlsplit(str(value)) + if not split.username or not split.password: + msg = "RabbitMQ URL must contain username and password" + raise ValueError(msg) + + return value + + +RabbitMQURL = Annotated[ + AnyUrl, + UrlConstraints(allowed_schemes=["amqp"], host_required=True, preserve_empty_path=False), + AfterValidator(validate_url), +] class RabbitMQSettings(BaseModel): @@ -20,10 +40,20 @@ class RabbitMQSettings(BaseModel): ``` """ - url: str = Field( + url: RabbitMQURL = Field( description=( "RabbitMQ connection URL.\n\nSee the [RabbitMQ documentation](https://www.rabbitmq.com/uri-spec.html) " ), ) model_config = ConfigDict(extra="allow") + + def __repr_args__(self): + safe_url = make_url(str(self.url)).render_as_string( + hide_password=True, + ) + extra = super().__repr_args__() + return [ + ("url", safe_url), + *[item for item in extra if item[0] != "url"], + ] diff --git a/syncmaster/settings/database.py b/syncmaster/settings/database.py index f6692b31..594d170b 100644 --- a/syncmaster/settings/database.py +++ b/syncmaster/settings/database.py @@ -1,9 +1,31 @@ # SPDX-FileCopyrightText: 2023-present MTS PJSC # SPDX-License-Identifier: Apache-2.0 import textwrap -from urllib.parse import urlparse, urlunparse +from typing import Annotated +from urllib.parse import urlparse, urlsplit -from pydantic import BaseModel, ConfigDict, Field +from pydantic import AfterValidator, BaseModel, ConfigDict, Field, PostgresDsn, UrlConstraints +from sqlalchemy import make_url + + +def validate_url(value: PostgresDsn): + if not value.path or len(value.path) <= 1: + msg = "Database URL must contain database name" + raise ValueError(msg) + + split = urlsplit(str(value)) + if not split.username or not split.password: + msg = "Database URL must contain username and password" + raise ValueError(msg) + + return value + + +PostgresURL = Annotated[ + PostgresDsn, + UrlConstraints(allowed_schemes=["postgresql+asyncpg"], host_required=True), + AfterValidator(validate_url), +] class DatabaseSettings(BaseModel): @@ -27,7 +49,7 @@ class DatabaseSettings(BaseModel): ``` """ - url: str = Field( + url: PostgresDsn = Field( description=textwrap.dedent( """ Database connection URL. @@ -43,10 +65,17 @@ class DatabaseSettings(BaseModel): @property def sync_url(self) -> str: - parsed_url = urlparse(self.url) - # replace '+asyncpg' with '+psycopg2' in the scheme - used by celery - scheme = parsed_url.scheme.replace("+asyncpg", "+psycopg2") - sync_parsed_url = parsed_url._replace(scheme=scheme) - return urlunparse(sync_parsed_url) + schema = urlparse(str(self.url)).scheme + return str(self.url).replace(schema, "postgresql+psycopg2") model_config = ConfigDict(extra="allow") + + def __repr_args__(self): + safe_url = make_url(str(self.url)).render_as_string( + hide_password=True, + ) + extra = super().__repr_args__() + return [ + ("url", safe_url), + *[item for item in extra if item[0] != "url"], + ] diff --git a/syncmaster/worker/__init__.py b/syncmaster/worker/__init__.py index fb5421ff..5ab04aa8 100644 --- a/syncmaster/worker/__init__.py +++ b/syncmaster/worker/__init__.py @@ -9,7 +9,7 @@ def celery_factory(settings: WorkerAppSettings) -> Celery: return Celery( __name__, - broker=settings.broker.url, + broker=str(settings.broker.url), backend="db+" + settings.database.sync_url, task_cls=WorkerTask, imports=[ diff --git a/tests/conftest.py b/tests/conftest.py index 01cee090..a4246128 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -87,7 +87,7 @@ def test_settings(): def alembic_config(settings: Settings) -> AlembicConfig: alembic_cfg = AlembicConfig(PROJECT_PATH / "syncmaster" / "db" / "alembic.ini") alembic_cfg.set_main_option("script_location", os.fspath(PROJECT_PATH / "syncmaster/db/migrations")) - alembic_cfg.set_main_option("sqlalchemy.url", settings.database.url) + alembic_cfg.set_main_option("sqlalchemy.url", str(settings.database.url)) return alembic_cfg @@ -98,7 +98,7 @@ async def async_engine(settings: Settings, alembic_config: AlembicConfig): await run_async_migrations(alembic_config, Base.metadata, "-1", "down") await run_async_migrations(alembic_config, Base.metadata, "head") - engine = create_async_engine(settings.database.url) + engine = create_async_engine(url=str(settings.database.url), **settings.database.model_dump(exclude={"url"})) yield engine await engine.dispose() diff --git a/tests/test_unit/test_settings/test_scheduler_settings.py b/tests/test_unit/test_settings/test_scheduler_settings.py index 62d970de..3cf7ed5d 100644 --- a/tests/test_unit/test_settings/test_scheduler_settings.py +++ b/tests/test_unit/test_settings/test_scheduler_settings.py @@ -26,7 +26,7 @@ def test_scheduler_settings_are_loaded_from_default_yaml_file( dedent( """\ database: - url: "postgresql+asyncpg://user:password'#[value]@localhost:5432/syncmaster" + url: "postgresql+asyncpg://user:password@localhost:5432/syncmaster" broker: url: amqp://user:password@localhost:5672/ encryption: @@ -40,8 +40,8 @@ def test_scheduler_settings_are_loaded_from_default_yaml_file( settings = SchedulerAppSettings() - assert settings.database.url == "postgresql+asyncpg://user:password'#[value]@localhost:5432/syncmaster" - assert settings.broker.url == "amqp://user:password@localhost:5672/" + assert str(settings.database.url) == "postgresql+asyncpg://user:password@localhost:5432/syncmaster" + assert str(settings.broker.url) == "amqp://user:password@localhost:5672/" assert settings.encryption.secret_key == "secret_key" assert settings.scheduler.transfer_fetching_timeout_seconds == 200 @@ -56,9 +56,9 @@ def test_scheduler_settings_yaml_file_overrides_environment( dedent( """\ database: - url: postgresql+asyncpg://yaml@localhost:5432/syncmaster + url: postgresql+asyncpg://yaml:yaml@localhost:5432/syncmaster broker: - url: amqp://yaml@localhost:5672/ + url: amqp://yaml:yaml@localhost:5672/ encryption: secret_key: "yaml_secret_key" scheduler: @@ -70,19 +70,19 @@ def test_scheduler_settings_yaml_file_overrides_environment( monkeypatch.setenv("SYNCMASTER_CONFIG_FILE", str(config_path)) monkeypatch.setenv( "SYNCMASTER__DATABASE__URL", - "postgresql+asyncpg://env@localhost:5432/syncmaster", + "postgresql+asyncpg://env:env@localhost:5432/syncmaster", ) monkeypatch.setenv( "SYNCMASTER__BROKER__URL", - "amqp://env@localhost:5672/", + "amqp://env:env@localhost:5672/", ) monkeypatch.setenv("SYNCMASTER__ENCRYPTION__SECRET_KEY", "env_secret_key") monkeypatch.setenv("SYNCMASTER__SCHEDULER__transfer_fetching_timeout_seconds", "300") settings = SchedulerAppSettings() - assert settings.database.url == "postgresql+asyncpg://yaml@localhost:5432/syncmaster" - assert settings.broker.url == "amqp://yaml@localhost:5672/" + assert str(settings.database.url) == "postgresql+asyncpg://yaml:yaml@localhost:5432/syncmaster" + assert str(settings.broker.url) == "amqp://yaml:yaml@localhost:5672/" assert settings.encryption.secret_key == "yaml_secret_key" assert settings.scheduler.transfer_fetching_timeout_seconds == 200 @@ -95,18 +95,18 @@ def test_scheduler_settings_can_be_loaded_from_environment_without_yaml_file( monkeypatch.setenv("SYNCMASTER_CONFIG_FILE", str(tmp_path / "missing.yml")) monkeypatch.setenv( "SYNCMASTER__DATABASE__URL", - "postgresql+asyncpg://env@localhost:5432/syncmaster", + "postgresql+asyncpg://env:env@localhost:5432/syncmaster", ) monkeypatch.setenv( "SYNCMASTER__BROKER__URL", - "amqp://env@localhost:5672/", + "amqp://env:env@localhost:5672/", ) monkeypatch.setenv("SYNCMASTER__ENCRYPTION__SECRET_KEY", "env_secret_key") monkeypatch.setenv("SYNCMASTER__SCHEDULER__transfer_fetching_timeout_seconds", "300") settings = SchedulerAppSettings() - assert settings.database.url == "postgresql+asyncpg://env@localhost:5432/syncmaster" - assert settings.broker.url == "amqp://env@localhost:5672/" + assert str(settings.database.url) == "postgresql+asyncpg://env:env@localhost:5432/syncmaster" + assert str(settings.broker.url) == "amqp://env:env@localhost:5672/" assert settings.encryption.secret_key == "env_secret_key" assert settings.scheduler.transfer_fetching_timeout_seconds == 300 diff --git a/tests/test_unit/test_settings/test_server_settings.py b/tests/test_unit/test_settings/test_server_settings.py index ee1e6931..39de679b 100644 --- a/tests/test_unit/test_settings/test_server_settings.py +++ b/tests/test_unit/test_settings/test_server_settings.py @@ -26,7 +26,7 @@ def test_server_settings_are_loaded_from_default_yaml_file( dedent( """\ database: - url: "postgresql+asyncpg://user:password'#[value]@localhost:5432/syncmaster" + url: "postgresql+asyncpg://user:password@localhost:5432/syncmaster" broker: url: amqp://user:password@localhost:5672/ encryption: @@ -47,8 +47,8 @@ def test_server_settings_are_loaded_from_default_yaml_file( settings = ServerAppSettings() - assert settings.database.url == "postgresql+asyncpg://user:password'#[value]@localhost:5432/syncmaster" - assert settings.broker.url == "amqp://user:password@localhost:5672/" + assert str(settings.database.url) == "postgresql+asyncpg://user:password@localhost:5432/syncmaster" + assert str(settings.broker.url) == "amqp://user:password@localhost:5672/" assert settings.encryption.secret_key == "secret_key" assert settings.server.debug is True assert settings.server.cors.dict() == { @@ -71,9 +71,9 @@ def test_server_settings_yaml_file_overrides_environment( dedent( """\ database: - url: postgresql+asyncpg://yaml@localhost:5432/syncmaster + url: postgresql+asyncpg://yaml:yaml@localhost:5432/syncmaster broker: - url: amqp://yaml@localhost:5672/ + url: amqp://yaml:yaml@localhost:5672/ encryption: secret_key: "yaml_secret_key" server: @@ -87,11 +87,11 @@ def test_server_settings_yaml_file_overrides_environment( monkeypatch.setenv("SYNCMASTER_CONFIG_FILE", str(config_path)) monkeypatch.setenv( "SYNCMASTER__DATABASE__URL", - "postgresql+asyncpg://env@localhost:5432/syncmaster", + "postgresql+asyncpg://env:env@localhost:5432/syncmaster", ) monkeypatch.setenv( "SYNCMASTER__BROKER__URL", - "amqp://env@localhost:5672/", + "amqp://env:env@localhost:5672/", ) monkeypatch.setenv("SYNCMASTER__ENCRYPTION__SECRET_KEY", "env_secret_key") monkeypatch.setenv("SYNCMASTER__SERVER__DEBUG", "true") @@ -102,8 +102,8 @@ def test_server_settings_yaml_file_overrides_environment( settings = ServerAppSettings() - assert settings.database.url == "postgresql+asyncpg://yaml@localhost:5432/syncmaster" - assert settings.broker.url == "amqp://yaml@localhost:5672/" + assert str(settings.database.url) == "postgresql+asyncpg://yaml:yaml@localhost:5432/syncmaster" + assert str(settings.broker.url) == "amqp://yaml:yaml@localhost:5672/" assert settings.encryption.secret_key == "yaml_secret_key" assert settings.server.debug is False assert settings.server.cors.allow_origins == ["https://yaml.example.com"] @@ -117,16 +117,16 @@ def test_server_settings_can_be_loaded_from_environment_without_yaml_file( monkeypatch.setenv("SYNCMASTER_CONFIG_FILE", str(tmp_path / "missing.yml")) monkeypatch.setenv( "SYNCMASTER__DATABASE__URL", - "postgresql+asyncpg://env@localhost:5432/syncmaster", + "postgresql+asyncpg://env:env@localhost:5432/syncmaster", ) monkeypatch.setenv( "SYNCMASTER__BROKER__URL", - "amqp://env@localhost:5672/", + "amqp://env:env@localhost:5672/", ) monkeypatch.setenv("SYNCMASTER__ENCRYPTION__SECRET_KEY", "env_secret_key") settings = ServerAppSettings() - assert settings.database.url == "postgresql+asyncpg://env@localhost:5432/syncmaster" - assert settings.broker.url == "amqp://env@localhost:5672/" + assert str(settings.database.url) == "postgresql+asyncpg://env:env@localhost:5432/syncmaster" + assert str(settings.broker.url) == "amqp://env:env@localhost:5672/" assert settings.encryption.secret_key == "env_secret_key" diff --git a/tests/test_unit/test_settings/test_worker_settings.py b/tests/test_unit/test_settings/test_worker_settings.py index 349c2fd8..4531ade9 100644 --- a/tests/test_unit/test_settings/test_worker_settings.py +++ b/tests/test_unit/test_settings/test_worker_settings.py @@ -26,7 +26,7 @@ def test_worker_settings_are_loaded_from_default_yaml_file( dedent( """\ database: - url: "postgresql+asyncpg://user:password'#[value]@localhost:5432/syncmaster" + url: "postgresql+asyncpg://user:password@localhost:5432/syncmaster" broker: url: amqp://user:password@localhost:5672/ encryption: @@ -42,8 +42,8 @@ def test_worker_settings_are_loaded_from_default_yaml_file( settings = WorkerAppSettings() - assert settings.database.url == "postgresql+asyncpg://user:password'#[value]@localhost:5432/syncmaster" - assert settings.broker.url == "amqp://user:password@localhost:5672/" + assert str(settings.database.url) == "postgresql+asyncpg://user:password@localhost:5432/syncmaster" + assert str(settings.broker.url) == "amqp://user:password@localhost:5672/" assert settings.encryption.secret_key == "secret_key" assert ( settings.worker.log_url_template @@ -62,9 +62,9 @@ def test_worker_settings_yaml_file_overrides_environment( dedent( """\ database: - url: postgresql+asyncpg://yaml@localhost:5432/syncmaster + url: postgresql+asyncpg://yaml:yaml@localhost:5432/syncmaster broker: - url: amqp://yaml@localhost:5672/ + url: amqp://yaml:yaml@localhost:5672/ encryption: secret_key: "yaml_secret_key" worker: @@ -78,11 +78,11 @@ def test_worker_settings_yaml_file_overrides_environment( monkeypatch.setenv("SYNCMASTER_CONFIG_FILE", str(config_path)) monkeypatch.setenv( "SYNCMASTER__DATABASE__URL", - "postgresql+asyncpg://env@localhost:5432/syncmaster", + "postgresql+asyncpg://env:env@localhost:5432/syncmaster", ) monkeypatch.setenv( "SYNCMASTER__BROKER__URL", - "amqp://env@localhost:5672/", + "amqp://env:env@localhost:5672/", ) monkeypatch.setenv("SYNCMASTER__ENCRYPTION__SECRET_KEY", "env_secret_key") monkeypatch.setenv( @@ -93,8 +93,8 @@ def test_worker_settings_yaml_file_overrides_environment( settings = WorkerAppSettings() - assert settings.database.url == "postgresql+asyncpg://yaml@localhost:5432/syncmaster" - assert settings.broker.url == "amqp://yaml@localhost:5672/" + assert str(settings.database.url) == "postgresql+asyncpg://yaml:yaml@localhost:5432/syncmaster" + assert str(settings.broker.url) == "amqp://yaml:yaml@localhost:5672/" assert settings.encryption.secret_key == "yaml_secret_key" assert ( settings.worker.log_url_template @@ -111,11 +111,11 @@ def test_worker_settings_can_be_loaded_from_environment_without_yaml_file( monkeypatch.setenv("SYNCMASTER_CONFIG_FILE", str(tmp_path / "missing.yml")) monkeypatch.setenv( "SYNCMASTER__DATABASE__URL", - "postgresql+asyncpg://env@localhost:5432/syncmaster", + "postgresql+asyncpg://env:env@localhost:5432/syncmaster", ) monkeypatch.setenv( "SYNCMASTER__BROKER__URL", - "amqp://env@localhost:5672/", + "amqp://env:env@localhost:5672/", ) monkeypatch.setenv("SYNCMASTER__ENCRYPTION__SECRET_KEY", "env_secret_key") monkeypatch.setenv( @@ -126,8 +126,8 @@ def test_worker_settings_can_be_loaded_from_environment_without_yaml_file( settings = WorkerAppSettings() - assert settings.database.url == "postgresql+asyncpg://env@localhost:5432/syncmaster" - assert settings.broker.url == "amqp://env@localhost:5672/" + assert str(settings.database.url) == "postgresql+asyncpg://env:env@localhost:5432/syncmaster" + assert str(settings.broker.url) == "amqp://env:env@localhost:5672/" assert settings.encryption.secret_key == "env_secret_key" assert ( settings.worker.log_url_template diff --git a/tests/utils.py b/tests/utils.py index 1fbe1ef4..ebaac01b 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -36,9 +36,7 @@ async def prepare_new_database(settings: Settings) -> None: """Using default postgres db for creating new test db""" - connection_url = settings.database.url - engine = create_async_engine(connection_url, echo=True) - + engine = create_async_engine(url=str(settings.database.url), **settings.database.model_dump(exclude={"url"})) async with engine.begin() as conn: if not await database_exists(conn, "postgres"): await create_database(conn, "postgres") diff --git a/uv.lock b/uv.lock index 468a9895..f74ddd31 100644 --- a/uv.lock +++ b/uv.lock @@ -181,6 +181,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/66/40/c53deb2cd0c9b0fb636d24d9f40924cf2e65028e6b20b10cd5c1eeb2c730/ast_serialize-0.6.0-cp39-abi3-win_arm64.whl", hash = "sha256:ccd132fe8db56f61fe743b1f644d01b8d65b83248a8da506f3132bda86d6ed5e", size = 1072965, upload-time = "2026-06-30T20:02:54.097Z" }, ] +[[package]] +name = "asttokens" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/45/1d/f03bcb60c4a3212e15f99a56085d93093a497718adf828d050b9d675da81/asttokens-2.4.1.tar.gz", hash = "sha256:b03869718ba9a6eb027e134bfdf69f38a236d681c83c160d510768af11254ba0", size = 62284, upload-time = "2023-10-26T10:03:05.06Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/86/4736ac618d82a20d87d2f92ae19441ebc7ac9e7a581d7e58bbe79233b24a/asttokens-2.4.1-py2.py3-none-any.whl", hash = "sha256:051ed49c3dcae8913ea7cd08e46a606dba30b79993209636c4875bc1d637bc24", size = 27764, upload-time = "2023-10-26T10:03:01.789Z" }, +] + [[package]] name = "asyncpg" version = "0.31.0" @@ -762,6 +774,7 @@ dependencies = [ { name = "asgi-correlation-id" }, { name = "celery" }, { name = "coloredlogs" }, + { name = "devtools" }, { name = "psycopg2-binary" }, { name = "pydantic" }, { name = "pydantic-settings" }, @@ -844,6 +857,7 @@ requires-dist = [ { name = "asyncpg", marker = "extra == 'server'", specifier = "~=0.31.0" }, { name = "celery", specifier = "~=5.6.0" }, { name = "coloredlogs", specifier = "~=15.0.1" }, + { name = "devtools", specifier = "~=0.12.2" }, { name = "fastapi", marker = "extra == 'server'", specifier = "~=0.141.0" }, { name = "horizon-hwm-store", marker = "extra == 'worker'", specifier = "~=1.1.4" }, { name = "itsdangerous", marker = "extra == 'server'", specifier = "~=2.2.0" }, @@ -925,6 +939,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/02/c3/253a89ee03fc9b9682f1541728eb66db7db22148cd94f89ab22528cd1e1b/deprecation-2.1.0-py2.py3-none-any.whl", hash = "sha256:a10811591210e1fb0e768a8c25517cabeabcba6f0bf96564f8ff45189f90b14a", size = 11178, upload-time = "2020-04-20T14:23:36.581Z" }, ] +[[package]] +name = "devtools" +version = "0.12.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asttokens" }, + { name = "executing" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/84/75/b78198620640d394bc435c17bb49db18419afdd6cfa3ed8bcfe14034ec80/devtools-0.12.2.tar.gz", hash = "sha256:efceab184cb35e3a11fa8e602cc4fadacaa2e859e920fc6f87bf130b69885507", size = 75005, upload-time = "2023-09-03T16:57:00.679Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/ae/afb1487556e2dc827a17097aac8158a25b433a345386f0e249f6d2694ccb/devtools-0.12.2-py3-none-any.whl", hash = "sha256:c366e3de1df4cdd635f1ad8cbcd3af01a384d7abda71900e68d43b04eb6aaca7", size = 19411, upload-time = "2023-09-03T16:56:59.049Z" }, +] + [[package]] name = "dirty-equals" version = "0.11" @@ -966,6 +994,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/37/52/0e4caa5d4cfadbbb86a17df30f79c3fb9cd1d46f4dbeb1aa323e4af78acd/evacuator-1.0.4-py3-none-any.whl", hash = "sha256:0c3a5fafa2c41fba1272e18bead116954cd3ac7bf97c3050fa77f516def88c1b", size = 9096, upload-time = "2024-03-20T08:29:37.485Z" }, ] +[[package]] +name = "executing" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488, upload-time = "2025-09-01T09:48:10.866Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, +] + [[package]] name = "faker" version = "40.36.0"