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
|
||||
|
||||
Reference in New Issue
Block a user