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:
@@ -88,20 +88,20 @@ GET /version
|
||||
GET /status
|
||||
GET /users?top=25&search=jane
|
||||
GET /users/{user_id}
|
||||
GET /users/{user_id}/mail/messages?folder=Inbox&top=25
|
||||
GET /users/{user_id}/mail/messages?folder=Inbox&top=25&select=...
|
||||
POST /users/{user_id}/mail/send
|
||||
GET /admin-consent/url?redirect_uri=...&state=...
|
||||
GET /users/{user_id}/calendar
|
||||
GET /users/{user_id}/calendar/events?top=25
|
||||
GET /users/{user_id}/calendar/events?top=25&select=...
|
||||
POST /users/{user_id}/calendar/events
|
||||
GET /users/{user_id}/calendar/view?start=...&end=...&top=25&time_zone=Europe/Prague
|
||||
GET /users/{user_id}/calendar/view?start=...&end=...&top=25&time_zone=Europe/Prague&include_body=true&body_type=text&select=...
|
||||
POST /users/{user_id}/calendar/schedule
|
||||
GET /users/{user_id}/calendar/events/{event_id}
|
||||
GET /users/{user_id}/calendar/events/{event_id}?select=...
|
||||
PATCH /users/{user_id}/calendar/events/{event_id}
|
||||
DELETE /users/{user_id}/calendar/events/{event_id}
|
||||
GET /users/{user_id}/drive/root/children?top=25
|
||||
PUT /users/{user_id}/drive/root/{path}
|
||||
GET /groups?top=25
|
||||
GET /groups?top=25&select=...
|
||||
GET /groups/{group_id}/planner/plans
|
||||
GET /teams/{team_id}/channels
|
||||
GET /planner/plans/{plan_id}/buckets
|
||||
@@ -113,7 +113,13 @@ DELETE /planner/tasks/{task_id} (volitelně hlavička If-Match)
|
||||
```
|
||||
|
||||
Parametry `start` a `end` u calendarView jsou ISO 8601 s offsetem, například
|
||||
`2026-09-08T08:00:00+02:00`. Datumy u Planneru (`due_date_time`, `start_date_time`)
|
||||
`2026-09-08T08:00:00+02:00`. Tělo událostí se ve výpisu nevrací, dokud se nepošle
|
||||
`include_body=true`; `body_type=text` vrátí tělo jako čistý text místo HTML.
|
||||
|
||||
Výpisy mailů, událostí a skupin a načtení jedné události vracejí jen výchozí sadu polí.
|
||||
Parametr `select` (pole oddělená čárkou) ji nahradí; výchozí sada i seznam dostupných
|
||||
polí jsou u každého endpointu ve Swaggeru a v `app/graph_fields.py`. Neznámé pole
|
||||
Graph odmítne a služba vrátí 502 s jeho chybou. Datumy u Planneru (`due_date_time`, `start_date_time`)
|
||||
jsou ISO 8601 s offsetem, například `2026-09-10T16:00:00Z`. Řešitelé se předávají
|
||||
jako Graph user id v `assign_user_ids`, odebírají v `unassign_user_ids`.
|
||||
|
||||
@@ -155,6 +161,12 @@ Vygenerované OpenAPI UI otevřete na `http://localhost:8000/docs`.
|
||||
|
||||
## Záznam změn
|
||||
|
||||
- 2026-09-22: parametr `select` u výpisu mailů, událostí, calendarView, jedné události
|
||||
a skupin. Výchozí sady a seznamy polí v `app/graph_fields.py`, Swagger je zobrazuje.
|
||||
|
||||
- 2026-09-22: `calendar/view` má `include_body` a `body_type` (html, text), tělo události
|
||||
se dá dotáhnout rovnou ve výpisu bez dalšího volání na jednotlivé události.
|
||||
|
||||
- 2026-09-22: šifrování hlaviček `X-MS365-*` odstraněno, hodnoty se posílají tak, jak jsou.
|
||||
Zrušena proměnná `MS365_CREDENTIAL_ENCODING_SECRET` a závislost `cryptography`.
|
||||
- 2026-09-22: `GET /users/{user_id}/calendar` (objekt kalendáře schránky, id a name) pro
|
||||
|
||||
@@ -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
@@ -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
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user