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
This commit is contained in:
JiriUhlir
2026-09-23 06:51:05 +02:00
parent d80193da8b
commit fb97a8bf63
4 changed files with 121 additions and 24 deletions
+40
View File
@@ -0,0 +1,40 @@
"""Selectable Microsoft Graph fields per resource. Single source for defaults and Swagger descriptions."""
EVENT_FIELDS = (
"id", "subject", "bodyPreview", "body", "start", "end", "location", "locations", "attendees", "organizer",
"webLink", "isOnlineMeeting", "onlineMeeting", "onlineMeetingUrl", "onlineMeetingProvider", "showAs",
"isCancelled", "isAllDay", "isDraft", "isOrganizer", "isReminderOn", "reminderMinutesBeforeStart",
"importance", "sensitivity", "categories", "recurrence", "seriesMasterId", "type", "responseStatus",
"responseRequested", "allowNewTimeProposals", "hideAttendees", "hasAttachments", "iCalUId",
"createdDateTime", "lastModifiedDateTime", "changeKey", "originalStart", "originalStartTimeZone",
"originalEndTimeZone", "transactionId",
)
EVENT_DEFAULT_SELECT = (
"id,subject,start,end,location,attendees,webLink,isOnlineMeeting,onlineMeeting,showAs,isCancelled,organizer"
)
MESSAGE_FIELDS = (
"id", "subject", "body", "bodyPreview", "uniqueBody", "from", "sender", "toRecipients", "ccRecipients",
"bccRecipients", "replyTo", "receivedDateTime", "sentDateTime", "createdDateTime", "lastModifiedDateTime",
"hasAttachments", "isRead", "isDraft", "importance", "categories", "conversationId", "conversationIndex",
"internetMessageId", "internetMessageHeaders", "flag", "parentFolderId", "webLink",
"inferenceClassification", "isDeliveryReceiptRequested", "isReadReceiptRequested", "changeKey",
)
MESSAGE_DEFAULT_SELECT = "id,subject,from,toRecipients,receivedDateTime,hasAttachments,isRead,webLink"
GROUP_FIELDS = (
"id", "displayName", "description", "mail", "mailNickname", "mailEnabled", "securityEnabled", "groupTypes",
"visibility", "classification", "createdDateTime", "renewedDateTime", "expirationDateTime",
"resourceProvisioningOptions", "resourceBehaviorOptions", "proxyAddresses", "membershipRule",
"membershipRuleProcessingState", "isAssignableToRole", "preferredLanguage", "theme",
"onPremisesSyncEnabled", "deletedDateTime",
)
GROUP_DEFAULT_SELECT = "id,displayName,mail,groupTypes,securityEnabled,mailEnabled"
def select_description(default: str, fields: tuple[str, ...]) -> str:
"""Swagger text for the select query parameter."""
return (
"Comma separated Graph fields to return. Replaces the default selection. "
f"Default: {default}. Available: {', '.join(fields)}."
)
+35 -5
View File
@@ -6,6 +6,15 @@ from fastapi import APIRouter, Body, Depends, Header, HTTPException, Query, Resp
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,
@@ -48,6 +57,11 @@ def get_planner_service(graph: MicrosoftGraphClient = Depends(get_graph_client))
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 {
@@ -103,9 +117,10 @@ 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)
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)
@@ -129,9 +144,10 @@ async def get_calendar(user_id: str, service: CalendarService = Depends(get_cale
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)
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)
@@ -150,9 +166,21 @@ async def list_calendar_view(
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)
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")
@@ -168,9 +196,10 @@ async def get_schedule(
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)
return await service.get_event(user_id=user_id, event_id=event_id, select=select)
@router.patch("/users/{user_id}/calendar/events/{event_id}")
@@ -215,9 +244,10 @@ async def upload_small_file(
@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)
return await service.list_groups(top=top, select=select)
@router.get("/teams/{team_id}/channels")
+28 -13
View File
@@ -6,6 +6,7 @@ from urllib.parse import quote
from fastapi import HTTPException, status
from .graph_client import MicrosoftGraphClient
from .graph_fields import EVENT_DEFAULT_SELECT, GROUP_DEFAULT_SELECT, MESSAGE_DEFAULT_SELECT
from .schemas import (
CalendarEventRequest,
CalendarEventUpdateRequest,
@@ -45,11 +46,13 @@ class MailService:
def __init__(self, graph: MicrosoftGraphClient) -> None:
self._graph = graph
async def list_messages(self, user_id: str, folder: str = "Inbox", top: int = 25) -> Any:
async def list_messages(
self, user_id: str, folder: str = "Inbox", top: int = 25, select: str | None = None
) -> Any:
params = {
"$top": top,
"$orderby": "receivedDateTime desc",
"$select": "id,subject,from,toRecipients,receivedDateTime,hasAttachments,isRead,webLink",
"$select": select or MESSAGE_DEFAULT_SELECT,
}
return await self._graph.request(
"GET",
@@ -82,9 +85,12 @@ class MailService:
)
CALENDAR_EVENT_SELECT = (
"id,subject,start,end,location,attendees,webLink,isOnlineMeeting,onlineMeeting,showAs,isCancelled,organizer"
)
def _with_body(select: str) -> str:
fields = [f.strip() for f in select.split(",") if f.strip()]
for extra in ("body", "bodyPreview"):
if extra not in fields:
fields.append(extra)
return ",".join(fields)
class CalendarService:
@@ -95,11 +101,11 @@ class CalendarService:
"""Default calendar of the mailbox (id, name, owner). Cheapest read-only access check."""
return await self._graph.request("GET", f"/users/{graph_segment(user_id)}/calendar")
async def list_events(self, user_id: str, top: int = 25) -> Any:
async def list_events(self, user_id: str, top: int = 25, select: str | None = None) -> Any:
params = {
"$top": top,
"$orderby": "start/dateTime",
"$select": CALENDAR_EVENT_SELECT,
"$select": select or EVENT_DEFAULT_SELECT,
}
return await self._graph.request("GET", f"/users/{graph_segment(user_id)}/events", params=params)
@@ -110,15 +116,24 @@ class CalendarService:
end: str,
top: int = 25,
time_zone: str | None = None,
include_body: bool = False,
body_type: str = "html",
select: str | None = None,
) -> Any:
fields = select or EVENT_DEFAULT_SELECT
params = {
"startDateTime": start,
"endDateTime": end,
"$top": top,
"$orderby": "start/dateTime",
"$select": CALENDAR_EVENT_SELECT,
"$select": _with_body(fields) if include_body else fields,
}
headers = {"Prefer": f'outlook.timezone="{time_zone}"'} if time_zone else None
prefer: list[str] = []
if time_zone:
prefer.append(f'outlook.timezone="{time_zone}"')
if include_body:
prefer.append(f'outlook.body-content-type="{body_type}"')
headers = {"Prefer": ", ".join(prefer)} if prefer else None
return await self._graph.request(
"GET",
f"/users/{graph_segment(user_id)}/calendarView",
@@ -133,11 +148,11 @@ class CalendarService:
json=request.as_graph_payload(),
)
async def get_event(self, user_id: str, event_id: str) -> Any:
async def get_event(self, user_id: str, event_id: str, select: str | None = None) -> Any:
return await self._graph.request(
"GET",
f"/users/{graph_segment(user_id)}/events/{graph_segment(event_id)}",
params={"$select": CALENDAR_EVENT_SELECT + ",body"},
params={"$select": select or _with_body(EVENT_DEFAULT_SELECT)},
)
async def update_event(self, user_id: str, event_id: str, request: CalendarEventUpdateRequest) -> Any:
@@ -264,8 +279,8 @@ class GroupsService:
def __init__(self, graph: MicrosoftGraphClient) -> None:
self._graph = graph
async def list_groups(self, top: int = 25) -> Any:
params = {"$top": top, "$select": "id,displayName,mail,groupTypes,securityEnabled,mailEnabled"}
async def list_groups(self, top: int = 25, select: str | None = None) -> Any:
params = {"$top": top, "$select": select or GROUP_DEFAULT_SELECT}
return await self._graph.request("GET", "/groups", params=params)
async def list_team_channels(self, team_id: str) -> Any: