first
This commit is contained in:
@@ -1,3 +1,66 @@
|
|||||||
# Microsoft 365 Service
|
# Microsoft 365 Service
|
||||||
|
|
||||||
Generated by AppFactory.
|
FastAPI service for server-to-server communication with Microsoft 365 through Microsoft Graph.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Create an app registration in Microsoft Entra ID, grant the required Microsoft Graph application permissions, and expose these environment variables:
|
||||||
|
|
||||||
|
```env
|
||||||
|
MS365_TENANT_ID=00000000-0000-0000-0000-000000000000
|
||||||
|
MS365_CLIENT_ID=00000000-0000-0000-0000-000000000000
|
||||||
|
MS365_CLIENT_SECRET=secret-value
|
||||||
|
MS365_GRAPH_BASE_URL=https://graph.microsoft.com/v1.0
|
||||||
|
MS365_GRAPH_SCOPE=https://graph.microsoft.com/.default
|
||||||
|
MS365_REQUEST_TIMEOUT_SECONDS=30
|
||||||
|
```
|
||||||
|
|
||||||
|
The service uses the OAuth2 client credentials flow, so Microsoft Graph permissions must be application permissions approved by an administrator.
|
||||||
|
|
||||||
|
## Implemented Services
|
||||||
|
|
||||||
|
- Users: list and read users.
|
||||||
|
- Mail: list messages and send email, including file attachments.
|
||||||
|
- Calendar: list and create events, including Teams online meetings.
|
||||||
|
- OneDrive: list root files and upload small files.
|
||||||
|
- Groups and Teams: list groups and list team channels.
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /health
|
||||||
|
GET /version
|
||||||
|
GET /microsoft365/status
|
||||||
|
GET /microsoft365/users?top=25&search=jane
|
||||||
|
GET /microsoft365/users/{user_id}
|
||||||
|
GET /microsoft365/users/{user_id}/mail/messages?folder=Inbox&top=25
|
||||||
|
POST /microsoft365/users/{user_id}/mail/send
|
||||||
|
GET /microsoft365/users/{user_id}/calendar/events?top=25
|
||||||
|
POST /microsoft365/users/{user_id}/calendar/events
|
||||||
|
GET /microsoft365/users/{user_id}/drive/root/children?top=25
|
||||||
|
PUT /microsoft365/users/{user_id}/drive/root/{path}
|
||||||
|
GET /microsoft365/groups?top=25
|
||||||
|
GET /microsoft365/teams/{team_id}/channels
|
||||||
|
```
|
||||||
|
|
||||||
|
`user_id` can be a Microsoft Graph user id or user principal name, for example `jane@example.com`.
|
||||||
|
|
||||||
|
## Required Graph Permissions
|
||||||
|
|
||||||
|
Grant only the permissions your deployment actually uses:
|
||||||
|
|
||||||
|
- Users: `User.Read.All`
|
||||||
|
- Mail read: `Mail.Read`
|
||||||
|
- Mail send: `Mail.Send`
|
||||||
|
- Calendar read/write: `Calendars.ReadWrite`
|
||||||
|
- OneDrive read/write: `Files.ReadWrite.All`
|
||||||
|
- Groups and Teams channel listing: `Group.Read.All`, `Team.ReadBasic.All`, `Channel.ReadBasic.All`
|
||||||
|
|
||||||
|
## Run Locally
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
uvicorn app.main:app --reload
|
||||||
|
```
|
||||||
|
|
||||||
|
Open `http://localhost:8000/docs` for the generated OpenAPI UI.
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import os
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Settings:
|
||||||
|
app_name: str = os.getenv("APP_NAME", "Microsoft 365 Service")
|
||||||
|
app_version: str = os.getenv("APP_VERSION", "1.0.0")
|
||||||
|
root_path: str = os.getenv("ROOT_PATH", "")
|
||||||
|
tenant_id: str = os.getenv("MS365_TENANT_ID", "")
|
||||||
|
client_id: str = os.getenv("MS365_CLIENT_ID", "")
|
||||||
|
client_secret: str = os.getenv("MS365_CLIENT_SECRET", "")
|
||||||
|
graph_base_url: str = os.getenv("MS365_GRAPH_BASE_URL", "https://graph.microsoft.com/v1.0")
|
||||||
|
graph_scope: str = os.getenv("MS365_GRAPH_SCOPE", "https://graph.microsoft.com/.default")
|
||||||
|
request_timeout_seconds: float = float(os.getenv("MS365_REQUEST_TIMEOUT_SECONDS", "30"))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def token_url(self) -> str:
|
||||||
|
return f"https://login.microsoftonline.com/{self.tenant_id}/oauth2/v2.0/token"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_configured(self) -> bool:
|
||||||
|
return bool(self.tenant_id and self.client_id and self.client_secret)
|
||||||
|
|
||||||
|
|
||||||
|
settings = Settings()
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
import time
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
from fastapi import HTTPException, status
|
||||||
|
|
||||||
|
from .config import Settings
|
||||||
|
|
||||||
|
|
||||||
|
class MicrosoftGraphClient:
|
||||||
|
def __init__(self, settings: Settings) -> None:
|
||||||
|
self._settings = settings
|
||||||
|
self._access_token: str | None = None
|
||||||
|
self._expires_at = 0.0
|
||||||
|
|
||||||
|
async def request(
|
||||||
|
self,
|
||||||
|
method: str,
|
||||||
|
path: str,
|
||||||
|
*,
|
||||||
|
params: dict[str, Any] | None = None,
|
||||||
|
json: Any | None = None,
|
||||||
|
content: bytes | None = None,
|
||||||
|
headers: dict[str, str] | None = None,
|
||||||
|
) -> Any:
|
||||||
|
token = await self._get_access_token()
|
||||||
|
request_headers = {
|
||||||
|
"Authorization": f"Bearer {token}",
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
if headers:
|
||||||
|
request_headers.update(headers)
|
||||||
|
|
||||||
|
url = self._build_url(path)
|
||||||
|
async with httpx.AsyncClient(timeout=self._settings.request_timeout_seconds) as client:
|
||||||
|
response = await client.request(
|
||||||
|
method,
|
||||||
|
url,
|
||||||
|
params=params,
|
||||||
|
json=json,
|
||||||
|
content=content,
|
||||||
|
headers=request_headers,
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code >= 400:
|
||||||
|
raise self._graph_exception(response)
|
||||||
|
if response.status_code == status.HTTP_204_NO_CONTENT or not response.content:
|
||||||
|
return None
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
async def _get_access_token(self) -> str:
|
||||||
|
if self._access_token and time.time() < self._expires_at - 60:
|
||||||
|
return self._access_token
|
||||||
|
|
||||||
|
if not self._settings.is_configured:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||||
|
detail="Microsoft 365 credentials are not configured.",
|
||||||
|
)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"client_id": self._settings.client_id,
|
||||||
|
"client_secret": self._settings.client_secret,
|
||||||
|
"scope": self._settings.graph_scope,
|
||||||
|
"grant_type": "client_credentials",
|
||||||
|
}
|
||||||
|
async with httpx.AsyncClient(timeout=self._settings.request_timeout_seconds) as client:
|
||||||
|
response = await client.post(self._settings.token_url, data=data)
|
||||||
|
|
||||||
|
if response.status_code >= 400:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_502_BAD_GATEWAY,
|
||||||
|
detail={
|
||||||
|
"message": "Unable to authenticate with Microsoft identity platform.",
|
||||||
|
"upstream_status": response.status_code,
|
||||||
|
"upstream_response": self._safe_json(response),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
payload = response.json()
|
||||||
|
self._access_token = payload["access_token"]
|
||||||
|
self._expires_at = time.time() + int(payload.get("expires_in", 3599))
|
||||||
|
return self._access_token
|
||||||
|
|
||||||
|
def _build_url(self, path: str) -> str:
|
||||||
|
if path.startswith("https://"):
|
||||||
|
return path
|
||||||
|
return f"{self._settings.graph_base_url.rstrip('/')}/{path.lstrip('/')}"
|
||||||
|
|
||||||
|
def _graph_exception(self, response: httpx.Response) -> HTTPException:
|
||||||
|
return HTTPException(
|
||||||
|
status_code=status.HTTP_502_BAD_GATEWAY,
|
||||||
|
detail={
|
||||||
|
"message": "Microsoft Graph request failed.",
|
||||||
|
"upstream_status": response.status_code,
|
||||||
|
"upstream_response": self._safe_json(response),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _safe_json(response: httpx.Response) -> Any:
|
||||||
|
try:
|
||||||
|
return response.json()
|
||||||
|
except ValueError:
|
||||||
|
return response.text
|
||||||
+9
-10
@@ -1,15 +1,14 @@
|
|||||||
import os
|
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
|
|
||||||
APP_NAME = os.getenv("APP_NAME", "Microsoft 365 Service")
|
from .config import settings
|
||||||
APP_VERSION = os.getenv("APP_VERSION", "1.0.0")
|
from .routes import router as microsoft365_router
|
||||||
ROOT_PATH = os.getenv("ROOT_PATH", "")
|
|
||||||
|
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title=APP_NAME,
|
title=settings.app_name,
|
||||||
version=APP_VERSION,
|
version=settings.app_version,
|
||||||
root_path=ROOT_PATH
|
root_path=settings.root_path
|
||||||
)
|
)
|
||||||
|
app.include_router(microsoft365_router)
|
||||||
|
|
||||||
@app.get("/health")
|
@app.get("/health")
|
||||||
def health():
|
def health():
|
||||||
@@ -18,8 +17,8 @@ def health():
|
|||||||
@app.get("/version")
|
@app.get("/version")
|
||||||
def version():
|
def version():
|
||||||
return {
|
return {
|
||||||
"app": APP_NAME,
|
"app": settings.app_name,
|
||||||
"version": APP_VERSION,
|
"version": settings.app_version,
|
||||||
"language": "python",
|
"language": "python",
|
||||||
"root_path": ROOT_PATH
|
"root_path": settings.root_path
|
||||||
}
|
}
|
||||||
|
|||||||
+131
@@ -0,0 +1,131 @@
|
|||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Body, Depends, Query, Response, status
|
||||||
|
|
||||||
|
from .config import settings
|
||||||
|
from .graph_client import MicrosoftGraphClient
|
||||||
|
from .schemas import CalendarEventRequest, DriveUploadRequest, SendMailRequest
|
||||||
|
from .services import CalendarService, DriveService, GroupsService, MailService, UsersService
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/microsoft365", tags=["microsoft365"])
|
||||||
|
graph_client = MicrosoftGraphClient(settings)
|
||||||
|
|
||||||
|
|
||||||
|
def get_graph_client() -> MicrosoftGraphClient:
|
||||||
|
return graph_client
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/status")
|
||||||
|
def microsoft365_status() -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"configured": settings.is_configured,
|
||||||
|
"tenant_id": bool(settings.tenant_id),
|
||||||
|
"client_id": bool(settings.client_id),
|
||||||
|
"client_secret": bool(settings.client_secret),
|
||||||
|
"graph_base_url": settings.graph_base_url,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@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),
|
||||||
|
service: MailService = Depends(get_mail_service),
|
||||||
|
) -> Any:
|
||||||
|
return await service.list_messages(user_id=user_id, folder=folder, top=top)
|
||||||
|
|
||||||
|
|
||||||
|
@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/events")
|
||||||
|
async def list_events(
|
||||||
|
user_id: str,
|
||||||
|
top: int = Query(25, ge=1, le=100),
|
||||||
|
service: CalendarService = Depends(get_calendar_service),
|
||||||
|
) -> Any:
|
||||||
|
return await service.list_events(user_id=user_id, top=top)
|
||||||
|
|
||||||
|
|
||||||
|
@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}/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),
|
||||||
|
service: GroupsService = Depends(get_groups_service),
|
||||||
|
) -> Any:
|
||||||
|
return await service.list_groups(top=top)
|
||||||
|
|
||||||
|
|
||||||
|
@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)
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from pydantic import BaseModel, EmailStr, Field
|
||||||
|
|
||||||
|
|
||||||
|
class EmailAddress(BaseModel):
|
||||||
|
address: EmailStr
|
||||||
|
name: str | None = None
|
||||||
|
|
||||||
|
def as_graph_recipient(self) -> dict[str, Any]:
|
||||||
|
payload: dict[str, Any] = {"address": str(self.address)}
|
||||||
|
if self.name:
|
||||||
|
payload["name"] = self.name
|
||||||
|
return {"emailAddress": payload}
|
||||||
|
|
||||||
|
|
||||||
|
class FileAttachment(BaseModel):
|
||||||
|
name: str
|
||||||
|
content_type: str = "application/octet-stream"
|
||||||
|
content_bytes_base64: str = Field(..., description="Base64 encoded file content.")
|
||||||
|
|
||||||
|
def as_graph_attachment(self) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"@odata.type": "#microsoft.graph.fileAttachment",
|
||||||
|
"name": self.name,
|
||||||
|
"contentType": self.content_type,
|
||||||
|
"contentBytes": self.content_bytes_base64,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class SendMailRequest(BaseModel):
|
||||||
|
subject: str
|
||||||
|
body: str
|
||||||
|
to: list[EmailAddress]
|
||||||
|
cc: list[EmailAddress] = Field(default_factory=list)
|
||||||
|
bcc: list[EmailAddress] = Field(default_factory=list)
|
||||||
|
reply_to: list[EmailAddress] = Field(default_factory=list)
|
||||||
|
body_content_type: str = Field("HTML", pattern="^(HTML|Text)$")
|
||||||
|
save_to_sent_items: bool = True
|
||||||
|
attachments: list[FileAttachment] = Field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
class CalendarAttendee(BaseModel):
|
||||||
|
email: EmailStr
|
||||||
|
name: str | None = None
|
||||||
|
type: str = Field("required", pattern="^(required|optional|resource)$")
|
||||||
|
|
||||||
|
def as_graph_attendee(self) -> dict[str, Any]:
|
||||||
|
email_address: dict[str, Any] = {"address": str(self.email)}
|
||||||
|
if self.name:
|
||||||
|
email_address["name"] = self.name
|
||||||
|
return {"emailAddress": email_address, "type": self.type}
|
||||||
|
|
||||||
|
|
||||||
|
class DateTimeTimeZone(BaseModel):
|
||||||
|
date_time: str = Field(..., alias="dateTime")
|
||||||
|
time_zone: str = Field("UTC", alias="timeZone")
|
||||||
|
|
||||||
|
|
||||||
|
class CalendarEventRequest(BaseModel):
|
||||||
|
subject: str
|
||||||
|
body: str | None = None
|
||||||
|
body_content_type: str = Field("HTML", pattern="^(HTML|Text)$")
|
||||||
|
start: DateTimeTimeZone
|
||||||
|
end: DateTimeTimeZone
|
||||||
|
location: str | None = None
|
||||||
|
attendees: list[CalendarAttendee] = Field(default_factory=list)
|
||||||
|
is_online_meeting: bool = False
|
||||||
|
online_meeting_provider: str | None = Field(None, pattern="^(teamsForBusiness|skypeForBusiness|skypeForConsumer)$")
|
||||||
|
|
||||||
|
|
||||||
|
class DriveUploadRequest(BaseModel):
|
||||||
|
content_base64: str
|
||||||
|
content_type: str = "application/octet-stream"
|
||||||
|
|
||||||
|
|
||||||
|
class GraphCollectionResponse(BaseModel):
|
||||||
|
value: list[dict[str, Any]]
|
||||||
|
next_link: str | None = Field(None, alias="@odata.nextLink")
|
||||||
+142
@@ -0,0 +1,142 @@
|
|||||||
|
import base64
|
||||||
|
import binascii
|
||||||
|
from typing import Any
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
|
from fastapi import HTTPException, status
|
||||||
|
|
||||||
|
from .graph_client import MicrosoftGraphClient
|
||||||
|
from .schemas import CalendarEventRequest, DriveUploadRequest, SendMailRequest
|
||||||
|
|
||||||
|
|
||||||
|
def graph_segment(value: str) -> str:
|
||||||
|
return quote(value, safe="")
|
||||||
|
|
||||||
|
|
||||||
|
def graph_path(value: str) -> str:
|
||||||
|
return quote(value.strip("/"), safe="/")
|
||||||
|
|
||||||
|
|
||||||
|
class UsersService:
|
||||||
|
def __init__(self, graph: MicrosoftGraphClient) -> None:
|
||||||
|
self._graph = graph
|
||||||
|
|
||||||
|
async def list_users(self, top: int = 25, search: str | None = None) -> Any:
|
||||||
|
params: dict[str, Any] = {"$top": top}
|
||||||
|
headers = None
|
||||||
|
if search:
|
||||||
|
params["$search"] = f'"displayName:{search}" OR "mail:{search}" OR "userPrincipalName:{search}"'
|
||||||
|
headers = {"ConsistencyLevel": "eventual"}
|
||||||
|
return await self._graph.request("GET", "/users", params=params, headers=headers)
|
||||||
|
|
||||||
|
async def get_user(self, user_id: str) -> Any:
|
||||||
|
return await self._graph.request("GET", f"/users/{graph_segment(user_id)}")
|
||||||
|
|
||||||
|
|
||||||
|
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:
|
||||||
|
params = {
|
||||||
|
"$top": top,
|
||||||
|
"$orderby": "receivedDateTime desc",
|
||||||
|
"$select": "id,subject,from,toRecipients,receivedDateTime,hasAttachments,isRead,webLink",
|
||||||
|
}
|
||||||
|
return await self._graph.request(
|
||||||
|
"GET",
|
||||||
|
f"/users/{graph_segment(user_id)}/mailFolders/{graph_segment(folder)}/messages",
|
||||||
|
params=params,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def send_mail(self, user_id: str, request: SendMailRequest) -> None:
|
||||||
|
message: dict[str, Any] = {
|
||||||
|
"subject": request.subject,
|
||||||
|
"body": {
|
||||||
|
"contentType": request.body_content_type,
|
||||||
|
"content": request.body,
|
||||||
|
},
|
||||||
|
"toRecipients": [recipient.as_graph_recipient() for recipient in request.to],
|
||||||
|
}
|
||||||
|
if request.cc:
|
||||||
|
message["ccRecipients"] = [recipient.as_graph_recipient() for recipient in request.cc]
|
||||||
|
if request.bcc:
|
||||||
|
message["bccRecipients"] = [recipient.as_graph_recipient() for recipient in request.bcc]
|
||||||
|
if request.reply_to:
|
||||||
|
message["replyTo"] = [recipient.as_graph_recipient() for recipient in request.reply_to]
|
||||||
|
if request.attachments:
|
||||||
|
message["attachments"] = [attachment.as_graph_attachment() for attachment in request.attachments]
|
||||||
|
|
||||||
|
await self._graph.request(
|
||||||
|
"POST",
|
||||||
|
f"/users/{graph_segment(user_id)}/sendMail",
|
||||||
|
json={"message": message, "saveToSentItems": request.save_to_sent_items},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CalendarService:
|
||||||
|
def __init__(self, graph: MicrosoftGraphClient) -> None:
|
||||||
|
self._graph = graph
|
||||||
|
|
||||||
|
async def list_events(self, user_id: str, top: int = 25) -> Any:
|
||||||
|
params = {
|
||||||
|
"$top": top,
|
||||||
|
"$orderby": "start/dateTime",
|
||||||
|
"$select": "id,subject,start,end,location,attendees,webLink,isOnlineMeeting,onlineMeeting",
|
||||||
|
}
|
||||||
|
return await self._graph.request("GET", f"/users/{graph_segment(user_id)}/events", params=params)
|
||||||
|
|
||||||
|
async def create_event(self, user_id: str, request: CalendarEventRequest) -> Any:
|
||||||
|
payload: dict[str, Any] = {
|
||||||
|
"subject": request.subject,
|
||||||
|
"start": request.start.model_dump(by_alias=True),
|
||||||
|
"end": request.end.model_dump(by_alias=True),
|
||||||
|
"attendees": [attendee.as_graph_attendee() for attendee in request.attendees],
|
||||||
|
"isOnlineMeeting": request.is_online_meeting,
|
||||||
|
}
|
||||||
|
if request.body is not None:
|
||||||
|
payload["body"] = {"contentType": request.body_content_type, "content": request.body}
|
||||||
|
if request.location:
|
||||||
|
payload["location"] = {"displayName": request.location}
|
||||||
|
if request.online_meeting_provider:
|
||||||
|
payload["onlineMeetingProvider"] = request.online_meeting_provider
|
||||||
|
return await self._graph.request("POST", f"/users/{graph_segment(user_id)}/events", json=payload)
|
||||||
|
|
||||||
|
|
||||||
|
class DriveService:
|
||||||
|
def __init__(self, graph: MicrosoftGraphClient) -> None:
|
||||||
|
self._graph = graph
|
||||||
|
|
||||||
|
async def list_root_children(self, user_id: str, top: int = 25) -> Any:
|
||||||
|
return await self._graph.request(
|
||||||
|
"GET",
|
||||||
|
f"/users/{graph_segment(user_id)}/drive/root/children",
|
||||||
|
params={"$top": top},
|
||||||
|
)
|
||||||
|
|
||||||
|
async def upload_small_file(self, user_id: str, path: str, request: DriveUploadRequest) -> Any:
|
||||||
|
try:
|
||||||
|
content = base64.b64decode(request.content_base64, validate=True)
|
||||||
|
except binascii.Error as exc:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="content_base64 must contain valid base64 data.",
|
||||||
|
) from exc
|
||||||
|
return await self._graph.request(
|
||||||
|
"PUT",
|
||||||
|
f"/users/{graph_segment(user_id)}/drive/root:/{graph_path(path)}:/content",
|
||||||
|
content=content,
|
||||||
|
headers={"Content-Type": request.content_type},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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"}
|
||||||
|
return await self._graph.request("GET", "/groups", params=params)
|
||||||
|
|
||||||
|
async def list_team_channels(self, team_id: str) -> Any:
|
||||||
|
return await self._graph.request("GET", f"/teams/{graph_segment(team_id)}/channels")
|
||||||
@@ -1,2 +1,4 @@
|
|||||||
fastapi
|
fastapi
|
||||||
|
httpx
|
||||||
|
pydantic[email]
|
||||||
uvicorn[standard]
|
uvicorn[standard]
|
||||||
|
|||||||
Reference in New Issue
Block a user