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:
2026-04-30 19:31:11 +02:00
parent 3d1a6f5538
commit 100fd47c4b
15 changed files with 1542 additions and 396 deletions

View File

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

View File

@@ -742,6 +742,11 @@ This policy eliminates a whole class of frontendbackend 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

File diff suppressed because it is too large Load Diff