Files
duty-teller/duty_teller/importers/duty_schedule.py
Nikolay Tatarinov 28973489a5 Refactor project structure and enhance Docker configuration
- Updated `.dockerignore` to exclude test and development artifacts, optimizing the Docker image size.
- Refactored `main.py` to delegate execution to `duty_teller.run.main()`, simplifying the entry point.
- Introduced a new `duty_teller` package to encapsulate core functionality, improving modularity and organization.
- Enhanced `pyproject.toml` to define a script for running the application, streamlining the execution process.
- Updated README documentation to reflect changes in project structure and usage instructions.
- Improved Alembic environment configuration to utilize the new package structure for database migrations.
2026-02-18 13:03:14 +03:00

115 lines
3.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Parser for duty-schedule JSON format. No DB access."""
import json
from dataclasses import dataclass
from datetime import date, timedelta
# Символы дежурства в ячейке duty (CSV с разделителем ;)
DUTY_MARKERS = frozenset({"б", "Б", "в", "В"})
UNAVAILABLE_MARKER = "Н"
VACATION_MARKER = "О"
@dataclass
class DutyScheduleEntry:
"""One person's schedule: full_name and three lists of dates by event type."""
full_name: str
duty_dates: list[date]
unavailable_dates: list[date]
vacation_dates: list[date]
@dataclass
class DutyScheduleResult:
"""Parsed duty schedule: start_date, end_date, and per-person entries."""
start_date: date
end_date: date
entries: list[DutyScheduleEntry]
class DutyScheduleParseError(Exception):
"""Invalid or missing fields in duty-schedule JSON."""
pass
def parse_duty_schedule(raw_bytes: bytes) -> DutyScheduleResult:
"""Parse duty-schedule JSON. Returns start_date, end_date, and list of DutyScheduleEntry.
- meta.start_date (YYYY-MM-DD) and schedule (array) required.
- meta.weeks optional; number of days from max duty string length (split by ';').
- For each schedule item: name (required), duty = CSV with ';'; index i = start_date + i days.
- Cell value after strip: в/В/б/Б => duty, Н => unavailable, О => vacation; rest ignored.
"""
try:
data = json.loads(raw_bytes.decode("utf-8"))
except (json.JSONDecodeError, UnicodeDecodeError) as e:
raise DutyScheduleParseError(f"Invalid JSON or encoding: {e}") from e
meta = data.get("meta")
if not meta or not isinstance(meta, dict):
raise DutyScheduleParseError("Missing or invalid 'meta'")
start_str = meta.get("start_date")
if not start_str or not isinstance(start_str, str):
raise DutyScheduleParseError("Missing or invalid meta.start_date")
try:
start_date = date.fromisoformat(start_str.strip())
except ValueError as e:
raise DutyScheduleParseError(f"Invalid meta.start_date: {start_str}") from e
schedule = data.get("schedule")
if not isinstance(schedule, list):
raise DutyScheduleParseError("Missing or invalid 'schedule' (must be array)")
max_days = 0
entries: list[DutyScheduleEntry] = []
for row in schedule:
if not isinstance(row, dict):
raise DutyScheduleParseError("schedule item must be an object")
name = row.get("name")
if name is None or not isinstance(name, str):
raise DutyScheduleParseError("schedule item must have 'name' (string)")
full_name = name.strip()
if not full_name:
raise DutyScheduleParseError("schedule item 'name' cannot be empty")
duty_str = row.get("duty")
if duty_str is None:
duty_str = ""
if not isinstance(duty_str, str):
raise DutyScheduleParseError("schedule item 'duty' must be string")
cells = [c.strip() for c in duty_str.split(";")]
max_days = max(max_days, len(cells))
duty_dates: list[date] = []
unavailable_dates: list[date] = []
vacation_dates: list[date] = []
for i, cell in enumerate(cells):
d = start_date + timedelta(days=i)
if cell in DUTY_MARKERS:
duty_dates.append(d)
elif cell == UNAVAILABLE_MARKER:
unavailable_dates.append(d)
elif cell == VACATION_MARKER:
vacation_dates.append(d)
entries.append(
DutyScheduleEntry(
full_name=full_name,
duty_dates=duty_dates,
unavailable_dates=unavailable_dates,
vacation_dates=vacation_dates,
)
)
if max_days == 0:
end_date = start_date
else:
end_date = start_date + timedelta(days=max_days - 1)
return DutyScheduleResult(start_date=start_date, end_date=end_date, entries=entries)