Task 8: Standardize modeling style (TypedDict vs Pydantic)
Convert inconsistent modeling style to standardized Pydantic models for all external-facing data structures while maintaining TypedDict compatibility where appropriate for internal layer-private structures. Changes: - Converted IpLookupResult TypedDict to use IpLookupResponse Pydantic model in jail_service.lookup_ip() for consistency with routers - Added GeoCacheEntry Pydantic model for geo cache repository rows - Converted GeoCacheRow TypedDict to use GeoCacheEntry alias - Converted ImportLogRow TypedDict to use ImportLogEntry alias - Updated routers and services to work with Pydantic models - Updated all tests to use Pydantic model field access (attributes) instead of dict subscripting Documentation: - Added 'Model Type Usage by Layer' section to Backend-Development.md - Defines when TypedDict is allowed (internal structures) vs Pydantic (external-facing, cross-boundary data) - Provides clear guidance on modeling conventions per layer Benefits: - Consistent validation and serialization behavior - Better IDE support and type checking - Clearer separation of concerns by layer - Reduced maintenance cost from mixed validation approaches Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -401,6 +401,69 @@ async def delete_log_path(
|
||||
- **Never use string prefix matching** for path validation (e.g., `path.startswith("/var/log")`). The helper uses `Path.relative_to()` to prevent bypasses like `/var/log_evil/file.log`.
|
||||
- Symlinks are resolved before validating to prevent symlink-based escapes.
|
||||
|
||||
### Model Type Usage by Layer
|
||||
|
||||
**Pydantic models** are mandatory for all **external-facing** data structures — anything that crosses layer boundaries or is serialized to HTTP responses. **TypedDict** may be used **only** for internal, layer-private data structures where they provide precise typing without runtime overhead.
|
||||
|
||||
**Rules:**
|
||||
|
||||
1. **Routers (HTTP boundary):** All request and response types **must be Pydantic models**. FastAPI uses these for validation, serialization, and OpenAPI documentation.
|
||||
- Use Pydantic request models for request bodies and query parameters.
|
||||
- Use Pydantic response models in the `response_model` parameter.
|
||||
|
||||
```python
|
||||
# Good — Pydantic models for router layer
|
||||
class JailStatsRequest(BaseModel):
|
||||
jail_name: str
|
||||
|
||||
class JailStatsResponse(BaseModel):
|
||||
jail_name: str
|
||||
active_bans: int
|
||||
|
||||
@router.post("/stats", response_model=JailStatsResponse)
|
||||
async def get_stats(req: JailStatsRequest) -> JailStatsResponse:
|
||||
...
|
||||
```
|
||||
|
||||
2. **Services (business logic):** Return types should be **Pydantic models** if the result is:
|
||||
- Returned to a router (likely — they become API responses).
|
||||
- Used across multiple services (shared interfaces).
|
||||
- Exposed to external consumers (even indirectly).
|
||||
|
||||
If a service returns a purely internal intermediate result used by a single caller, TypedDict is acceptable but should be rare.
|
||||
|
||||
```python
|
||||
# Good — service returns Pydantic (may be used by multiple routers)
|
||||
async def get_jail_details(name: str) -> JailDetailResponse:
|
||||
...
|
||||
|
||||
# Acceptable — purely internal utility result
|
||||
def _parse_fail2ban_response(raw: str) -> ParsedResponse:
|
||||
"""Internal helper—used only by this service."""
|
||||
...
|
||||
```
|
||||
|
||||
3. **Repositories (data access):** Return types may use **TypedDict** because they represent **raw database rows** that:
|
||||
- Are layer-private (only called by their own service).
|
||||
- Do not cross HTTP boundaries directly.
|
||||
- Benefit from lightweight typing without runtime validation.
|
||||
|
||||
```python
|
||||
# Good — TypedDict for raw repository rows
|
||||
class GeoRow(TypedDict):
|
||||
ip: str
|
||||
country_code: str | None
|
||||
|
||||
async def load_all(db: aiosqlite.Connection) -> list[GeoRow]:
|
||||
...
|
||||
```
|
||||
|
||||
If a repository result becomes part of a service's public interface (returned to routers or other services), convert it to a Pydantic model.
|
||||
|
||||
4. **Utilities and helpers:** Internal helper results may use TypedDict if they are not part of a public module interface.
|
||||
|
||||
**Migration path:** Existing internal TypedDicts (e.g., `GeoCacheRow`, `ImportLogRow`) may remain as TypedDicts so long as they stay within their layer. If a type needs to cross layer boundaries (repo → service → router), convert it to a Pydantic model incrementally as you refactor that data flow.
|
||||
|
||||
---
|
||||
|
||||
## 6. Async Rules
|
||||
|
||||
@@ -1,22 +1,3 @@
|
||||
## 7) Service layer coupled to response/presentation models
|
||||
- Where found:
|
||||
- [backend/app/services/ban_service.py](backend/app/services/ban_service.py)
|
||||
- Why this is needed:
|
||||
- Domain logic becomes tied to API shape and slows model evolution.
|
||||
- Goal:
|
||||
- Keep service returns domain-centered; map to API models at router boundary.
|
||||
- What to do:
|
||||
- Introduce service DTO/domain objects.
|
||||
- Map to response models in routers or dedicated mappers.
|
||||
- Possible traps and issues:
|
||||
- Temporary duplicate model definitions during migration.
|
||||
- Docs changes needed:
|
||||
- Add layer responsibilities and mapping policy.
|
||||
- Doc references:
|
||||
- [Docs/Architekture.md](Docs/Architekture.md)
|
||||
|
||||
---
|
||||
|
||||
## 8) Inconsistent modeling style (TypedDict vs Pydantic)
|
||||
- Where found:
|
||||
- [backend/app/services/jail_service.py](backend/app/services/jail_service.py)
|
||||
|
||||
Reference in New Issue
Block a user