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:
106
api/app.py
106
api/app.py
@@ -1,59 +1,78 @@
|
||||
"""FastAPI app: /api/duties and static webapp."""
|
||||
|
||||
import logging
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Annotated, Generator
|
||||
|
||||
import config
|
||||
from fastapi import FastAPI, Header, HTTPException, Query, Request
|
||||
from fastapi import Depends, FastAPI, Header, HTTPException, Query, Request
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from db.session import session_scope
|
||||
from db.repository import get_duties
|
||||
from db.schemas import DutyWithUser, CalendarEvent
|
||||
from api.telegram_auth import validate_init_data_with_reason
|
||||
from api.calendar_ics import get_calendar_events
|
||||
from utils.dates import validate_date_range
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# ISO date YYYY-MM-DD
|
||||
_DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
|
||||
|
||||
|
||||
def _validate_duty_dates(from_date: str, to_date: str) -> None:
|
||||
"""Raise HTTPException 400 if dates are invalid or from_date > to_date."""
|
||||
if not _DATE_RE.match(from_date) or not _DATE_RE.match(to_date):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Параметры from и to должны быть в формате YYYY-MM-DD",
|
||||
)
|
||||
if from_date > to_date:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Дата from не должна быть позже to",
|
||||
)
|
||||
try:
|
||||
validate_date_range(from_date, to_date)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e)) from e
|
||||
|
||||
|
||||
def _fetch_duties_response(from_date: str, to_date: str) -> list[DutyWithUser]:
|
||||
"""Fetch duties in range and return list of DutyWithUser. Uses config.DATABASE_URL."""
|
||||
def get_validated_dates(
|
||||
from_date: str = Query(..., description="ISO date YYYY-MM-DD", alias="from"),
|
||||
to_date: str = Query(..., description="ISO date YYYY-MM-DD", alias="to"),
|
||||
) -> tuple[str, str]:
|
||||
"""FastAPI dependency: validate from_date/to_date and return (from_date, to_date). Raises 400 if invalid."""
|
||||
_validate_duty_dates(from_date, to_date)
|
||||
return (from_date, to_date)
|
||||
|
||||
|
||||
def get_db_session() -> Generator[Session, None, None]:
|
||||
"""FastAPI dependency: yield a DB session from session_scope."""
|
||||
with session_scope(config.DATABASE_URL) as session:
|
||||
rows = get_duties(session, from_date=from_date, to_date=to_date)
|
||||
return [
|
||||
DutyWithUser(
|
||||
id=duty.id,
|
||||
user_id=duty.user_id,
|
||||
start_at=duty.start_at,
|
||||
end_at=duty.end_at,
|
||||
full_name=full_name,
|
||||
event_type=(
|
||||
duty.event_type
|
||||
if duty.event_type in ("duty", "unavailable", "vacation")
|
||||
else "duty"
|
||||
),
|
||||
)
|
||||
for duty, full_name in rows
|
||||
]
|
||||
yield session
|
||||
|
||||
|
||||
def require_miniapp_username(
|
||||
request: Request,
|
||||
x_telegram_init_data: Annotated[
|
||||
str | None, Header(alias="X-Telegram-Init-Data")
|
||||
] = None,
|
||||
) -> str:
|
||||
"""FastAPI dependency: return authenticated username or raise 403."""
|
||||
return get_authenticated_username(request, x_telegram_init_data)
|
||||
|
||||
|
||||
def _fetch_duties_response(
|
||||
session: Session, from_date: str, to_date: str
|
||||
) -> list[DutyWithUser]:
|
||||
"""Fetch duties in range and return list of DutyWithUser."""
|
||||
rows = get_duties(session, from_date=from_date, to_date=to_date)
|
||||
return [
|
||||
DutyWithUser(
|
||||
id=duty.id,
|
||||
user_id=duty.user_id,
|
||||
start_at=duty.start_at,
|
||||
end_at=duty.end_at,
|
||||
full_name=full_name,
|
||||
event_type=(
|
||||
duty.event_type
|
||||
if duty.event_type in ("duty", "unavailable", "vacation")
|
||||
else "duty"
|
||||
),
|
||||
)
|
||||
for duty, full_name in rows
|
||||
]
|
||||
|
||||
|
||||
def _auth_error_detail(auth_reason: str) -> str:
|
||||
@@ -131,33 +150,30 @@ app.add_middleware(
|
||||
@app.get("/api/duties", response_model=list[DutyWithUser])
|
||||
def list_duties(
|
||||
request: Request,
|
||||
from_date: str = Query(..., description="ISO date YYYY-MM-DD", alias="from"),
|
||||
to_date: str = Query(..., description="ISO date YYYY-MM-DD", alias="to"),
|
||||
x_telegram_init_data: str | None = Header(None, alias="X-Telegram-Init-Data"),
|
||||
dates: tuple[str, str] = Depends(get_validated_dates),
|
||||
_username: str = Depends(require_miniapp_username),
|
||||
session: Session = Depends(get_db_session),
|
||||
) -> list[DutyWithUser]:
|
||||
_validate_duty_dates(from_date, to_date)
|
||||
from_date_val, to_date_val = dates
|
||||
log.info(
|
||||
"GET /api/duties from %s, has initData: %s",
|
||||
request.client.host if request.client else "?",
|
||||
bool((x_telegram_init_data or "").strip()),
|
||||
)
|
||||
get_authenticated_username(request, x_telegram_init_data)
|
||||
return _fetch_duties_response(from_date, to_date)
|
||||
return _fetch_duties_response(session, from_date_val, to_date_val)
|
||||
|
||||
|
||||
@app.get("/api/calendar-events", response_model=list[CalendarEvent])
|
||||
def list_calendar_events(
|
||||
request: Request,
|
||||
from_date: str = Query(..., description="ISO date YYYY-MM-DD", alias="from"),
|
||||
to_date: str = Query(..., description="ISO date YYYY-MM-DD", alias="to"),
|
||||
x_telegram_init_data: str | None = Header(None, alias="X-Telegram-Init-Data"),
|
||||
dates: tuple[str, str] = Depends(get_validated_dates),
|
||||
_username: str = Depends(require_miniapp_username),
|
||||
) -> list[CalendarEvent]:
|
||||
_validate_duty_dates(from_date, to_date)
|
||||
get_authenticated_username(request, x_telegram_init_data)
|
||||
from_date_val, to_date_val = dates
|
||||
url = config.EXTERNAL_CALENDAR_ICS_URL
|
||||
if not url:
|
||||
return []
|
||||
events = get_calendar_events(url, from_date=from_date, to_date=to_date)
|
||||
events = get_calendar_events(url, from_date=from_date_val, to_date=to_date_val)
|
||||
return [CalendarEvent(date=e["date"], summary=e["summary"]) for e in events]
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user