Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .env.template
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,8 @@ AZURE_AI_SPEECH_ENDPOINT="https://<speech-api-name>.cognitiveservices.azure.com/
# Chats WebSocket
# Azure Container Apps: `wss://yourcontainerapps.japaneast.azurecontainerapps.io`
CHATS_WEBSOCKET_URL="ws://localhost:8000"

# Microsoft Graph Sites
MICROSOFT_GRAPH_TENANT_ID="<YOUR_TENANT_ID>"
MICROSOFT_GRAPH_CLIENT_ID="<YOUR_CLIENT_ID>"
MICROSOFT_GRAPH_CLIENT_SECRET="<YOUR_CLIENT_SECRET>"
63 changes: 63 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,3 +169,66 @@ az resource update \
### Azure AI Speech

- [バッチ文字起こしとは](https://learn.microsoft.com/ja-jp/azure/ai-services/speech-service/batch-transcription)

### Microsoft Graph Sites Service

Microsoft Graph Sites サービスは、SharePoint Online のファイルを操作するためのサービスです。

```shell
# Help
uv run python scripts/msgraphs/sites.py --help

# List files in SharePoint site (site_id is required)
uv run python scripts/msgraphs/sites.py list-files <SITE_ID>

# List files in specific folder
uv run python scripts/msgraphs/sites.py list-files <SITE_ID> --folder "Documents"

# List files in JSON format
uv run python scripts/msgraphs/sites.py list-files <SITE_ID> --format json

# Upload a single file
uv run python scripts/msgraphs/sites.py upload-file <SITE_ID> "local_file.txt"

# Upload file to specific folder
uv run python scripts/msgraphs/sites.py upload-file <SITE_ID> "local_file.txt" --folder "Documents"

# Upload multiple files
uv run python scripts/msgraphs/sites.py upload-files <SITE_ID> "file1.txt" "file2.txt" "file3.txt"

# Upload multiple files to specific folder
uv run python scripts/msgraphs/sites.py upload-files <SITE_ID> "file1.txt" "file2.txt" --folder "Documents"

# Download a file
uv run python scripts/msgraphs/sites.py download-file <SITE_ID> "remote_file.txt"

# Download file from specific folder
uv run python scripts/msgraphs/sites.py download-file <SITE_ID> "remote_file.txt" --folder "Documents"

# Download file with custom output path
uv run python scripts/msgraphs/sites.py download-file <SITE_ID> "remote_file.txt" --output "downloaded_file.txt"

# Get file information
uv run python scripts/msgraphs/sites.py get-file-info <SITE_ID> "remote_file.txt"

# Get file information from specific folder
uv run python scripts/msgraphs/sites.py get-file-info <SITE_ID> "remote_file.txt" --folder "Documents"

# Delete a file
uv run python scripts/msgraphs/sites.py delete-file <SITE_ID> "remote_file.txt"

# Delete file from specific folder
uv run python scripts/msgraphs/sites.py delete-file <SITE_ID> "remote_file.txt" --folder "Documents"

# Delete file without confirmation
uv run python scripts/msgraphs/sites.py delete-file <SITE_ID> "remote_file.txt" --force

# Delete multiple files
uv run python scripts/msgraphs/sites.py delete-files <SITE_ID> "file1.txt" "file2.txt" "file3.txt"

# Delete multiple files from specific folder
uv run python scripts/msgraphs/sites.py delete-files <SITE_ID> "file1.txt" "file2.txt" --folder "Documents"

# Delete multiple files without confirmation
uv run python scripts/msgraphs/sites.py delete-files <SITE_ID> "file1.txt" "file2.txt" --force
```
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ dependencies = [
"fastapi[standard]>=0.115.12",
"langchain-community>=0.3.27",
"langchain-openai>=0.3.27",
"msgraph-sdk>=1.9.0",
"opentelemetry-instrumentation-fastapi>=0.52b1",
"pydantic-settings>=2.10.1",
"typer>=0.16.0",
Expand Down
1 change: 1 addition & 0 deletions scripts/msgraphs/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Microsoft Graph scripts module
249 changes: 249 additions & 0 deletions scripts/msgraphs/sites.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
#!/usr/bin/env python
# filepath: /home/runner/work/template-fastapi/template-fastapi/scripts/msgraphs/sites.py

import json
import os
from pathlib import Path

import typer
from rich.console import Console
from rich.table import Table

from template_fastapi.repositories.msgraphs import MicrosoftGraphRepository

app = typer.Typer()
console = Console()
msgraphs_repo = MicrosoftGraphRepository()


@app.command()
def list_files(
site_id: str = typer.Argument(..., help="SharePoint Site ID"),
folder: str = typer.Option("", help="フォルダパス(省略時はルートディレクトリ)"),
format: str = typer.Option("table", help="出力形式 (table/json)"),
) -> None:
"""SharePointサイトのファイル一覧を表示する"""
try:
files = msgraphs_repo.sites.list_files(site_id, folder)

if format == "json":
files_data = [
{
"name": file.name,
"size": file.size,
"content_type": file.content_type,
"created_at": file.created_at.isoformat() if file.created_at else None,
"updated_at": file.updated_at.isoformat() if file.updated_at else None,
}
for file in files
]
console.print(json.dumps(files_data, indent=2))
else:
table = Table(show_header=True, header_style="bold magenta")
table.add_column("ファイル名", style="cyan")
table.add_column("サイズ", justify="right")
table.add_column("タイプ", style="green")
table.add_column("作成日時", style="blue")
table.add_column("更新日時", style="blue")

for file in files:
size_str = f"{file.size:,} bytes" if file.size else "不明"
created_str = file.created_at.strftime("%Y-%m-%d %H:%M:%S") if file.created_at else "不明"
updated_str = file.updated_at.strftime("%Y-%m-%d %H:%M:%S") if file.updated_at else "不明"

table.add_row(
file.name,
size_str,
file.content_type,
created_str,
updated_str,
)

console.print(table)

console.print(f"\n📁 フォルダ: {folder if folder else 'ルート'}")
console.print(f"📄 ファイル数: {len(files)}")

except Exception as e:
console.print(f"❌ エラー: {e}", style="bold red")
raise typer.Exit(1)


@app.command()
def upload_file(
site_id: str = typer.Argument(..., help="SharePoint Site ID"),
file_path: str = typer.Argument(..., help="アップロードするファイルのパス"),
folder: str = typer.Option("", help="アップロード先のフォルダパス"),
) -> None:
"""SharePointサイトにファイルをアップロードする"""
try:
file_path_obj = Path(file_path)
if not file_path_obj.exists():
console.print(f"❌ ファイルが見つかりません: {file_path}", style="bold red")
raise typer.Exit(1)

with open(file_path_obj, "rb") as f:
file_content = f.read()

uploaded_file = msgraphs_repo.sites.upload_file(site_id, file_content, file_path_obj.name, folder)

console.print(f"✅ ファイルをアップロードしました: {uploaded_file.name}", style="bold green")
console.print(f"📁 フォルダ: {folder if folder else 'ルート'}")
console.print(f"📄 サイズ: {uploaded_file.size:,} bytes")

except Exception as e:
console.print(f"❌ エラー: {e}", style="bold red")
raise typer.Exit(1)


@app.command()
def upload_files(
site_id: str = typer.Argument(..., help="SharePoint Site ID"),
file_paths: list[str] = typer.Argument(..., help="アップロードするファイルのパス(複数指定可能)"),
folder: str = typer.Option("", help="アップロード先のフォルダパス"),
) -> None:
"""SharePointサイトに複数のファイルを同時にアップロードする"""
try:
files_data = []
for file_path in file_paths:
file_path_obj = Path(file_path)
if not file_path_obj.exists():
console.print(f"❌ ファイルが見つかりません: {file_path}", style="bold red")
continue

with open(file_path_obj, "rb") as f:
file_content = f.read()
files_data.append((file_content, file_path_obj.name))

if not files_data:
console.print("❌ 有効なファイルがありません", style="bold red")
raise typer.Exit(1)

uploaded_files = msgraphs_repo.sites.upload_multiple_files(site_id, files_data, folder)

console.print(f"✅ {len(uploaded_files)}個のファイルをアップロードしました:", style="bold green")
for file in uploaded_files:
console.print(f" 📄 {file.name} ({file.size:,} bytes)")
console.print(f"📁 フォルダ: {folder if folder else 'ルート'}")

except Exception as e:
console.print(f"❌ エラー: {e}", style="bold red")
raise typer.Exit(1)


@app.command()
def download_file(
site_id: str = typer.Argument(..., help="SharePoint Site ID"),
file_name: str = typer.Argument(..., help="ダウンロードするファイル名"),
folder: str = typer.Option("", help="ファイルがあるフォルダパス"),
output: str = typer.Option("", help="出力ファイルパス(省略時はファイル名を使用)"),
) -> None:
"""SharePointサイトからファイルをダウンロードする"""
try:
file_content = msgraphs_repo.sites.download_file(site_id, file_name, folder)

output_path = Path(output) if output else Path(file_name)
with open(output_path, "wb") as f:
f.write(file_content)

console.print(f"✅ ファイルをダウンロードしました: {output_path}", style="bold green")
console.print(f"📁 フォルダ: {folder if folder else 'ルート'}")
console.print(f"📄 サイズ: {len(file_content):,} bytes")

except Exception as e:
console.print(f"❌ エラー: {e}", style="bold red")
raise typer.Exit(1)


@app.command()
def delete_file(
site_id: str = typer.Argument(..., help="SharePoint Site ID"),
file_name: str = typer.Argument(..., help="削除するファイル名"),
folder: str = typer.Option("", help="ファイルがあるフォルダパス"),
force: bool = typer.Option(False, "--force", "-f", help="確認なしで削除する"),
) -> None:
"""SharePointサイトからファイルを削除する"""
try:
if not force:
confirm = typer.confirm(f"ファイル '{file_name}' を削除しますか?")
if not confirm:
console.print("削除をキャンセルしました", style="yellow")
return

success = msgraphs_repo.sites.delete_file(site_id, file_name, folder)

if success:
console.print(f"✅ ファイルを削除しました: {file_name}", style="bold green")
console.print(f"📁 フォルダ: {folder if folder else 'ルート'}")
else:
console.print(f"❌ ファイルの削除に失敗しました: {file_name}", style="bold red")

except Exception as e:
console.print(f"❌ エラー: {e}", style="bold red")
raise typer.Exit(1)


@app.command()
def delete_files(
site_id: str = typer.Argument(..., help="SharePoint Site ID"),
file_names: list[str] = typer.Argument(..., help="削除するファイル名(複数指定可能)"),
folder: str = typer.Option("", help="ファイルがあるフォルダパス"),
force: bool = typer.Option(False, "--force", "-f", help="確認なしで削除する"),
) -> None:
"""SharePointサイトから複数のファイルを削除する"""
try:
if not force:
console.print(f"削除予定のファイル:")
for file_name in file_names:
console.print(f" 📄 {file_name}")
confirm = typer.confirm(f"{len(file_names)}個のファイルを削除しますか?")
if not confirm:
console.print("削除をキャンセルしました", style="yellow")
return

deleted_files = msgraphs_repo.sites.delete_multiple_files(site_id, file_names, folder)

console.print(f"✅ {len(deleted_files)}個のファイルを削除しました:", style="bold green")
for file_name in deleted_files:
console.print(f" 📄 {file_name}")
console.print(f"📁 フォルダ: {folder if folder else 'ルート'}")

failed_count = len(file_names) - len(deleted_files)
if failed_count > 0:
console.print(f"⚠️ {failed_count}個のファイルの削除に失敗しました", style="yellow")

except Exception as e:
console.print(f"❌ エラー: {e}", style="bold red")
raise typer.Exit(1)


@app.command()
def get_file_info(
site_id: str = typer.Argument(..., help="SharePoint Site ID"),
file_name: str = typer.Argument(..., help="情報を取得するファイル名"),
folder: str = typer.Option("", help="ファイルがあるフォルダパス"),
) -> None:
"""SharePointサイトのファイル情報を取得する"""
try:
file_info = msgraphs_repo.sites.get_file_info(site_id, file_name, folder)

table = Table(show_header=True, header_style="bold magenta")
table.add_column("項目", style="cyan")
table.add_column("値", style="white")

table.add_row("ファイル名", file_info.name)
table.add_row("サイズ", f"{file_info.size:,} bytes" if file_info.size else "不明")
table.add_row("タイプ", file_info.content_type)
table.add_row("作成日時", file_info.created_at.strftime("%Y-%m-%d %H:%M:%S") if file_info.created_at else "不明")
table.add_row("更新日時", file_info.updated_at.strftime("%Y-%m-%d %H:%M:%S") if file_info.updated_at else "不明")
table.add_row("フォルダ", folder if folder else "ルート")

console.print(table)

except Exception as e:
console.print(f"❌ エラー: {e}", style="bold red")
raise typer.Exit(1)


if __name__ == "__main__":
app()
3 changes: 2 additions & 1 deletion template_fastapi/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from opentelemetry.trace import Span

from template_fastapi.routers import chats, demos, files, foodies, games, items, speeches
from template_fastapi.routers import chats, demos, files, foodies, games, items, msgraphs, speeches

app = FastAPI()

Expand Down Expand Up @@ -42,4 +42,5 @@ def server_request_hook(span: Span, scope: dict):
app.include_router(foodies.router)
app.include_router(files.router)
app.include_router(speeches.router)
app.include_router(msgraphs.router, prefix="/msgraphs")
app.include_router(chats.router)
Loading
Loading