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>
50 lines
1.5 KiB
Python
50 lines
1.5 KiB
Python
"""Shared types and constants used across multiple model modules.
|
|
|
|
This module defines types and constants that are used by multiple
|
|
model modules, ensuring a single source of truth for cross-model types.
|
|
"""
|
|
|
|
import math
|
|
from typing import Literal
|
|
|
|
#: The four supported time-range presets for the dashboard views.
|
|
TimeRange = Literal["24h", "7d", "30d", "365d"]
|
|
|
|
#: Number of seconds represented by each preset.
|
|
TIME_RANGE_SECONDS: dict[str, int] = {
|
|
"24h": 24 * 3600,
|
|
"7d": 7 * 24 * 3600,
|
|
"30d": 30 * 24 * 3600,
|
|
"365d": 365 * 24 * 3600,
|
|
}
|
|
|
|
#: Bucket size in seconds for each time-range preset.
|
|
BUCKET_SECONDS: dict[str, int] = {
|
|
"24h": 3_600, # 1 hour → 24 buckets
|
|
"7d": 6 * 3_600, # 6 hours → 28 buckets
|
|
"30d": 86_400, # 1 day → 30 buckets
|
|
"365d": 7 * 86_400, # 7 days → ~53 buckets
|
|
}
|
|
|
|
#: Human-readable bucket size label for each time-range preset.
|
|
BUCKET_SIZE_LABEL: dict[str, str] = {
|
|
"24h": "1h",
|
|
"7d": "6h",
|
|
"30d": "1d",
|
|
"365d": "7d",
|
|
}
|
|
|
|
|
|
def bucket_count(range_: TimeRange) -> int:
|
|
"""Return the number of buckets needed to cover *range_* completely.
|
|
|
|
Args:
|
|
range_: One of the supported time-range presets.
|
|
|
|
Returns:
|
|
Ceiling division of the range duration by the bucket size so that
|
|
the last bucket is included even when the window is not an exact
|
|
multiple of the bucket size.
|
|
"""
|
|
return math.ceil(TIME_RANGE_SECONDS[range_] / BUCKET_SECONDS[range_])
|