Refactor: Make model packages true leaf nodes - remove app-layer dependencies
Models in app/models/ are now pure data classes with no cross-layer dependencies. This ensures the models layer remains a true leaf node in the dependency graph. Changes: - Create app/models/_common.py with shared types (TimeRange, bucket_count, constants) - Move TimeRange and time-range constants from ban.py to _common.py - Update history.py, routers, and services to import from _common.py - Remove imports from app.config and app.utils from config.py models - Move field validators from models to router layer: - Add log_target validation in config_misc router - Add log_path validation in jail_config router - Update test_models.py to reflect validators moved to router layer - Update documentation (Architekture.md, Backend-Development.md) with model layering rules - Fix import ordering and type annotations in affected files Model layering rule: Models may only import from: ✓ Standard library and third-party packages (Pydantic, typing) ✓ Other models in app/models/ (sibling models) ✓ app.models.response (response envelopes) ✗ app.services, app.config, app.utils, or any application layer Validation requiring app-level state (settings, allowed directories) now happens at the router or service layer, not in model validators. Fixes: Models were not true leaf nodes due to circular imports and app-layer dependencies Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -491,6 +491,13 @@ Pydantic schemas that define data shapes and validation. Models are split into t
|
||||
| `server.py` | Server status and settings models |
|
||||
| `setup.py` | First-run setup wizard models |
|
||||
|
||||
**Model Layering Rules:** Models are pure data classes (leaf nodes) in the dependency graph. They must not import from application-layer modules (`app.services`, `app.config`, `app.utils`). Models may import from:
|
||||
- Standard library and third-party packages (Pydantic, typing)
|
||||
- Other models in `app.models/` (sibling models)
|
||||
- `app.models.response` (response envelopes)
|
||||
|
||||
Validation that requires access to app-level state (e.g., allowed log directories) must be moved to the router or service layer, not in model validators.
|
||||
|
||||
#### Tasks (`app/tasks/`)
|
||||
|
||||
APScheduler background jobs that run on a schedule without user interaction.
|
||||
|
||||
@@ -742,6 +742,11 @@ This policy eliminates a whole class of frontend–backend contract bugs. If the
|
||||
- Validate at the boundary — once data enters a Pydantic model it is trusted.
|
||||
- Use `Field(...)` with descriptions for every field to keep auto-generated docs useful.
|
||||
- Separate **request models**, **response models**, and **domain (internal) models** — do not reuse one model for all three.
|
||||
- **Models are leaf nodes**: Models in `app/models/` must not import from application-layer modules (`app.services`, `app.config`, `app.utils`). Models may only import from:
|
||||
- Standard library and third-party packages (Pydantic, typing)
|
||||
- Other models in `app/models/` (sibling models)
|
||||
- `app.models.response` (response envelopes)
|
||||
- Validation that requires app-level state (e.g., `settings`, allowed directories) must happen at the router or service layer, never in model validators.
|
||||
|
||||
### Using `Literal` Types for Constrained Strings
|
||||
|
||||
@@ -765,27 +770,34 @@ This provides:
|
||||
- **API documentation** — OpenAPI docs automatically list all allowed values.
|
||||
- **Validation** — Pydantic rejects invalid values and provides a clear error message.
|
||||
|
||||
### Custom Field Validators
|
||||
### Field Validators and Validation Placement
|
||||
|
||||
For fields that require complex validation (e.g., file paths that must be within allowed directories), use `@field_validator`:
|
||||
Field validators in models should only contain logic that is **stateless and does not depend on application configuration or state**. Validators must not import from `app.config`, `app.utils`, or `app.services`.
|
||||
|
||||
For validation that depends on app-level state (e.g., file paths that must be within allowed directories), perform validation in the router or service layer:
|
||||
|
||||
```python
|
||||
from pydantic import field_validator
|
||||
# ✅ Good: Validation in router (has access to settings)
|
||||
from fastapi import APIRouter
|
||||
from app.config import get_settings
|
||||
from app.utils.path_utils import validate_log_path
|
||||
|
||||
class AddLogPathRequest(BaseModel):
|
||||
log_path: str = Field(..., description="Absolute path to the log file to monitor.")
|
||||
@router.post("/jails/{name}/logpath")
|
||||
async def add_log_path(name: str, body: AddLogPathRequest) -> None:
|
||||
# Validate before using
|
||||
validate_log_path(body.log_path)
|
||||
await config_service.add_log_path(socket_path, name, body)
|
||||
|
||||
@field_validator("log_path", mode="after")
|
||||
@classmethod
|
||||
def validate_log_path_field(cls, value: str) -> str:
|
||||
"""Validate that the log path is within allowed directories."""
|
||||
return validate_log_path(value)
|
||||
# ❌ Avoid: Importing from app layer in model validators
|
||||
# Do NOT do this in app/models/config.py:
|
||||
# from app.config import get_settings
|
||||
# @field_validator("log_path")
|
||||
# def validate_log_path_field(cls, value: str) -> str:
|
||||
# settings = get_settings() # ← Models must not import from app layer
|
||||
# ...
|
||||
```
|
||||
|
||||
**Path Validation Helper:**
|
||||
|
||||
For query parameters and other contexts where Pydantic validators cannot be used directly, use the `validate_log_path()` helper from `app.utils.path_utils`:
|
||||
**Common Helper:** For shared path validation logic, use the `validate_log_path()` helper from `app.utils.path_utils` in your router or service, not in model validators.
|
||||
|
||||
```python
|
||||
from fastapi import HTTPException, status
|
||||
|
||||
1387
Docs/Tasks.md
1387
Docs/Tasks.md
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user