Files
microsoft-365-service/app/routes.py
T
JiriUhlir fb97a8bf63 Parametr select u vypisu, telo udalosti v calendarView
- calendar/view: include_body a body_type (html, text), telo udalosti
  rovnou ve vypisu bez dalsiho volani
- select u vypisu mailu, udalosti, calendarView, jedne udalosti a skupin;
  nahrazuje vychozi sadu poli
- app/graph_fields.py: vychozi sady a seznamy dostupnych poli, Swagger
  je zobrazuje v popisu parametru
- README: endpointy, popis select, zaznam zmen
2026-09-23 06:51:05 +02:00

312 lines
11 KiB
Python

from typing import Any
from urllib.parse import urlencode
from fastapi import APIRouter, Body, Depends, Header, HTTPException, Query, Response, status
from .config import Settings
from .credentials import get_request_settings
from .graph_client import MicrosoftGraphClient
from .graph_fields import (
EVENT_DEFAULT_SELECT,
EVENT_FIELDS,
GROUP_DEFAULT_SELECT,
GROUP_FIELDS,
MESSAGE_DEFAULT_SELECT,
MESSAGE_FIELDS,
select_description,
)
from .schemas import (
CalendarEventRequest,
CalendarEventUpdateRequest,
DriveUploadRequest,
PlannerTaskCreateRequest,
PlannerTaskUpdateRequest,
ScheduleRequest,
SendMailRequest,
)
from .services import CalendarService, DriveService, GroupsService, MailService, PlannerService, UsersService
router = APIRouter(tags=["microsoft365"])
def get_graph_client(request_settings: Settings = Depends(get_request_settings)) -> MicrosoftGraphClient:
return MicrosoftGraphClient(request_settings)
def get_users_service(graph: MicrosoftGraphClient = Depends(get_graph_client)) -> UsersService:
return UsersService(graph)
def get_mail_service(graph: MicrosoftGraphClient = Depends(get_graph_client)) -> MailService:
return MailService(graph)
def get_calendar_service(graph: MicrosoftGraphClient = Depends(get_graph_client)) -> CalendarService:
return CalendarService(graph)
def get_drive_service(graph: MicrosoftGraphClient = Depends(get_graph_client)) -> DriveService:
return DriveService(graph)
def get_groups_service(graph: MicrosoftGraphClient = Depends(get_graph_client)) -> GroupsService:
return GroupsService(graph)
def get_planner_service(graph: MicrosoftGraphClient = Depends(get_graph_client)) -> PlannerService:
return PlannerService(graph)
EVENT_SELECT_QUERY = Query(None, description=select_description(EVENT_DEFAULT_SELECT, EVENT_FIELDS))
MESSAGE_SELECT_QUERY = Query(None, description=select_description(MESSAGE_DEFAULT_SELECT, MESSAGE_FIELDS))
GROUP_SELECT_QUERY = Query(None, description=select_description(GROUP_DEFAULT_SELECT, GROUP_FIELDS))
@router.get("/status")
def microsoft365_status(request_settings: Settings = Depends(get_request_settings)) -> dict[str, Any]:
return {
"configured": request_settings.is_configured,
"tenant_id": bool(request_settings.tenant_id),
"client_id": bool(request_settings.client_id),
"client_secret": bool(request_settings.client_secret),
"graph_base_url": request_settings.graph_base_url,
}
@router.get("/admin-consent/url")
def admin_consent_url(
redirect_uri: str = Query(..., description="HTTPS callback URL registered on the Entra app registration."),
state: str | None = Query(None, description="Opaque value returned to the callback, e.g. customer id."),
request_settings: Settings = Depends(get_request_settings),
) -> dict[str, Any]:
"""Build the admin consent URL a client tenant administrator must open to approve the multitenant app."""
if not request_settings.tenant_id or not request_settings.client_id:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Tenant id and client id are required to build the admin consent URL.",
)
query: dict[str, str] = {
"client_id": request_settings.client_id,
"scope": request_settings.graph_scope,
"redirect_uri": redirect_uri,
}
if state:
query["state"] = state
return {
"tenant_id": request_settings.tenant_id,
"url": f"https://login.microsoftonline.com/{request_settings.tenant_id}/v2.0/adminconsent?{urlencode(query)}",
}
@router.get("/users")
async def list_users(
top: int = Query(25, ge=1, le=999),
search: str | None = None,
service: UsersService = Depends(get_users_service),
) -> Any:
return await service.list_users(top=top, search=search)
@router.get("/users/{user_id}")
async def get_user(user_id: str, service: UsersService = Depends(get_users_service)) -> Any:
return await service.get_user(user_id)
@router.get("/users/{user_id}/mail/messages")
async def list_messages(
user_id: str,
folder: str = "Inbox",
top: int = Query(25, ge=1, le=100),
select: str | None = MESSAGE_SELECT_QUERY,
service: MailService = Depends(get_mail_service),
) -> Any:
return await service.list_messages(user_id=user_id, folder=folder, top=top, select=select)
@router.post("/users/{user_id}/mail/send", status_code=status.HTTP_202_ACCEPTED)
async def send_mail(
user_id: str,
request: SendMailRequest,
response: Response,
service: MailService = Depends(get_mail_service),
) -> None:
await service.send_mail(user_id=user_id, request=request)
response.status_code = status.HTTP_202_ACCEPTED
@router.get("/users/{user_id}/calendar")
async def get_calendar(user_id: str, service: CalendarService = Depends(get_calendar_service)) -> Any:
"""user_id is a Graph user id or the mailbox address, e.g. servis@firma.cz."""
return await service.get_calendar(user_id=user_id)
@router.get("/users/{user_id}/calendar/events")
async def list_events(
user_id: str,
top: int = Query(25, ge=1, le=100),
select: str | None = EVENT_SELECT_QUERY,
service: CalendarService = Depends(get_calendar_service),
) -> Any:
return await service.list_events(user_id=user_id, top=top, select=select)
@router.post("/users/{user_id}/calendar/events", status_code=status.HTTP_201_CREATED)
async def create_event(
user_id: str,
request: CalendarEventRequest,
service: CalendarService = Depends(get_calendar_service),
) -> Any:
return await service.create_event(user_id=user_id, request=request)
@router.get("/users/{user_id}/calendar/view")
async def list_calendar_view(
user_id: str,
start: str = Query(..., description="ISO 8601 with offset, e.g. 2026-09-08T08:00:00+02:00."),
end: str = Query(..., description="ISO 8601 with offset, e.g. 2026-09-08T18:00:00+02:00."),
top: int = Query(25, ge=1, le=100),
time_zone: str | None = Query(None, description="Time zone for returned dates, e.g. Europe/Prague."),
include_body: bool = Query(False, description="Also return body and bodyPreview of each event."),
body_type: str = Query("html", pattern="^(html|text)$", description="Body format when include_body is true."),
select: str | None = EVENT_SELECT_QUERY,
service: CalendarService = Depends(get_calendar_service),
) -> Any:
return await service.list_calendar_view(
user_id=user_id,
start=start,
end=end,
top=top,
time_zone=time_zone,
include_body=include_body,
body_type=body_type,
select=select,
)
@router.post("/users/{user_id}/calendar/schedule")
async def get_schedule(
user_id: str,
request: ScheduleRequest,
service: CalendarService = Depends(get_calendar_service),
) -> Any:
return await service.get_schedule(user_id=user_id, request=request)
@router.get("/users/{user_id}/calendar/events/{event_id}")
async def get_event(
user_id: str,
event_id: str,
select: str | None = EVENT_SELECT_QUERY,
service: CalendarService = Depends(get_calendar_service),
) -> Any:
return await service.get_event(user_id=user_id, event_id=event_id, select=select)
@router.patch("/users/{user_id}/calendar/events/{event_id}")
async def update_event(
user_id: str,
event_id: str,
request: CalendarEventUpdateRequest,
service: CalendarService = Depends(get_calendar_service),
) -> Any:
return await service.update_event(user_id=user_id, event_id=event_id, request=request)
@router.delete("/users/{user_id}/calendar/events/{event_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_event(
user_id: str,
event_id: str,
service: CalendarService = Depends(get_calendar_service),
) -> Response:
await service.delete_event(user_id=user_id, event_id=event_id)
return Response(status_code=status.HTTP_204_NO_CONTENT)
@router.get("/users/{user_id}/drive/root/children")
async def list_drive_root_children(
user_id: str,
top: int = Query(25, ge=1, le=200),
service: DriveService = Depends(get_drive_service),
) -> Any:
return await service.list_root_children(user_id=user_id, top=top)
@router.put("/users/{user_id}/drive/root/{path:path}")
async def upload_small_file(
user_id: str,
path: str,
request: DriveUploadRequest = Body(...),
service: DriveService = Depends(get_drive_service),
) -> Any:
return await service.upload_small_file(user_id=user_id, path=path, request=request)
@router.get("/groups")
async def list_groups(
top: int = Query(25, ge=1, le=999),
select: str | None = GROUP_SELECT_QUERY,
service: GroupsService = Depends(get_groups_service),
) -> Any:
return await service.list_groups(top=top, select=select)
@router.get("/teams/{team_id}/channels")
async def list_team_channels(team_id: str, service: GroupsService = Depends(get_groups_service)) -> Any:
return await service.list_team_channels(team_id=team_id)
@router.get("/groups/{group_id}/planner/plans")
async def list_group_plans(group_id: str, service: PlannerService = Depends(get_planner_service)) -> Any:
return await service.list_group_plans(group_id=group_id)
@router.get("/planner/plans/{plan_id}/buckets")
async def list_plan_buckets(plan_id: str, service: PlannerService = Depends(get_planner_service)) -> Any:
return await service.list_plan_buckets(plan_id=plan_id)
@router.get("/planner/plans/{plan_id}/tasks")
async def list_plan_tasks(plan_id: str, service: PlannerService = Depends(get_planner_service)) -> Any:
return await service.list_plan_tasks(plan_id=plan_id)
@router.post("/planner/tasks", status_code=status.HTTP_201_CREATED)
async def create_planner_task(
request: PlannerTaskCreateRequest,
service: PlannerService = Depends(get_planner_service),
) -> Any:
return await service.create_task(request=request)
@router.get("/planner/tasks/{task_id}")
async def get_planner_task(task_id: str, service: PlannerService = Depends(get_planner_service)) -> Any:
return await service.get_task(task_id=task_id)
@router.patch("/planner/tasks/{task_id}")
async def update_planner_task(
task_id: str,
request: PlannerTaskUpdateRequest,
if_match: str | None = Header(
None,
alias="If-Match",
description="Task ETag (@odata.etag). When omitted, the service loads the current ETag first.",
),
service: PlannerService = Depends(get_planner_service),
) -> Any:
return await service.update_task(task_id=task_id, request=request, if_match=if_match)
@router.delete("/planner/tasks/{task_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_planner_task(
task_id: str,
if_match: str | None = Header(
None,
alias="If-Match",
description="Task ETag (@odata.etag). When omitted, the service loads the current ETag first.",
),
service: PlannerService = Depends(get_planner_service),
) -> Response:
await service.delete_task(task_id=task_id, if_match=if_match)
return Response(status_code=status.HTTP_204_NO_CONTENT)