Files
duty-teller/duty_teller/importers/duty_schedule.py
Nikolay Tatarinov 86f6d66865
All checks were successful
CI / lint-and-test (push) Successful in 17s
chore: add changelog and documentation updates
- Created a new `CHANGELOG.md` file to document all notable changes to the project, adhering to the Keep a Changelog format.
- Updated `CONTRIBUTING.md` to include instructions for building and previewing documentation using MkDocs.
- Added `mkdocs.yml` configuration for documentation generation, including navigation structure and theme settings.
- Enhanced various documentation files, including API reference, architecture overview, configuration reference, and runbook, to provide comprehensive guidance for users and developers.
- Included new sections in the README for changelog and documentation links, improving accessibility to project information.
2026-02-20 15:32:10 +03:00

124 lines
4.1 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 into DutyScheduleResult.
Expects meta.start_date (YYYY-MM-DD) and schedule (array). For each schedule
item: name (required), duty string with ';' separator; index i = start_date + i days.
Cell values: в/В/б/Б => duty, Н => unavailable, О => vacation; rest ignored.
Args:
raw_bytes: UTF-8 encoded JSON bytes.
Returns:
DutyScheduleResult with start_date, end_date, and entries (per-person dates).
Raises:
DutyScheduleParseError: On invalid JSON, missing/invalid meta or schedule,
or invalid item fields.
"""
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)