refactor: improve language normalization and date handling utilities
All checks were successful
CI / lint-and-test (push) Successful in 21s

- Introduced a new `normalize_lang` function to standardize language codes across the application, ensuring consistent handling of user language preferences.
- Refactored date handling utilities by adding `parse_utc_iso` and `parse_utc_iso_naive` functions for better parsing of ISO 8601 date strings, enhancing timezone awareness.
- Updated various modules to utilize the new language normalization and date parsing functions, improving code clarity and maintainability.
- Enhanced error handling in date validation to raise specific `DateRangeValidationError` exceptions, providing clearer feedback on validation issues.
- Improved test coverage for date range validation and language normalization functionalities, ensuring robustness and reliability.
This commit is contained in:
2026-02-20 22:42:54 +03:00
parent f53ef81306
commit d02d0a1835
19 changed files with 216 additions and 158 deletions

View File

@@ -1,7 +1,8 @@
"""Date and ISO helpers for duty ranges and API validation."""
import re
from datetime import date, datetime, timezone
from datetime import date, datetime, timedelta, timezone
from typing import Literal
def day_start_iso(d: date) -> str:
@@ -23,6 +24,57 @@ def duty_to_iso(d: date, hour_utc: int, minute_utc: int) -> str:
_ISO_DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
class DateRangeValidationError(ValueError):
"""Raised when from_date/to_date validation fails. API uses kind for i18n key."""
def __init__(self, kind: Literal["bad_format", "from_after_to"]) -> None:
self.kind = kind
super().__init__(kind)
def to_date_exclusive_iso(to_date: str) -> str:
"""Return the day after to_date in YYYY-MM-DD for exclusive range end.
Use for queries like start_at < to_date_next (e.g. filter end before next day).
Args:
to_date: End date in YYYY-MM-DD.
Returns:
(to_date + 1 day) in YYYY-MM-DD.
"""
dt = datetime.fromisoformat(to_date + "T00:00:00") + timedelta(days=1)
return dt.strftime("%Y-%m-%d")
def parse_utc_iso(iso_str: str) -> datetime:
"""Parse UTC ISO 8601 string with Z suffix to timezone-aware datetime (UTC).
Args:
iso_str: e.g. '2025-01-15T09:00:00Z'.
Returns:
Timezone-aware datetime in UTC.
"""
normalized = (iso_str or "").strip().replace("Z", "+00:00")
return datetime.fromisoformat(normalized)
def parse_utc_iso_naive(iso_str: str) -> datetime:
"""Parse UTC ISO 8601 string with Z to naive datetime in UTC.
Use for job queue or code that expects naive UTC.
Args:
iso_str: e.g. '2025-01-15T09:00:00Z'.
Returns:
Naive datetime with same UTC wall time.
"""
dt = parse_utc_iso(iso_str)
return dt.replace(tzinfo=None)
def parse_iso_date(s: str) -> date | None:
"""Parse YYYY-MM-DD string to date. Returns None if invalid."""
if not s or not _ISO_DATE_RE.match(s.strip()):
@@ -34,8 +86,12 @@ def parse_iso_date(s: str) -> date | None:
def validate_date_range(from_date: str, to_date: str) -> None:
"""Validate from_date and to_date are YYYY-MM-DD and from_date <= to_date. Raises ValueError if invalid."""
"""Validate from_date and to_date are YYYY-MM-DD and from_date <= to_date.
Raises:
DateRangeValidationError: bad_format if format invalid, from_after_to if from > to.
"""
if not _ISO_DATE_RE.match(from_date or "") or not _ISO_DATE_RE.match(to_date or ""):
raise ValueError("Параметры from и to должны быть в формате YYYY-MM-DD")
raise DateRangeValidationError("bad_format")
if from_date > to_date:
raise ValueError("Дата from не должна быть позже to")
raise DateRangeValidationError("from_after_to")