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

@@ -2,12 +2,13 @@
import hashlib
import secrets
from datetime import datetime, timedelta, timezone
from datetime import datetime, timezone
from sqlalchemy.orm import Session
import duty_teller.config as config
from duty_teller.db.models import User, Duty, GroupDutyPin, CalendarSubscriptionToken
from duty_teller.utils.dates import parse_utc_iso_naive, to_date_exclusive_iso
def get_user_by_telegram_id(session: Session, telegram_user_id: int) -> User | None:
@@ -168,9 +169,7 @@ def delete_duties_in_range(
Returns:
Number of duties deleted.
"""
to_next = (
datetime.fromisoformat(to_date + "T00:00:00") + timedelta(days=1)
).strftime("%Y-%m-%d")
to_next = to_date_exclusive_iso(to_date)
q = session.query(Duty).filter(
Duty.user_id == user_id,
Duty.start_at < to_next,
@@ -197,9 +196,7 @@ def get_duties(
Returns:
List of (Duty, full_name) tuples.
"""
to_date_next = (
datetime.fromisoformat(to_date + "T00:00:00") + timedelta(days=1)
).strftime("%Y-%m-%d")
to_date_next = to_date_exclusive_iso(to_date)
q = (
session.query(Duty, User.full_name)
.join(User, Duty.user_id == User.id)
@@ -230,9 +227,7 @@ def get_duties_for_user(
Returns:
List of (Duty, full_name) tuples.
"""
to_date_next = (
datetime.fromisoformat(to_date + "T00:00:00") + timedelta(days=1)
).strftime("%Y-%m-%d")
to_date_next = to_date_exclusive_iso(to_date)
filters = [
Duty.user_id == user_id,
Duty.start_at < to_date_next,
@@ -392,9 +387,7 @@ def get_next_shift_end(session: Session, after_utc: datetime) -> datetime | None
.first()
)
if current:
return datetime.fromisoformat(current.end_at.replace("Z", "+00:00")).replace(
tzinfo=None
)
return parse_utc_iso_naive(current.end_at)
next_duty = (
session.query(Duty)
.filter(Duty.event_type == "duty", Duty.start_at > after_iso)
@@ -402,9 +395,7 @@ def get_next_shift_end(session: Session, after_utc: datetime) -> datetime | None
.first()
)
if next_duty:
return datetime.fromisoformat(next_duty.end_at.replace("Z", "+00:00")).replace(
tzinfo=None
)
return parse_utc_iso_naive(next_duty.end_at)
return None

View File

@@ -4,6 +4,9 @@ from typing import Literal
from pydantic import BaseModel, ConfigDict
# Allowed duty event types; API maps unknown DB values to "duty".
DUTY_EVENT_TYPES = ("duty", "unavailable", "vacation")
class UserBase(BaseModel):
"""Base user fields (full_name, username, first/last name)."""