Spaces:
Build error
Build error
| # Backend Class Dependencies | |
| Guide to FastAPI's dependency injection system and available dependencies in the PDF-TEI Editor. | |
| ## Overview | |
| The application uses FastAPI's dependency injection system to provide reusable components to route handlers. This approach: | |
| - Enables proper mocking in tests via `app.dependency_overrides` | |
| - Prevents database access failures in CI environments | |
| - Improves code testability and maintainability | |
| - Provides automatic resource lifecycle management | |
| ## How Depends() Works | |
| FastAPI's `Depends()` creates injectable dependencies that are resolved automatically when a route is called: | |
| ```python | |
| from fastapi import APIRouter, Depends | |
| from fastapi_app.lib.core.dependencies import get_db, get_file_storage | |
| router = APIRouter() | |
| @router.get("/files") | |
| async def list_files( | |
| db=Depends(get_db), # Injected automatically | |
| file_storage=Depends(get_file_storage) # Injected automatically | |
| ): | |
| """db and file_storage are provided by FastAPI's DI system.""" | |
| file_repo = FileRepository(db) | |
| # ... use db and file_storage ... | |
| ``` | |
| **Key Points:** | |
| - Dependencies are declared as function parameters with `Depends(dependency_function)` | |
| - FastAPI calls the dependency function and injects the result | |
| - Dependencies can depend on other dependencies (nested dependencies) | |
| - Each dependency is cached per request by default | |
| - Tests can override dependencies using `app.dependency_overrides` | |
| ## Available Dependencies | |
| All dependency functions are defined in [fastapi_app/lib/core/dependencies.py](../../fastapi_app/lib/core/dependencies.py). | |
| ### Database Dependencies | |
| #### `get_db() -> DatabaseManager` | |
| Returns a configured DatabaseManager instance for metadata.db. | |
| ```python | |
| from fastapi import Depends | |
| from fastapi_app.lib.core.dependencies import get_db | |
| from fastapi_app.lib.core.database import DatabaseManager | |
| @router.get("/example") | |
| async def example(db: DatabaseManager = Depends(get_db)): | |
| # Use db to execute SQL queries | |
| result = db.execute("SELECT * FROM files") | |
| ``` | |
| **Used for:** | |
| - Direct database queries | |
| - Creating FileRepository instances | |
| - Transaction management | |
| #### `get_file_repository(db: DatabaseManager = Depends(get_db)) -> FileRepository` | |
| Returns a FileRepository instance with injected database. | |
| ```python | |
| from fastapi import Depends | |
| from fastapi_app.lib.core.dependencies import get_file_repository | |
| from fastapi_app.lib.repository.file_repository import FileRepository | |
| @router.get("/files") | |
| async def list_files(file_repo: FileRepository = Depends(get_file_repository)): | |
| # Use file_repo for high-level file operations | |
| files = file_repo.get_all_files() | |
| ``` | |
| **Used for:** | |
| - File metadata queries | |
| - Collection management | |
| - File versioning operations | |
| **Note:** Depends on `get_db()` internally. | |
| #### `get_file_storage() -> FileStorage` | |
| Returns a FileStorage instance for physical file access. | |
| ```python | |
| from fastapi import Depends | |
| from fastapi_app.lib.core.dependencies import get_file_storage | |
| from fastapi_app.lib.storage.file_storage import FileStorage | |
| @router.get("/file/{file_id}") | |
| async def read_file( | |
| file_id: str, | |
| file_storage: FileStorage = Depends(get_file_storage) | |
| ): | |
| # Read physical file from content-addressable storage | |
| content = file_storage.read_file(file_id, "tei") | |
| ``` | |
| **Used for:** | |
| - Reading file content from disk | |
| - Writing files to content-addressable storage | |
| - File reference counting and cleanup | |
| ### Authentication Dependencies | |
| #### `get_session_manager() -> SessionManager` | |
| Returns a SessionManager instance for session validation. | |
| ```python | |
| from fastapi import Depends | |
| from fastapi_app.lib.core.dependencies import get_session_manager | |
| from fastapi_app.lib.core.sessions import SessionManager | |
| @router.get("/protected") | |
| async def protected_route( | |
| session_manager: SessionManager = Depends(get_session_manager) | |
| ): | |
| # Validate session manually if needed | |
| is_valid = session_manager.is_session_valid(session_id, timeout) | |
| ``` | |
| **Used for:** | |
| - Session validation in custom auth logic | |
| - Session lifecycle management | |
| - Testing authentication flows | |
| #### `get_auth_manager() -> AuthManager` | |
| Returns an AuthManager instance for user authentication. | |
| ```python | |
| from fastapi import Depends | |
| from fastapi_app.lib.core.dependencies import get_auth_manager | |
| from fastapi_app.lib.utils.auth import AuthManager | |
| @router.post("/login") | |
| async def login( | |
| auth_manager: AuthManager = Depends(get_auth_manager) | |
| ): | |
| # Authenticate user | |
| user = auth_manager.authenticate(username, password_hash) | |
| ``` | |
| **Used for:** | |
| - User authentication | |
| - Password validation | |
| - User lookup by session ID | |
| #### `get_session_id(request: Request) -> Optional[str]` | |
| Extracts session ID from request headers or query parameters (returns None if not present). | |
| ```python | |
| from fastapi import Request, Depends | |
| from fastapi_app.lib.core.dependencies import get_session_id | |
| @router.get("/optional-auth") | |
| async def optional_auth( | |
| request: Request, | |
| session_id: str | None = Depends(get_session_id) | |
| ): | |
| if session_id: | |
| # User is authenticated | |
| pass | |
| else: | |
| # Anonymous access | |
| pass | |
| ``` | |
| **Session ID Sources (in order of precedence):** | |
| 1. `X-Session-ID` header | |
| 2. `X-Session-Id` header (case variation) | |
| 3. `session_id` query parameter | |
| #### `require_session_id(request: Request) -> str` | |
| Extracts session ID from request (raises 401 if not present). | |
| ```python | |
| from fastapi import Request, Depends | |
| from fastapi_app.lib.core.dependencies import require_session_id | |
| @router.get("/requires-session") | |
| async def requires_session( | |
| request: Request, | |
| session_id: str = Depends(require_session_id) | |
| ): | |
| # session_id is guaranteed to be present (or 401 raised) | |
| ``` | |
| **Raises:** `HTTPException(401)` if session ID not found. | |
| #### `get_current_user(request: Request, ...) -> Optional[Dict]` | |
| Returns authenticated user or None (does not raise errors). | |
| ```python | |
| from fastapi import Request, Depends | |
| from typing import Optional, Dict | |
| from fastapi_app.lib.core.dependencies import get_current_user | |
| @router.get("/optional-user") | |
| async def optional_user( | |
| request: Request, | |
| user: Optional[Dict] = Depends(get_current_user) | |
| ): | |
| if user: | |
| # User is authenticated | |
| username = user['username'] | |
| roles = user['roles'] | |
| else: | |
| # Anonymous access allowed | |
| pass | |
| ``` | |
| **Returns:** User dict with keys: `username`, `email`, `roles`, `groups`, etc. | |
| **Use when:** Endpoints support both authenticated and anonymous access. | |
| #### `require_authenticated_user(request: Request, ...) -> Dict` | |
| Returns authenticated user (raises 401 if not authenticated). | |
| ```python | |
| from fastapi import Request, Depends | |
| from typing import Dict | |
| from fastapi_app.lib.core.dependencies import require_authenticated_user | |
| @router.get("/protected") | |
| async def protected( | |
| request: Request, | |
| user: Dict = Depends(require_authenticated_user) | |
| ): | |
| # user is guaranteed to be authenticated (or 401 raised) | |
| username = user['username'] | |
| roles = user['roles'] | |
| ``` | |
| **Returns:** User dict with keys: `username`, `email`, `roles`, `groups`, etc. | |
| **Raises:** `HTTPException(401)` if not authenticated or session expired. | |
| **Use when:** Endpoint requires authentication. | |
| **Development Mode:** Set `FASTAPI_ALLOW_ANONYMOUS_ACCESS=true` to bypass authentication (returns mock user with admin roles). | |
| #### `require_admin_user(user: Dict = Depends(require_authenticated_user)) -> Dict` | |
| Returns authenticated user with admin role (raises 403 if not admin). | |
| ```python | |
| from fastapi import Depends | |
| from typing import Dict | |
| from fastapi_app.lib.core.dependencies import require_admin_user | |
| @router.delete("/users/{username}") | |
| async def delete_user( | |
| username: str, | |
| user: Dict = Depends(require_admin_user) | |
| ): | |
| # user is guaranteed to have admin role (or 403 raised) | |
| ``` | |
| **Returns:** User dict (same as `require_authenticated_user`). | |
| **Raises:** | |
| - `HTTPException(401)` if not authenticated | |
| - `HTTPException(403)` if authenticated but not admin | |
| **Admin Check:** User must have `'admin'` role or wildcard `'*'` in their roles array. | |
| **Use when:** Endpoint requires admin privileges (user management, system config, etc.). | |
| ### SSE and Sync Dependencies | |
| #### `get_sse_service() -> SSEService` | |
| Returns singleton SSEService instance for server-sent events. | |
| ```python | |
| from fastapi import Depends | |
| from fastapi_app.lib.core.dependencies import get_sse_service | |
| from fastapi_app.lib.sse.sse_service import SSEService | |
| @router.post("/broadcast") | |
| async def broadcast( | |
| sse_service: SSEService = Depends(get_sse_service) | |
| ): | |
| # Broadcast event to all connected clients | |
| await sse_service.broadcast("event_type", {"data": "value"}) | |
| ``` | |
| **Used for:** | |
| - Broadcasting events to all clients | |
| - Session-specific event delivery | |
| - Real-time notifications | |
| #### `get_sync_service(...) -> Optional[SyncService]` | |
| Returns SyncService instance with dependencies (returns None if WebDAV not configured). | |
| ```python | |
| from fastapi import Depends | |
| from typing import Optional | |
| from fastapi_app.lib.core.dependencies import get_sync_service | |
| from fastapi_app.lib.services.sync_service import SyncService | |
| @router.post("/sync") | |
| async def sync_files( | |
| sync_service: Optional[SyncService] = Depends(get_sync_service) | |
| ): | |
| if not sync_service: | |
| raise HTTPException(503, "WebDAV not configured") | |
| # Perform sync operation | |
| result = await sync_service.sync_collection(collection_id) | |
| ``` | |
| **Returns:** `SyncService` instance or `None` if WebDAV not configured. | |
| **Dependencies:** Automatically injects `FileRepository`, `FileStorage`, and `SSEService`. | |
| **Use when:** Implementing WebDAV synchronization features. | |
| #### `get_event_bus() -> EventBus` | |
| Returns singleton EventBus instance for decoupled component communication. | |
| ```python | |
| from fastapi import Depends | |
| from fastapi_app.lib.core.dependencies import get_event_bus | |
| from fastapi_app.lib.sse.event_bus import EventBus | |
| @router.post("/files/{file_id}") | |
| async def update_file( | |
| file_id: str, | |
| event_bus: EventBus = Depends(get_event_bus) | |
| ): | |
| # Update file... | |
| # Notify other components about the change | |
| await event_bus.emit("file.updated", file_id=file_id, variant="tei") | |
| ``` | |
| **Used for:** | |
| - Loose coupling between plugins and application components | |
| - Event-driven architecture patterns | |
| - Broadcasting state changes without direct dependencies | |
| **Key Features:** | |
| - Async-native (handlers are async functions) | |
| - Error isolation (exceptions in one handler don't affect others) | |
| - Concurrent handler execution | |
| - Singleton instance shared across application | |
| **Common Events:** | |
| - `file.updated` - File content or metadata changed | |
| - `file.deleted` - File removed | |
| - `collection.modified` - Collection membership changed | |
| - `plugin.initialized` - Plugin finished initialization | |
| **Handler Registration:** | |
| Plugins typically register handlers during initialization: | |
| ```python | |
| class MyPlugin(PluginBase): | |
| async def initialize(self): | |
| bus = get_event_bus() | |
| bus.on("file.updated", self._handle_file_update) | |
| async def _handle_file_update(self, file_id: str, variant: str, **kwargs): | |
| # Handle the event | |
| pass | |
| ``` | |
| ## Dependency Nesting | |
| Dependencies can depend on other dependencies. FastAPI resolves the entire dependency tree: | |
| ```python | |
| # get_file_repository depends on get_db | |
| def get_file_repository(db: DatabaseManager = Depends(get_db)) -> FileRepository: | |
| return FileRepository(db) | |
| # get_sync_service depends on get_file_repository (which depends on get_db) | |
| def get_sync_service( | |
| file_repo: FileRepository = Depends(get_file_repository), | |
| file_storage: FileStorage = Depends(get_file_storage), | |
| sse_service: SSEService = Depends(get_sse_service) | |
| ) -> SyncService: | |
| return SyncService(file_repo, file_storage, sse_service) | |
| # Your route only needs to declare the top-level dependency | |
| @router.post("/sync") | |
| async def sync_files(sync_service: SyncService = Depends(get_sync_service)): | |
| # All nested dependencies (db, file_repo, etc.) are resolved automatically | |
| ``` | |
| FastAPI caches dependency results per request, so `get_db()` is called once even if multiple dependencies need it. | |
| ## Testing with Dependency Overrides | |
| Dependencies can be overridden in tests using `app.dependency_overrides`: | |
| ```python | |
| from unittest.mock import MagicMock | |
| from fastapi import FastAPI | |
| from fastapi.testclient import TestClient | |
| from fastapi_app.lib.core.dependencies import get_db, get_auth_manager | |
| # Create test app | |
| app = FastAPI() | |
| app.include_router(router) | |
| # Create mocks | |
| mock_db = MagicMock() | |
| mock_auth = MagicMock() | |
| # Override dependencies | |
| app.dependency_overrides[get_db] = lambda: mock_db | |
| app.dependency_overrides[get_auth_manager] = lambda: mock_auth | |
| # Test client will use mocks | |
| client = TestClient(app) | |
| response = client.get("/files") | |
| ``` | |
| **Key Points:** | |
| - Override in `setUp()` to apply to all tests | |
| - Mock return values: `mock_db.execute.return_value = [...]` | |
| - Individual tests can modify mock behavior | |
| - Always use dependency injection (not direct calls) to enable mocking | |
| See [testing-guide.md](../code-assistant/testing-guide.md#python-unit-tests-with-fastapi-routes) for complete testing patterns. | |
| ## Common Patterns | |
| ### Basic Authenticated Route | |
| ```python | |
| from fastapi import APIRouter, Depends, Request | |
| from typing import Dict | |
| from fastapi_app.lib.core.dependencies import require_authenticated_user | |
| router = APIRouter() | |
| @router.get("/profile") | |
| async def get_profile( | |
| request: Request, | |
| user: Dict = Depends(require_authenticated_user) | |
| ): | |
| return { | |
| "username": user['username'], | |
| "roles": user['roles'] | |
| } | |
| ``` | |
| ### Admin-Only Route | |
| ```python | |
| from fastapi import APIRouter, Depends | |
| from typing import Dict | |
| from fastapi_app.lib.core.dependencies import require_admin_user | |
| router = APIRouter() | |
| @router.delete("/users/{username}") | |
| async def delete_user( | |
| username: str, | |
| user: Dict = Depends(require_admin_user) | |
| ): | |
| # Only admin users can reach here | |
| # ... delete user logic ... | |
| ``` | |
| ### File Operation Route | |
| ```python | |
| from fastapi import APIRouter, Depends | |
| from fastapi_app.lib.core.dependencies import get_file_repository, get_file_storage | |
| from fastapi_app.lib.repository.file_repository import FileRepository | |
| from fastapi_app.lib.storage.file_storage import FileStorage | |
| router = APIRouter() | |
| @router.get("/files/{file_id}") | |
| async def get_file( | |
| file_id: str, | |
| file_repo: FileRepository = Depends(get_file_repository), | |
| file_storage: FileStorage = Depends(get_file_storage) | |
| ): | |
| # Get metadata | |
| file_metadata = file_repo.get_file_by_stable_id(file_id) | |
| # Read content | |
| content = file_storage.read_file(file_metadata.file_id, file_metadata.file_type) | |
| return {"content": content.decode('utf-8')} | |
| ``` | |
| ### Custom Authentication Route | |
| When implementing custom authentication logic (e.g., plugin routes), use the individual auth dependencies: | |
| ```python | |
| from fastapi import APIRouter, Depends, Header, Query, HTTPException | |
| from fastapi_app.lib.core.dependencies import get_auth_manager, get_session_manager | |
| from fastapi_app.lib.utils.auth import AuthManager | |
| from fastapi_app.lib.core.sessions import SessionManager | |
| router = APIRouter(prefix="/api/plugins/my-plugin") | |
| @router.get("/custom") | |
| async def custom_route( | |
| session_id: str | None = Query(None), | |
| x_session_id: str | None = Header(None, alias="X-Session-ID"), | |
| session_manager: SessionManager = Depends(get_session_manager), | |
| auth_manager: AuthManager = Depends(get_auth_manager) | |
| ): | |
| from fastapi_app.config import get_settings | |
| # Extract session ID (header takes precedence) | |
| session_id_value = x_session_id or session_id | |
| if not session_id_value: | |
| raise HTTPException(status_code=401, detail="Authentication required") | |
| # Validate session | |
| settings = get_settings() | |
| if not session_manager.is_session_valid(session_id_value, settings.session_timeout): | |
| raise HTTPException(status_code=401, detail="Invalid or expired session") | |
| # Get user | |
| user = auth_manager.get_user_by_session_id(session_id_value, session_manager) | |
| if not user: | |
| raise HTTPException(status_code=401, detail="User not found") | |
| # ... use user ... | |
| ``` | |
| **Note:** For standard routes, prefer `require_authenticated_user` instead of manual authentication. | |
| ## Anti-Patterns | |
| ### ❌ Direct Function Calls (Wrong) | |
| ```python | |
| # BAD - Cannot be mocked in tests, fails if database doesn't exist | |
| @router.get("/files") | |
| async def list_files(): | |
| db = get_db() # Direct call - bad! | |
| file_storage = get_file_storage() # Direct call - bad! | |
| # ... | |
| ``` | |
| **Problems:** | |
| - Tests cannot override these dependencies | |
| - Fails in CI if database doesn't exist | |
| - Not following FastAPI patterns | |
| ### ✅ Dependency Injection (Correct) | |
| ```python | |
| # GOOD - Can be mocked in tests, follows FastAPI patterns | |
| @router.get("/files") | |
| async def list_files( | |
| db=Depends(get_db), | |
| file_storage=Depends(get_file_storage) | |
| ): | |
| # ... | |
| ``` | |
| ## Related Documentation | |
| - [Testing Guide](../code-assistant/testing-guide.md) - Dependency mocking patterns | |
| - [Backend Plugins](../code-assistant/backend-plugins.md) - Using dependencies in plugins | |
| - [Architecture](./architecture.md) - System architecture overview | |