Skip to content

Commit 5d9f27c

Browse files
authored
Merge pull request #27 from ks6088ts-labs/feature/issue-25_msgraphs-service
add an example script for calling Microsoft Graph API
2 parents f2a5f8e + ad87b14 commit 5d9f27c

File tree

6 files changed

+561
-40
lines changed

6 files changed

+561
-40
lines changed

.env.template

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,9 @@ AZURE_AI_SPEECH_ENDPOINT="https://<speech-api-name>.cognitiveservices.azure.com/
2121
# Chats WebSocket
2222
# Azure Container Apps: `wss://yourcontainerapps.japaneast.azurecontainerapps.io`
2323
CHATS_WEBSOCKET_URL="ws://localhost:8000"
24+
25+
# Microsoft Graph Sites
26+
MICROSOFT_GRAPH_TENANT_ID="<YOUR_TENANT_ID>"
27+
MICROSOFT_GRAPH_CLIENT_ID="<YOUR_CLIENT_ID>"
28+
MICROSOFT_GRAPH_CLIENT_SECRET="<YOUR_CLIENT_SECRET>"
29+
MICROSOFT_GRAPH_USER_SCOPES="User.Read Sites.Read.All" # scopes for Microsoft Graph API

docs/index.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,31 @@ uv run python scripts/speeches.py delete-transcription "$JOB_ID"
111111
uv run python scripts/speeches.py delete-transcription "$JOB_ID" --force
112112
```
113113

114+
## Microsoft Graph API
115+
116+
- [Build Python apps with Microsoft Graph](https://learn.microsoft.com/en-us/graph/tutorials/python?tabs=aad)
117+
118+
### Fundamentals
119+
120+
```shell
121+
# Help
122+
uv run python scripts/microsoft_graphs.py --help
123+
124+
# Get access token
125+
uv run python scripts/microsoft_graphs.py get-access-token
126+
127+
# Get my profile
128+
uv run python scripts/microsoft_graphs.py get-my-profile \
129+
--access-token $ACCESS_TOKEN \
130+
--expires-on $EXPIRES_ON
131+
132+
# Get SharePoint sites
133+
uv run python scripts/microsoft_graphs.py get-sites \
134+
--site-id $SITE_ID \
135+
--access-token $ACCESS_TOKEN \
136+
--expires-on $EXPIRES_ON
137+
```
138+
114139
## MCP
115140

116141
- [FastAPI-MCP](https://github.com/tadata-org/fastapi_mcp)

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@ requires-python = ">=3.10"
77
dependencies = [
88
"azure-cosmos>=4.9.0",
99
"azure-functions>=1.23.0",
10+
"azure-identity>=1.23.0",
1011
"azure-monitor-opentelemetry>=1.6.10",
1112
"azure-storage-blob>=12.25.1",
1213
"fastapi-mcp>=0.3.4",
1314
"fastapi[standard]>=0.115.12",
1415
"langchain-community>=0.3.27",
1516
"langchain-openai>=0.3.27",
17+
"msgraph-sdk>=1.37.0",
1618
"opentelemetry-instrumentation-fastapi>=0.52b1",
1719
"pydantic-settings>=2.10.1",
1820
"typer>=0.16.0",

scripts/microsoft_graphs.py

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
#!/usr/bin/env python
2+
3+
import asyncio
4+
5+
import typer
6+
from azure.core.credentials import AccessToken
7+
from azure.identity import DeviceCodeCredential
8+
from msgraph import GraphServiceClient
9+
from msgraph.generated.users.item.user_item_request_builder import UserItemRequestBuilder
10+
from rich.console import Console
11+
from rich.table import Table
12+
13+
from template_fastapi.settings.microsoft_graphs import get_microsoft_graph_settings
14+
15+
app = typer.Typer()
16+
console = Console()
17+
18+
19+
class RawAccessTokenProvider:
20+
def __init__(self, access_token: str, expires_on: int):
21+
self._access_token = access_token
22+
self._expires_on = expires_on
23+
24+
def get_token(self, *scopes, **kwargs) -> AccessToken:
25+
return AccessToken(self._access_token, self._expires_on)
26+
27+
28+
def get_graph_client(
29+
access_token: str | None = None,
30+
expires_on: int | None = None,
31+
) -> GraphServiceClient:
32+
"""Microsoft Graph クライアントを取得する"""
33+
settings = get_microsoft_graph_settings()
34+
scopes = settings.microsoft_graph_user_scopes.split(" ")
35+
36+
if access_token and expires_on:
37+
# アクセストークンが指定されている場合はそれを使用
38+
access_token_provider = RawAccessTokenProvider(access_token, expires_on)
39+
return GraphServiceClient(credentials=access_token_provider, scopes=scopes)
40+
device_code_credential = DeviceCodeCredential(
41+
client_id=settings.microsoft_graph_client_id,
42+
tenant_id=settings.microsoft_graph_tenant_id,
43+
)
44+
return GraphServiceClient(
45+
credentials=device_code_credential,
46+
scopes=scopes,
47+
)
48+
49+
50+
@app.command()
51+
def get_my_profile(
52+
fields: list[str] | None = typer.Option(None, "--field", "-f", help="取得するフィールドを指定(複数指定可能)"),
53+
access_token: str | None = typer.Option(None, "--access-token", "-a", help="アクセストークンを指定して認証する"),
54+
expires_on: int | None = typer.Option(
55+
None, "--expires-on", "-e", help="アクセストークンの有効期限を指定(Unix時間)"
56+
),
57+
):
58+
"""自分のプロファイル情報を取得する"""
59+
console.print("[bold green]ユーザープロファイル[/bold green]を取得します")
60+
61+
# デフォルトのフィールド
62+
default_fields = ["displayName", "mail", "userPrincipalName", "id", "jobTitle", "department"]
63+
select_fields = fields or default_fields
64+
65+
async def _get_profile():
66+
try:
67+
client = get_graph_client(access_token=access_token, expires_on=expires_on)
68+
69+
query_params = UserItemRequestBuilder.UserItemRequestBuilderGetQueryParameters(select=select_fields)
70+
request_config = UserItemRequestBuilder.UserItemRequestBuilderGetRequestConfiguration(
71+
query_parameters=query_params
72+
)
73+
74+
user = await client.me.get(request_configuration=request_config)
75+
76+
# テーブルで表示
77+
table = Table(title="ユーザープロファイル")
78+
table.add_column("項目", style="cyan")
79+
table.add_column("値", style="green")
80+
81+
# 動的にフィールドを表示
82+
for field in select_fields:
83+
value = getattr(user, field.replace("_", ""), "N/A")
84+
if value is not None:
85+
table.add_row(field, str(value))
86+
87+
console.print(table)
88+
89+
except Exception as e:
90+
console.print(f"[bold red]エラー[/bold red]: {str(e)}")
91+
92+
asyncio.run(_get_profile())
93+
94+
95+
@app.command()
96+
def get_access_token():
97+
"""アクセストークンを取得する"""
98+
console.print("[bold green]アクセストークン[/bold green]を取得します")
99+
100+
try:
101+
settings = get_microsoft_graph_settings()
102+
scopes = settings.microsoft_graph_user_scopes.split(" ")
103+
104+
device_code_credential = DeviceCodeCredential(
105+
client_id=settings.microsoft_graph_client_id,
106+
tenant_id=settings.microsoft_graph_tenant_id,
107+
)
108+
109+
access_token = device_code_credential.get_token(*scopes)
110+
111+
print(f"access_token: {access_token.token}")
112+
print(f"expires_on: {access_token.expires_on}")
113+
114+
except Exception as e:
115+
console.print(f"[bold red]エラー[/bold red]: {str(e)}")
116+
117+
118+
@app.command()
119+
def get_sites(
120+
site_id: str | None = typer.Option(None, "--site-id", "-s", help="取得するサイトの ID(省略時は全サイト)"),
121+
access_token: str | None = typer.Option(None, "--access-token", "-a", help="アクセストークンを指定して認証する"),
122+
expires_on: int | None = typer.Option(
123+
None, "--expires-on", "-e", help="アクセストークンの有効期限を指定(Unix時間)"
124+
),
125+
):
126+
"""SharePoint サイトの情報を取得する"""
127+
console.print("[bold green]SharePoint サイト[/bold green]の情報を取得します")
128+
129+
async def _get_sites():
130+
try:
131+
client = get_graph_client(access_token=access_token, expires_on=expires_on)
132+
133+
if site_id:
134+
site = await client.sites.by_site_id(site_id).get()
135+
sites = [site]
136+
else:
137+
sites_response = await client.sites.get()
138+
sites = sites_response.value if sites_response.value else []
139+
140+
# テーブルで表示
141+
table = Table(title="SharePoint サイト")
142+
table.add_column("ID", style="cyan")
143+
table.add_column("名前", style="green")
144+
table.add_column("URL", style="blue")
145+
146+
for site in sites:
147+
table.add_row(site.id, site.name, site.web_url)
148+
149+
console.print(table)
150+
151+
except Exception as e:
152+
console.print(f"[bold red]エラー[/bold red]: {str(e)}")
153+
154+
asyncio.run(_get_sites())
155+
156+
157+
if __name__ == "__main__":
158+
app()
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from functools import lru_cache
2+
3+
from pydantic_settings import BaseSettings, SettingsConfigDict
4+
5+
6+
class Settings(BaseSettings):
7+
chats_websocket_url: str = "ws://localhost:8000"
8+
microsoft_graph_tenant_id: str = "<YOUR_TENANT_ID>"
9+
microsoft_graph_client_id: str = "<YOUR_CLIENT_ID>"
10+
microsoft_graph_client_secret: str = "<YOUR_CLIENT_SECRET>"
11+
microsoft_graph_user_scopes: str = "User.Read Sites.Read.All" # Space-separated scopes
12+
13+
model_config = SettingsConfigDict(
14+
env_file=".env",
15+
env_ignore_empty=True,
16+
extra="ignore",
17+
)
18+
19+
20+
@lru_cache
21+
def get_microsoft_graph_settings() -> Settings:
22+
return Settings()

0 commit comments

Comments
 (0)