Release v0.1.5

This commit is contained in:
2026-07-07 15:49:06 +02:00
parent 3bcb04ff76
commit b328a6d649
21 changed files with 5718 additions and 714 deletions

View File

@@ -2,13 +2,23 @@ from __future__ import annotations
from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException, Query, Response, status
from fastapi import APIRouter, Body, Depends, HTTPException, Query, Response, status
from sqlalchemy.orm import Session
from govoplan_access.backend.auth.dependencies import ApiPrincipal, get_api_principal, has_scope
from govoplan_calendar.backend.ical import ICalendarError, event_to_ics, http_last_modified
from govoplan_calendar.backend.caldav import CalDAVError
from govoplan_calendar.backend.schemas import (
CalendarCalDavDueSyncItemResponse,
CalendarCalDavDueSyncResponse,
CalendarCalDavSourceCreateRequest,
CalendarCalDavSourceListResponse,
CalendarCalDavSourceResponse,
CalendarCalDavSourceUpdateRequest,
CalendarCalDavSyncRequest,
CalendarCalDavSyncResponse,
CalendarCollectionCreateRequest,
CalendarCollectionDeleteRequest,
CalendarCollectionListResponse,
CalendarCollectionResponse,
CalendarCollectionUpdateRequest,
@@ -16,21 +26,33 @@ from govoplan_calendar.backend.schemas import (
CalendarEventListResponse,
CalendarEventResponse,
CalendarEventUpdateRequest,
CalendarFreeBusyRequest,
CalendarFreeBusyResponse,
CalendarIcsImportRequest,
)
from govoplan_calendar.backend.service import (
CalendarError,
caldav_source_response,
caldav_sync_response,
calendar_response,
create_calendar,
create_caldav_source,
create_event,
delete_calendar,
delete_caldav_source,
delete_event,
event_response,
get_caldav_source,
get_event,
import_ics_event,
list_freebusy,
list_caldav_sources,
list_calendars,
list_events,
sync_due_caldav_sources,
sync_caldav_source,
update_calendar,
update_caldav_source,
update_event,
)
from govoplan_core.db.session import get_session
@@ -56,6 +78,10 @@ def _event_response(event) -> CalendarEventResponse:
return CalendarEventResponse.model_validate(event_response(event))
def _caldav_source_response(source) -> CalendarCalDavSourceResponse:
return CalendarCalDavSourceResponse.model_validate(caldav_source_response(source))
@router.get("/calendars", response_model=CalendarCollectionListResponse)
def api_list_calendars(
principal: ApiPrincipal = Depends(get_api_principal),
@@ -105,12 +131,13 @@ def api_update_calendar(
@router.delete("/calendars/{calendar_id}", status_code=status.HTTP_204_NO_CONTENT)
def api_delete_calendar(
calendar_id: str,
payload: CalendarCollectionDeleteRequest | None = Body(default=None),
principal: ApiPrincipal = Depends(get_api_principal),
session: Session = Depends(get_session),
):
_require_scope(principal, "calendar:calendar:admin")
try:
delete_calendar(session, tenant_id=principal.tenant_id, calendar_id=calendar_id)
delete_calendar(session, tenant_id=principal.tenant_id, calendar_id=calendar_id, payload=payload)
session.commit()
return Response(status_code=status.HTTP_204_NO_CONTENT)
except CalendarError as exc:
@@ -118,6 +145,122 @@ def api_delete_calendar(
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc)) from exc
@router.get("/caldav/sources", response_model=CalendarCalDavSourceListResponse)
def api_list_caldav_sources(
calendar_id: str | None = None,
principal: ApiPrincipal = Depends(get_api_principal),
session: Session = Depends(get_session),
):
_require_scope(principal, "calendar:calendar:read")
sources = list_caldav_sources(session, tenant_id=principal.tenant_id, calendar_id=calendar_id)
return CalendarCalDavSourceListResponse(sources=[_caldav_source_response(source) for source in sources])
@router.post("/caldav/sources", response_model=CalendarCalDavSourceResponse, status_code=status.HTTP_201_CREATED)
def api_create_caldav_source(
payload: CalendarCalDavSourceCreateRequest,
principal: ApiPrincipal = Depends(get_api_principal),
session: Session = Depends(get_session),
):
_require_scope(principal, "calendar:calendar:admin")
try:
source = create_caldav_source(session, tenant_id=principal.tenant_id, user_id=principal.user.id, payload=payload)
session.commit()
session.refresh(source)
return _caldav_source_response(source)
except CalendarError as exc:
session.rollback()
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc)) from exc
@router.patch("/caldav/sources/{source_id}", response_model=CalendarCalDavSourceResponse)
def api_update_caldav_source(
source_id: str,
payload: CalendarCalDavSourceUpdateRequest,
principal: ApiPrincipal = Depends(get_api_principal),
session: Session = Depends(get_session),
):
_require_scope(principal, "calendar:calendar:admin")
try:
source = update_caldav_source(session, tenant_id=principal.tenant_id, source_id=source_id, payload=payload)
session.commit()
session.refresh(source)
return _caldav_source_response(source)
except CalendarError as exc:
session.rollback()
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc)) from exc
@router.delete("/caldav/sources/{source_id}", status_code=status.HTTP_204_NO_CONTENT)
def api_delete_caldav_source(
source_id: str,
principal: ApiPrincipal = Depends(get_api_principal),
session: Session = Depends(get_session),
):
_require_scope(principal, "calendar:calendar:admin")
try:
delete_caldav_source(session, tenant_id=principal.tenant_id, source_id=source_id)
session.commit()
return Response(status_code=status.HTTP_204_NO_CONTENT)
except CalendarError as exc:
session.rollback()
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
@router.post("/caldav/sources/{source_id}/sync", response_model=CalendarCalDavSyncResponse)
def api_sync_caldav_source(
source_id: str,
payload: CalendarCalDavSyncRequest | None = None,
principal: ApiPrincipal = Depends(get_api_principal),
session: Session = Depends(get_session),
):
_require_scope(principal, "calendar:event:import")
payload = payload or CalendarCalDavSyncRequest()
try:
source, stats = sync_caldav_source(
session,
tenant_id=principal.tenant_id,
user_id=principal.user.id,
source_id=source_id,
password=payload.password,
bearer_token=payload.bearer_token,
force_full=payload.force_full,
)
session.commit()
session.refresh(source)
return CalendarCalDavSyncResponse.model_validate(caldav_sync_response(source, stats))
except (CalendarError, CalDAVError, ICalendarError) as exc:
session.rollback()
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc)) from exc
@router.post("/caldav/sync-due", response_model=CalendarCalDavDueSyncResponse)
def api_sync_due_caldav_sources(
limit: int = Query(default=50, ge=1, le=200),
principal: ApiPrincipal = Depends(get_api_principal),
session: Session = Depends(get_session),
):
_require_scope(principal, "calendar:event:import")
results = sync_due_caldav_sources(session, tenant_id=principal.tenant_id, user_id=principal.user.id, limit=limit)
session.commit()
return CalendarCalDavDueSyncResponse(
results=[
CalendarCalDavDueSyncItemResponse(
source_id=item.source_id,
calendar_id=item.calendar_id,
status=item.status,
error=item.error,
created=item.stats.created if item.stats else 0,
updated=item.stats.updated if item.stats else 0,
deleted=item.stats.deleted if item.stats else 0,
unchanged=item.stats.unchanged if item.stats else 0,
fetched=item.stats.fetched if item.stats else 0,
)
for item in results
]
)
@router.get("/events", response_model=CalendarEventListResponse)
def api_list_events(
calendar_id: str | None = None,
@@ -242,3 +385,23 @@ def api_export_ics_event(
"Last-Modified": http_last_modified(event.updated_at),
},
)
@router.post("/availability/freebusy", response_model=CalendarFreeBusyResponse)
def api_freebusy(
payload: CalendarFreeBusyRequest,
principal: ApiPrincipal = Depends(get_api_principal),
session: Session = Depends(get_session),
):
_require_scope(principal, "calendar:availability:read")
try:
busy = list_freebusy(
session,
tenant_id=principal.tenant_id,
start_at=payload.start_at,
end_at=payload.end_at,
calendar_ids=payload.calendar_ids,
)
return CalendarFreeBusyResponse(start_at=payload.start_at, end_at=payload.end_at, busy=busy)
except CalendarError as exc:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc)) from exc