Release v0.1.5
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user