Add configuration rules, refactor settings management, and enhance import functionality
- Introduced a new configuration file `.cursorrules` to define coding standards, error handling, testing requirements, and project-specific guidelines. - Refactored `config.py` to implement a `Settings` dataclass for better management of environment variables, improving testability and maintainability. - Updated the import duty schedule handler to utilize session management with `session_scope`, ensuring proper database session handling. - Enhanced the import service to streamline the duty schedule import process, improving code organization and readability. - Added new service layer functions to encapsulate business logic related to group duty pinning and duty schedule imports. - Updated README documentation to reflect the new configuration structure and improved import functionality.
This commit is contained in:
24
utils/__init__.py
Normal file
24
utils/__init__.py
Normal file
@@ -0,0 +1,24 @@
|
||||
"""Shared utilities: date/ISO helpers, user display names, handover time parsing.
|
||||
|
||||
Used by handlers, API, and services. No DB or Telegram dependencies.
|
||||
"""
|
||||
|
||||
from utils.dates import (
|
||||
day_end_iso,
|
||||
day_start_iso,
|
||||
duty_to_iso,
|
||||
parse_iso_date,
|
||||
validate_date_range,
|
||||
)
|
||||
from utils.user import build_full_name
|
||||
from utils.handover import parse_handover_time
|
||||
|
||||
__all__ = [
|
||||
"day_start_iso",
|
||||
"day_end_iso",
|
||||
"duty_to_iso",
|
||||
"parse_iso_date",
|
||||
"validate_date_range",
|
||||
"build_full_name",
|
||||
"parse_handover_time",
|
||||
]
|
||||
46
utils/dates.py
Normal file
46
utils/dates.py
Normal file
@@ -0,0 +1,46 @@
|
||||
"""Date and ISO helpers for duty ranges and API validation."""
|
||||
|
||||
import re
|
||||
from datetime import date, datetime, timezone
|
||||
|
||||
|
||||
def day_start_iso(d: date) -> str:
|
||||
"""ISO 8601 start of calendar day UTC: YYYY-MM-DDT00:00:00Z."""
|
||||
return d.isoformat() + "T00:00:00Z"
|
||||
|
||||
|
||||
def day_end_iso(d: date) -> str:
|
||||
"""ISO 8601 end of calendar day UTC: YYYY-MM-DDT23:59:59Z."""
|
||||
return d.isoformat() + "T23:59:59Z"
|
||||
|
||||
|
||||
def duty_to_iso(d: date, hour_utc: int, minute_utc: int) -> str:
|
||||
"""ISO 8601 with Z for start of duty on date d at given UTC time."""
|
||||
dt = datetime(d.year, d.month, d.day, hour_utc, minute_utc, 0, tzinfo=timezone.utc)
|
||||
return dt.strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
|
||||
# ISO date YYYY-MM-DD
|
||||
_ISO_DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
|
||||
|
||||
|
||||
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()):
|
||||
return None
|
||||
try:
|
||||
return date.fromisoformat(s.strip())
|
||||
except ValueError:
|
||||
return 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: With a user-facing message if invalid.
|
||||
"""
|
||||
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")
|
||||
if from_date > to_date:
|
||||
raise ValueError("Дата from не должна быть позже to")
|
||||
37
utils/handover.py
Normal file
37
utils/handover.py
Normal file
@@ -0,0 +1,37 @@
|
||||
"""Handover time parsing for duty schedule import."""
|
||||
|
||||
import re
|
||||
from datetime import datetime, timezone
|
||||
|
||||
# HH:MM or HH:MM:SS, optional space + timezone (IANA or "UTC")
|
||||
HANDOVER_TIME_RE = re.compile(
|
||||
r"^\s*(\d{1,2}):(\d{2})(?::(\d{2}))?\s*(?:\s+(\S+))?\s*$", re.IGNORECASE
|
||||
)
|
||||
|
||||
|
||||
def parse_handover_time(text: str) -> tuple[int, int] | None:
|
||||
"""Parse handover time string to (hour_utc, minute_utc). Returns None on failure."""
|
||||
m = HANDOVER_TIME_RE.match(text)
|
||||
if not m:
|
||||
return None
|
||||
hour = int(m.group(1))
|
||||
minute = int(m.group(2))
|
||||
# second = m.group(3) ignored
|
||||
tz_str = (m.group(4) or "").strip()
|
||||
if not tz_str or tz_str.upper() == "UTC":
|
||||
return (hour % 24, minute)
|
||||
try:
|
||||
from zoneinfo import ZoneInfo
|
||||
except ImportError:
|
||||
try:
|
||||
from backports.zoneinfo import ZoneInfo # type: ignore
|
||||
except ImportError:
|
||||
return None
|
||||
try:
|
||||
tz = ZoneInfo(tz_str)
|
||||
except Exception:
|
||||
return None
|
||||
# Build datetime in that tz and convert to UTC
|
||||
dt = datetime(2000, 1, 1, hour, minute, 0, tzinfo=tz)
|
||||
utc = dt.astimezone(timezone.utc)
|
||||
return (utc.hour, utc.minute)
|
||||
8
utils/user.py
Normal file
8
utils/user.py
Normal file
@@ -0,0 +1,8 @@
|
||||
"""User display name helpers."""
|
||||
|
||||
|
||||
def build_full_name(first_name: str | None, last_name: str | None) -> str:
|
||||
"""Build display full name from first and last name. Returns 'User' if both empty."""
|
||||
parts = [first_name or "", last_name or ""]
|
||||
full = " ".join(filter(None, parts)).strip()
|
||||
return full or "User"
|
||||
Reference in New Issue
Block a user