refactor: improve language normalization and date handling utilities
All checks were successful
CI / lint-and-test (push) Successful in 21s
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:
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user