Calendar a Planner endpointy, admin consent URL, C# ukazka (1.1.0)
Podle zadani "MS365 prava konektoru": - Calendar: calendarView, getSchedule, get/patch/delete udalosti - Planner: plany skupiny, buckety, tasky, create/patch/delete s ETag - GET /admin-consent/url pro onboarding klientskeho tenantu - examples/csharp/ListUsersTop10: ukazka volani GET /users?top=10 vcetne sifrovani hlavicek X-MS365-* - README: multitenant napojeni, endpointy, opravneni, zaznam zmen Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
275bd6c467
commit
fe3c3a736a
+128
-2
@@ -6,7 +6,15 @@ from urllib.parse import quote
|
||||
from fastapi import HTTPException, status
|
||||
|
||||
from .graph_client import MicrosoftGraphClient
|
||||
from .schemas import CalendarEventRequest, DriveUploadRequest, SendMailRequest
|
||||
from .schemas import (
|
||||
CalendarEventRequest,
|
||||
CalendarEventUpdateRequest,
|
||||
DriveUploadRequest,
|
||||
PlannerTaskCreateRequest,
|
||||
PlannerTaskUpdateRequest,
|
||||
ScheduleRequest,
|
||||
SendMailRequest,
|
||||
)
|
||||
|
||||
|
||||
def graph_segment(value: str) -> str:
|
||||
@@ -74,6 +82,11 @@ class MailService:
|
||||
)
|
||||
|
||||
|
||||
CALENDAR_EVENT_SELECT = (
|
||||
"id,subject,start,end,location,attendees,webLink,isOnlineMeeting,onlineMeeting,showAs,isCancelled,organizer"
|
||||
)
|
||||
|
||||
|
||||
class CalendarService:
|
||||
def __init__(self, graph: MicrosoftGraphClient) -> None:
|
||||
self._graph = graph
|
||||
@@ -82,10 +95,66 @@ class CalendarService:
|
||||
params = {
|
||||
"$top": top,
|
||||
"$orderby": "start/dateTime",
|
||||
"$select": "id,subject,start,end,location,attendees,webLink,isOnlineMeeting,onlineMeeting",
|
||||
"$select": CALENDAR_EVENT_SELECT,
|
||||
}
|
||||
return await self._graph.request("GET", f"/users/{graph_segment(user_id)}/events", params=params)
|
||||
|
||||
async def list_calendar_view(
|
||||
self,
|
||||
user_id: str,
|
||||
start: str,
|
||||
end: str,
|
||||
top: int = 25,
|
||||
time_zone: str | None = None,
|
||||
) -> Any:
|
||||
params = {
|
||||
"startDateTime": start,
|
||||
"endDateTime": end,
|
||||
"$top": top,
|
||||
"$orderby": "start/dateTime",
|
||||
"$select": CALENDAR_EVENT_SELECT,
|
||||
}
|
||||
headers = {"Prefer": f'outlook.timezone="{time_zone}"'} if time_zone else None
|
||||
return await self._graph.request(
|
||||
"GET",
|
||||
f"/users/{graph_segment(user_id)}/calendarView",
|
||||
params=params,
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
async def get_schedule(self, user_id: str, request: ScheduleRequest) -> Any:
|
||||
return await self._graph.request(
|
||||
"POST",
|
||||
f"/users/{graph_segment(user_id)}/calendar/getSchedule",
|
||||
json=request.as_graph_payload(),
|
||||
)
|
||||
|
||||
async def get_event(self, user_id: str, event_id: str) -> Any:
|
||||
return await self._graph.request(
|
||||
"GET",
|
||||
f"/users/{graph_segment(user_id)}/events/{graph_segment(event_id)}",
|
||||
params={"$select": CALENDAR_EVENT_SELECT + ",body"},
|
||||
)
|
||||
|
||||
async def update_event(self, user_id: str, event_id: str, request: CalendarEventUpdateRequest) -> Any:
|
||||
payload = request.as_graph_patch()
|
||||
if not payload:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Calendar event update must contain at least one field.",
|
||||
)
|
||||
return await self._graph.request(
|
||||
"PATCH",
|
||||
f"/users/{graph_segment(user_id)}/events/{graph_segment(event_id)}",
|
||||
json=payload,
|
||||
)
|
||||
|
||||
async def delete_event(self, user_id: str, event_id: str) -> None:
|
||||
await self._graph.request(
|
||||
"DELETE",
|
||||
f"/users/{graph_segment(user_id)}/events/{graph_segment(event_id)}",
|
||||
)
|
||||
|
||||
async def create_event(self, user_id: str, request: CalendarEventRequest) -> Any:
|
||||
payload: dict[str, Any] = {
|
||||
"subject": request.subject,
|
||||
@@ -103,6 +172,63 @@ class CalendarService:
|
||||
return await self._graph.request("POST", f"/users/{graph_segment(user_id)}/events", json=payload)
|
||||
|
||||
|
||||
class PlannerService:
|
||||
"""Microsoft Planner. Updates and deletes require the task ETag (Graph optimistic concurrency)."""
|
||||
|
||||
def __init__(self, graph: MicrosoftGraphClient) -> None:
|
||||
self._graph = graph
|
||||
|
||||
async def list_group_plans(self, group_id: str) -> Any:
|
||||
return await self._graph.request("GET", f"/groups/{graph_segment(group_id)}/planner/plans")
|
||||
|
||||
async def list_plan_buckets(self, plan_id: str) -> Any:
|
||||
return await self._graph.request("GET", f"/planner/plans/{graph_segment(plan_id)}/buckets")
|
||||
|
||||
async def list_plan_tasks(self, plan_id: str) -> Any:
|
||||
return await self._graph.request("GET", f"/planner/plans/{graph_segment(plan_id)}/tasks")
|
||||
|
||||
async def get_task(self, task_id: str) -> Any:
|
||||
return await self._graph.request("GET", f"/planner/tasks/{graph_segment(task_id)}")
|
||||
|
||||
async def create_task(self, request: PlannerTaskCreateRequest) -> Any:
|
||||
return await self._graph.request("POST", "/planner/tasks", json=request.as_graph_payload())
|
||||
|
||||
async def update_task(self, task_id: str, request: PlannerTaskUpdateRequest, if_match: str | None) -> Any:
|
||||
payload = request.as_graph_patch()
|
||||
if not payload:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Planner task update must contain at least one field.",
|
||||
)
|
||||
etag = await self._resolve_etag(task_id, if_match)
|
||||
return await self._graph.request(
|
||||
"PATCH",
|
||||
f"/planner/tasks/{graph_segment(task_id)}",
|
||||
json=payload,
|
||||
headers={"If-Match": etag, "Prefer": "return=representation"},
|
||||
)
|
||||
|
||||
async def delete_task(self, task_id: str, if_match: str | None) -> None:
|
||||
etag = await self._resolve_etag(task_id, if_match)
|
||||
await self._graph.request(
|
||||
"DELETE",
|
||||
f"/planner/tasks/{graph_segment(task_id)}",
|
||||
headers={"If-Match": etag},
|
||||
)
|
||||
|
||||
async def _resolve_etag(self, task_id: str, if_match: str | None) -> str:
|
||||
if if_match:
|
||||
return if_match
|
||||
task = await self.get_task(task_id)
|
||||
etag = task.get("@odata.etag") if isinstance(task, dict) else None
|
||||
if not etag:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_502_BAD_GATEWAY,
|
||||
detail={"message": "Microsoft Graph did not return an ETag for the Planner task.", "task_id": task_id},
|
||||
)
|
||||
return etag
|
||||
|
||||
|
||||
class DriveService:
|
||||
def __init__(self, graph: MicrosoftGraphClient) -> None:
|
||||
self._graph = graph
|
||||
|
||||
Reference in New Issue
Block a user