Add ruff linter/formatter for Python
- Add ruff as dev dependency - Configure ruff in pyproject.toml with strict 88-char line limit - Ignore B008 (FastAPI Depends pattern is standard) - Allow longer lines in tests for readability - Fix all lint issues in source files - Add Makefile targets: lint-backend, format-backend, fix-backend
This commit is contained in:
parent
69bc8413e0
commit
6c218130e9
31 changed files with 1234 additions and 876 deletions
|
|
@ -1,28 +1,28 @@
|
|||
"""Availability routes for admin to manage booking availability."""
|
||||
|
||||
from datetime import date, timedelta
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy import select, delete, and_
|
||||
from sqlalchemy import and_, delete, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from auth import require_permission
|
||||
from database import get_db
|
||||
from models import User, Availability, Permission
|
||||
from models import Availability, Permission, User
|
||||
from schemas import (
|
||||
TimeSlot,
|
||||
AvailabilityDay,
|
||||
AvailabilityResponse,
|
||||
SetAvailabilityRequest,
|
||||
CopyAvailabilityRequest,
|
||||
SetAvailabilityRequest,
|
||||
TimeSlot,
|
||||
)
|
||||
from shared_constants import MIN_ADVANCE_DAYS, MAX_ADVANCE_DAYS
|
||||
|
||||
from shared_constants import MAX_ADVANCE_DAYS, MIN_ADVANCE_DAYS
|
||||
|
||||
router = APIRouter(prefix="/api/admin/availability", tags=["availability"])
|
||||
|
||||
|
||||
def _get_date_range_bounds() -> tuple[date, date]:
|
||||
"""Get the valid date range for availability (using MIN_ADVANCE_DAYS to MAX_ADVANCE_DAYS)."""
|
||||
"""Get valid date range (MIN_ADVANCE_DAYS to MAX_ADVANCE_DAYS)."""
|
||||
today = date.today()
|
||||
min_date = today + timedelta(days=MIN_ADVANCE_DAYS)
|
||||
max_date = today + timedelta(days=MAX_ADVANCE_DAYS)
|
||||
|
|
@ -34,12 +34,14 @@ def _validate_date_in_range(d: date, min_date: date, max_date: date) -> None:
|
|||
if d < min_date:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Cannot set availability for past dates. Earliest allowed: {min_date}",
|
||||
detail=f"Cannot set availability for past dates. "
|
||||
f"Earliest allowed: {min_date}",
|
||||
)
|
||||
if d > max_date:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Cannot set availability more than {MAX_ADVANCE_DAYS} days ahead. Latest allowed: {max_date}",
|
||||
detail=f"Cannot set more than {MAX_ADVANCE_DAYS} days ahead. "
|
||||
f"Latest allowed: {max_date}",
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -56,7 +58,7 @@ async def get_availability(
|
|||
status_code=400,
|
||||
detail="'from' date must be before or equal to 'to' date",
|
||||
)
|
||||
|
||||
|
||||
# Query availability in range
|
||||
result = await db.execute(
|
||||
select(Availability)
|
||||
|
|
@ -64,23 +66,24 @@ async def get_availability(
|
|||
.order_by(Availability.date, Availability.start_time)
|
||||
)
|
||||
slots = result.scalars().all()
|
||||
|
||||
|
||||
# Group by date
|
||||
days_dict: dict[date, list[TimeSlot]] = {}
|
||||
for slot in slots:
|
||||
if slot.date not in days_dict:
|
||||
days_dict[slot.date] = []
|
||||
days_dict[slot.date].append(TimeSlot(
|
||||
start_time=slot.start_time,
|
||||
end_time=slot.end_time,
|
||||
))
|
||||
|
||||
days_dict[slot.date].append(
|
||||
TimeSlot(
|
||||
start_time=slot.start_time,
|
||||
end_time=slot.end_time,
|
||||
)
|
||||
)
|
||||
|
||||
# Convert to response format
|
||||
days = [
|
||||
AvailabilityDay(date=d, slots=days_dict[d])
|
||||
for d in sorted(days_dict.keys())
|
||||
AvailabilityDay(date=d, slots=days_dict[d]) for d in sorted(days_dict.keys())
|
||||
]
|
||||
|
||||
|
||||
return AvailabilityResponse(days=days)
|
||||
|
||||
|
||||
|
|
@ -93,29 +96,31 @@ async def set_availability(
|
|||
"""Set availability for a specific date. Replaces any existing availability."""
|
||||
min_date, max_date = _get_date_range_bounds()
|
||||
_validate_date_in_range(request.date, min_date, max_date)
|
||||
|
||||
|
||||
# Validate slots don't overlap
|
||||
sorted_slots = sorted(request.slots, key=lambda s: s.start_time)
|
||||
for i in range(len(sorted_slots) - 1):
|
||||
if sorted_slots[i].end_time > sorted_slots[i + 1].start_time:
|
||||
end = sorted_slots[i].end_time
|
||||
start = sorted_slots[i + 1].start_time
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Time slots overlap on {request.date}: slot ending at {sorted_slots[i].end_time} overlaps with slot starting at {sorted_slots[i + 1].start_time}. Please ensure all time slots are non-overlapping.",
|
||||
detail=f"Time slots overlap: slot ending at {end} "
|
||||
f"overlaps with slot starting at {start}",
|
||||
)
|
||||
|
||||
|
||||
# Validate each slot's end_time > start_time
|
||||
for slot in request.slots:
|
||||
if slot.end_time <= slot.start_time:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Invalid time slot on {request.date}: end time {slot.end_time} must be after start time {slot.start_time}. Please correct the time range.",
|
||||
detail=f"Invalid time slot: end time {slot.end_time} "
|
||||
f"must be after start time {slot.start_time}",
|
||||
)
|
||||
|
||||
|
||||
# Delete existing availability for this date
|
||||
await db.execute(
|
||||
delete(Availability).where(Availability.date == request.date)
|
||||
)
|
||||
|
||||
await db.execute(delete(Availability).where(Availability.date == request.date))
|
||||
|
||||
# Create new availability slots
|
||||
for slot in request.slots:
|
||||
availability = Availability(
|
||||
|
|
@ -124,9 +129,9 @@ async def set_availability(
|
|||
end_time=slot.end_time,
|
||||
)
|
||||
db.add(availability)
|
||||
|
||||
|
||||
await db.commit()
|
||||
|
||||
|
||||
return AvailabilityDay(date=request.date, slots=request.slots)
|
||||
|
||||
|
||||
|
|
@ -138,14 +143,14 @@ async def copy_availability(
|
|||
) -> AvailabilityResponse:
|
||||
"""Copy availability from one day to multiple target days."""
|
||||
min_date, max_date = _get_date_range_bounds()
|
||||
|
||||
# Validate source date is in range (for consistency, though DB query would fail anyway)
|
||||
|
||||
# Validate source date is in range
|
||||
_validate_date_in_range(request.source_date, min_date, max_date)
|
||||
|
||||
|
||||
# Validate target dates
|
||||
for target_date in request.target_dates:
|
||||
_validate_date_in_range(target_date, min_date, max_date)
|
||||
|
||||
|
||||
# Get source availability
|
||||
result = await db.execute(
|
||||
select(Availability)
|
||||
|
|
@ -153,13 +158,13 @@ async def copy_availability(
|
|||
.order_by(Availability.start_time)
|
||||
)
|
||||
source_slots = result.scalars().all()
|
||||
|
||||
|
||||
if not source_slots:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"No availability found for source date {request.source_date}",
|
||||
)
|
||||
|
||||
|
||||
# Copy to each target date within a single atomic transaction
|
||||
# All deletes and inserts happen before commit, ensuring atomicity
|
||||
copied_days: list[AvailabilityDay] = []
|
||||
|
|
@ -167,12 +172,11 @@ async def copy_availability(
|
|||
for target_date in request.target_dates:
|
||||
if target_date == request.source_date:
|
||||
continue # Skip copying to self
|
||||
|
||||
|
||||
# Delete existing availability for target date
|
||||
await db.execute(
|
||||
delete(Availability).where(Availability.date == target_date)
|
||||
)
|
||||
|
||||
del_query = delete(Availability).where(Availability.date == target_date)
|
||||
await db.execute(del_query)
|
||||
|
||||
# Copy slots
|
||||
target_slots: list[TimeSlot] = []
|
||||
for source_slot in source_slots:
|
||||
|
|
@ -182,19 +186,20 @@ async def copy_availability(
|
|||
end_time=source_slot.end_time,
|
||||
)
|
||||
db.add(new_availability)
|
||||
target_slots.append(TimeSlot(
|
||||
start_time=source_slot.start_time,
|
||||
end_time=source_slot.end_time,
|
||||
))
|
||||
|
||||
target_slots.append(
|
||||
TimeSlot(
|
||||
start_time=source_slot.start_time,
|
||||
end_time=source_slot.end_time,
|
||||
)
|
||||
)
|
||||
|
||||
copied_days.append(AvailabilityDay(date=target_date, slots=target_slots))
|
||||
|
||||
|
||||
# Commit all changes atomically
|
||||
await db.commit()
|
||||
except Exception:
|
||||
# Rollback on any error to maintain atomicity
|
||||
await db.rollback()
|
||||
raise
|
||||
|
||||
return AvailabilityResponse(days=copied_days)
|
||||
|
||||
return AvailabilityResponse(days=copied_days)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue