refactor: move repository and service imports to module level in dependencies.py

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>
This commit is contained in:
2026-04-30 20:06:10 +02:00
parent 277f2a467c
commit 3d5acb756f
2 changed files with 36 additions and 20 deletions

View File

@@ -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 #### 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. 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.

View File

@@ -60,6 +60,20 @@ from app.utils.rate_limiter import RateLimiter
from app.utils.runtime_state import ApplicationState, JailServiceState, RuntimeState from app.utils.runtime_state import ApplicationState, JailServiceState, RuntimeState
from app.utils.session_cache import NoOpSessionCache, SessionCache 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() 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 Protocol interface — its top-level async functions must match the Protocol
signatures exactly. This is documented in Backend-Development.md § 13.7.1. signatures exactly. This is documented in Backend-Development.md § 13.7.1.
""" """
from app.repositories import session_repo # noqa: PLC0415
return session_repo return session_repo
@@ -267,8 +279,6 @@ async def get_blocklist_repo() -> BlocklistRepository:
Protocol interface — its top-level async functions must match the Protocol Protocol interface — its top-level async functions must match the Protocol
signatures exactly. This is documented in Backend-Development.md § 13.7.1. 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) 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 Protocol interface — its top-level async functions must match the Protocol
signatures exactly. This is documented in Backend-Development.md § 13.7.1. 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) 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 Protocol interface — its top-level async functions must match the Protocol
signatures exactly. This is documented in Backend-Development.md § 13.7.1. 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) 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 must match the Protocol signatures exactly. This is documented in
Backend-Development.md § 13.7.1. Backend-Development.md § 13.7.1.
""" """
from app.repositories import history_archive_repo # noqa: PLC0415
return cast("HistoryArchiveRepository", history_archive_repo) 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 Protocol interface — its top-level async functions must match the Protocol
signatures exactly. This is documented in Backend-Development.md § 13.7.1. 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) 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 match the Protocol signatures exactly. This is documented in
Backend-Development.md § 13.7.1. Backend-Development.md § 13.7.1.
""" """
from app.repositories import fail2ban_db_repo # noqa: PLC0415
return cast("Fail2BanDbRepository", fail2ban_db_repo) 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. A callable that probes the fail2ban socket and returns ServerStatus.
This allows explicit dependency injection to avoid hidden service coupling. This allows explicit dependency injection to avoid hidden service coupling.
""" """
from app.services import health_service # noqa: PLC0415
return health_service.probe return health_service.probe
@@ -387,8 +385,6 @@ async def get_fail2ban_metadata_service() -> object:
The singleton Fail2BanMetadataService for resolving fail2ban metadata The singleton Fail2BanMetadataService for resolving fail2ban metadata
(such as the database path) and caching results. (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 return default_fail2ban_metadata_service
@@ -612,8 +608,6 @@ async def require_auth(
if cached is not None: if cached is not None:
return cached return cached
from app.services import auth_service # noqa: PLC0415
try: try:
session = await auth_service.validate_session( session = await auth_service.validate_session(
db, db,