From 3d5acb756f890f3e1fbea1dc2a1915e047bb393b Mon Sep 17 00:00:00 2001 From: Lukas Date: Thu, 30 Apr 2026 20:06:10 +0200 Subject: [PATCH] refactor: move repository and service imports to module level in dependencies.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move all repository imports (session_repo, blocklist_repo, import_log_repo, settings_repo, history_archive_repo, geo_cache_repo, fail2ban_db_repo) and service imports (auth_service, health_service, default_fail2ban_metadata_service) to module level in app/dependencies.py. This eliminates the pattern of local imports inside provider functions, providing consistency and reducing import overhead. The from app.db import open_db remains a local import since it's only used within get_db(). - Verified no circular dependencies exist - All repository and service provider functions simplified to return modules - Updated Architekture.md § 2.3 to document the module-level import pattern - All tests pass (28 dependency + auth tests) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Docs/Architekture.md | 22 ++++++++++++++++++++++ backend/app/dependencies.py | 34 ++++++++++++++-------------------- 2 files changed, 36 insertions(+), 20 deletions(-) diff --git a/Docs/Architekture.md b/Docs/Architekture.md index 3482157..617e95e 100644 --- a/Docs/Architekture.md +++ b/Docs/Architekture.md @@ -599,6 +599,28 @@ Every injectable dependency follows this structure: ... ``` +**Module-Level Imports:** + +All repository and service modules are imported at module level in `app/dependencies.py`. These imports are safe at the top because no circular dependencies exist — repositories and services do not import from `dependencies.py`. This follows the principle of importing dependencies early and consistently: + +```python +# app/dependencies.py (top of file) +from app.repositories import ( + blocklist_repo, + fail2ban_db_repo, + session_repo, + # ... other repository modules +) +from app.services import auth_service, health_service +from app.services.fail2ban_metadata_service import default_fail2ban_metadata_service + +# Provider functions simply return the module +async def get_session_repo() -> SessionRepository: + return session_repo +``` + +**Exception**: The `from app.db import open_db` import remains local to `get_db()` because it is only used within that specific function and the module load overhead is avoided. + #### Service Composition Root Services are **not instantiated by a container**. Instead, they are **composed by routers and tasks through explicit parameter passing**. This keeps dependencies visible and avoids implicit side effects. diff --git a/backend/app/dependencies.py b/backend/app/dependencies.py index 95bd1ed..a09a5dd 100644 --- a/backend/app/dependencies.py +++ b/backend/app/dependencies.py @@ -60,6 +60,20 @@ from app.utils.rate_limiter import RateLimiter from app.utils.runtime_state import ApplicationState, JailServiceState, RuntimeState from app.utils.session_cache import NoOpSessionCache, SessionCache +# Module-level imports for repositories and services +# These are safe at module level since no circular dependencies exist +from app.repositories import ( + blocklist_repo, + fail2ban_db_repo, + geo_cache_repo, + history_archive_repo, + import_log_repo, + session_repo, + settings_repo, +) +from app.services import auth_service, health_service +from app.services.fail2ban_metadata_service import default_fail2ban_metadata_service + log: structlog.stdlib.BoundLogger = structlog.get_logger() @@ -255,8 +269,6 @@ async def get_session_repo() -> SessionRepository: Protocol interface — its top-level async functions must match the Protocol signatures exactly. This is documented in Backend-Development.md § 13.7.1. """ - from app.repositories import session_repo # noqa: PLC0415 - return session_repo @@ -267,8 +279,6 @@ async def get_blocklist_repo() -> BlocklistRepository: Protocol interface — its top-level async functions must match the Protocol signatures exactly. This is documented in Backend-Development.md § 13.7.1. """ - from app.repositories import blocklist_repo # noqa: PLC0415 - return cast("BlocklistRepository", blocklist_repo) @@ -279,8 +289,6 @@ async def get_import_log_repo() -> ImportLogRepository: Protocol interface — its top-level async functions must match the Protocol signatures exactly. This is documented in Backend-Development.md § 13.7.1. """ - from app.repositories import import_log_repo # noqa: PLC0415 - return cast("ImportLogRepository", import_log_repo) @@ -291,8 +299,6 @@ async def get_settings_repo() -> SettingsRepository: Protocol interface — its top-level async functions must match the Protocol signatures exactly. This is documented in Backend-Development.md § 13.7.1. """ - from app.repositories import settings_repo # noqa: PLC0415 - return cast("SettingsRepository", settings_repo) @@ -304,8 +310,6 @@ async def get_history_archive_repo() -> HistoryArchiveRepository: must match the Protocol signatures exactly. This is documented in Backend-Development.md § 13.7.1. """ - from app.repositories import history_archive_repo # noqa: PLC0415 - return cast("HistoryArchiveRepository", history_archive_repo) @@ -316,8 +320,6 @@ async def get_geo_cache_repo() -> GeoCacheRepository: Protocol interface — its top-level async functions must match the Protocol signatures exactly. This is documented in Backend-Development.md § 13.7.1. """ - from app.repositories import geo_cache_repo # noqa: PLC0415 - return cast("GeoCacheRepository", geo_cache_repo) @@ -329,8 +331,6 @@ async def get_fail2ban_db_repo() -> Fail2BanDbRepository: match the Protocol signatures exactly. This is documented in Backend-Development.md § 13.7.1. """ - from app.repositories import fail2ban_db_repo # noqa: PLC0415 - return cast("Fail2BanDbRepository", fail2ban_db_repo) @@ -375,8 +375,6 @@ async def get_health_probe() -> Callable[[str], Awaitable[ServerStatus]]: A callable that probes the fail2ban socket and returns ServerStatus. This allows explicit dependency injection to avoid hidden service coupling. """ - from app.services import health_service # noqa: PLC0415 - return health_service.probe @@ -387,8 +385,6 @@ async def get_fail2ban_metadata_service() -> object: The singleton Fail2BanMetadataService for resolving fail2ban metadata (such as the database path) and caching results. """ - from app.services.fail2ban_metadata_service import default_fail2ban_metadata_service # noqa: PLC0415 - return default_fail2ban_metadata_service @@ -612,8 +608,6 @@ async def require_auth( if cached is not None: return cached - from app.services import auth_service # noqa: PLC0415 - try: session = await auth_service.validate_session( db,