gsuite-sdk 0.1.1__tar.gz → 0.1.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {gsuite_sdk-0.1.1 → gsuite_sdk-0.1.2}/PKG-INFO +11 -5
- gsuite_sdk-0.1.2/api/src/gsuite_api/__init__.py +3 -0
- gsuite_sdk-0.1.2/api/src/gsuite_api/dependencies.py +99 -0
- gsuite_sdk-0.1.2/api/src/gsuite_api/main.py +69 -0
- gsuite_sdk-0.1.2/api/src/gsuite_api/routes/__init__.py +5 -0
- gsuite_sdk-0.1.2/api/src/gsuite_api/routes/calendar.py +145 -0
- gsuite_sdk-0.1.2/api/src/gsuite_api/routes/drive.py +91 -0
- gsuite_sdk-0.1.2/api/src/gsuite_api/routes/gmail.py +148 -0
- gsuite_sdk-0.1.2/api/src/gsuite_api/routes/health.py +134 -0
- gsuite_sdk-0.1.2/api/src/gsuite_api/routes/sheets.py +203 -0
- {gsuite_sdk-0.1.1 → gsuite_sdk-0.1.2}/gsuite_sdk.egg-info/PKG-INFO +11 -5
- {gsuite_sdk-0.1.1 → gsuite_sdk-0.1.2}/gsuite_sdk.egg-info/SOURCES.txt +9 -0
- {gsuite_sdk-0.1.1 → gsuite_sdk-0.1.2}/gsuite_sdk.egg-info/requires.txt +9 -1
- {gsuite_sdk-0.1.1 → gsuite_sdk-0.1.2}/gsuite_sdk.egg-info/top_level.txt +1 -0
- {gsuite_sdk-0.1.1 → gsuite_sdk-0.1.2}/pyproject.toml +12 -4
- {gsuite_sdk-0.1.1 → gsuite_sdk-0.1.2}/LICENSE +0 -0
- {gsuite_sdk-0.1.1 → gsuite_sdk-0.1.2}/README.md +0 -0
- {gsuite_sdk-0.1.1 → gsuite_sdk-0.1.2}/gsuite_sdk.egg-info/dependency_links.txt +0 -0
- {gsuite_sdk-0.1.1 → gsuite_sdk-0.1.2}/packages/calendar/src/gsuite_calendar/__init__.py +0 -0
- {gsuite_sdk-0.1.1 → gsuite_sdk-0.1.2}/packages/calendar/src/gsuite_calendar/calendar_entity.py +0 -0
- {gsuite_sdk-0.1.1 → gsuite_sdk-0.1.2}/packages/calendar/src/gsuite_calendar/client.py +0 -0
- {gsuite_sdk-0.1.1 → gsuite_sdk-0.1.2}/packages/calendar/src/gsuite_calendar/event.py +0 -0
- {gsuite_sdk-0.1.1 → gsuite_sdk-0.1.2}/packages/calendar/src/gsuite_calendar/parser.py +0 -0
- {gsuite_sdk-0.1.1 → gsuite_sdk-0.1.2}/packages/calendar/src/gsuite_calendar/py.typed +0 -0
- {gsuite_sdk-0.1.1 → gsuite_sdk-0.1.2}/packages/core/src/gsuite_core/__init__.py +0 -0
- {gsuite_sdk-0.1.1 → gsuite_sdk-0.1.2}/packages/core/src/gsuite_core/api_utils.py +0 -0
- {gsuite_sdk-0.1.1 → gsuite_sdk-0.1.2}/packages/core/src/gsuite_core/auth/__init__.py +0 -0
- {gsuite_sdk-0.1.1 → gsuite_sdk-0.1.2}/packages/core/src/gsuite_core/auth/oauth.py +0 -0
- {gsuite_sdk-0.1.1 → gsuite_sdk-0.1.2}/packages/core/src/gsuite_core/auth/scopes.py +0 -0
- {gsuite_sdk-0.1.1 → gsuite_sdk-0.1.2}/packages/core/src/gsuite_core/config.py +0 -0
- {gsuite_sdk-0.1.1 → gsuite_sdk-0.1.2}/packages/core/src/gsuite_core/exceptions.py +0 -0
- {gsuite_sdk-0.1.1 → gsuite_sdk-0.1.2}/packages/core/src/gsuite_core/py.typed +0 -0
- {gsuite_sdk-0.1.1 → gsuite_sdk-0.1.2}/packages/core/src/gsuite_core/storage/__init__.py +0 -0
- {gsuite_sdk-0.1.1 → gsuite_sdk-0.1.2}/packages/core/src/gsuite_core/storage/base.py +0 -0
- {gsuite_sdk-0.1.1 → gsuite_sdk-0.1.2}/packages/core/src/gsuite_core/storage/secretmanager.py +0 -0
- {gsuite_sdk-0.1.1 → gsuite_sdk-0.1.2}/packages/core/src/gsuite_core/storage/sqlite.py +0 -0
- {gsuite_sdk-0.1.1 → gsuite_sdk-0.1.2}/packages/drive/src/gsuite_drive/__init__.py +0 -0
- {gsuite_sdk-0.1.1 → gsuite_sdk-0.1.2}/packages/drive/src/gsuite_drive/client.py +0 -0
- {gsuite_sdk-0.1.1 → gsuite_sdk-0.1.2}/packages/drive/src/gsuite_drive/file.py +0 -0
- {gsuite_sdk-0.1.1 → gsuite_sdk-0.1.2}/packages/drive/src/gsuite_drive/parser.py +0 -0
- {gsuite_sdk-0.1.1 → gsuite_sdk-0.1.2}/packages/drive/src/gsuite_drive/py.typed +0 -0
- {gsuite_sdk-0.1.1 → gsuite_sdk-0.1.2}/packages/gmail/src/gsuite_gmail/__init__.py +0 -0
- {gsuite_sdk-0.1.1 → gsuite_sdk-0.1.2}/packages/gmail/src/gsuite_gmail/client.py +0 -0
- {gsuite_sdk-0.1.1 → gsuite_sdk-0.1.2}/packages/gmail/src/gsuite_gmail/label.py +0 -0
- {gsuite_sdk-0.1.1 → gsuite_sdk-0.1.2}/packages/gmail/src/gsuite_gmail/message.py +0 -0
- {gsuite_sdk-0.1.1 → gsuite_sdk-0.1.2}/packages/gmail/src/gsuite_gmail/parser.py +0 -0
- {gsuite_sdk-0.1.1 → gsuite_sdk-0.1.2}/packages/gmail/src/gsuite_gmail/py.typed +0 -0
- {gsuite_sdk-0.1.1 → gsuite_sdk-0.1.2}/packages/gmail/src/gsuite_gmail/query.py +0 -0
- {gsuite_sdk-0.1.1 → gsuite_sdk-0.1.2}/packages/gmail/src/gsuite_gmail/thread.py +0 -0
- {gsuite_sdk-0.1.1 → gsuite_sdk-0.1.2}/packages/sheets/src/gsuite_sheets/__init__.py +0 -0
- {gsuite_sdk-0.1.1 → gsuite_sdk-0.1.2}/packages/sheets/src/gsuite_sheets/client.py +0 -0
- {gsuite_sdk-0.1.1 → gsuite_sdk-0.1.2}/packages/sheets/src/gsuite_sheets/parser.py +0 -0
- {gsuite_sdk-0.1.1 → gsuite_sdk-0.1.2}/packages/sheets/src/gsuite_sheets/py.typed +0 -0
- {gsuite_sdk-0.1.1 → gsuite_sdk-0.1.2}/packages/sheets/src/gsuite_sheets/spreadsheet.py +0 -0
- {gsuite_sdk-0.1.1 → gsuite_sdk-0.1.2}/packages/sheets/src/gsuite_sheets/worksheet.py +0 -0
- {gsuite_sdk-0.1.1 → gsuite_sdk-0.1.2}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gsuite-sdk
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Unified Google Workspace SDK - Gmail, Calendar, Drive, Sheets
|
|
5
5
|
Author-email: Pablo Alaniz <alanizpablo@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -30,11 +30,17 @@ Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
|
30
30
|
Requires-Dist: mypy>=1.8.0; extra == "dev"
|
|
31
31
|
Provides-Extra: cloudrun
|
|
32
32
|
Requires-Dist: google-cloud-secret-manager>=2.18.0; extra == "cloudrun"
|
|
33
|
+
Requires-Dist: google-cloud-logging>=3.9.0; extra == "cloudrun"
|
|
34
|
+
Provides-Extra: api
|
|
35
|
+
Requires-Dist: fastapi>=0.109.0; extra == "api"
|
|
36
|
+
Requires-Dist: uvicorn[standard]>=0.27.0; extra == "api"
|
|
37
|
+
Requires-Dist: python-multipart>=0.0.6; extra == "api"
|
|
38
|
+
Requires-Dist: email-validator>=2.1.0; extra == "api"
|
|
39
|
+
Provides-Extra: cli
|
|
40
|
+
Requires-Dist: typer>=0.9.0; extra == "cli"
|
|
41
|
+
Requires-Dist: rich>=13.7.0; extra == "cli"
|
|
33
42
|
Provides-Extra: all
|
|
34
|
-
Requires-Dist:
|
|
35
|
-
Requires-Dist: uvicorn>=0.27.0; extra == "all"
|
|
36
|
-
Requires-Dist: typer>=0.9.0; extra == "all"
|
|
37
|
-
Requires-Dist: rich>=13.7.0; extra == "all"
|
|
43
|
+
Requires-Dist: gsuite-sdk[api,cli,cloudrun]; extra == "all"
|
|
38
44
|
Dynamic: license-file
|
|
39
45
|
|
|
40
46
|
# Google Suite
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""FastAPI dependencies."""
|
|
2
|
+
|
|
3
|
+
from functools import lru_cache
|
|
4
|
+
from typing import Annotated
|
|
5
|
+
|
|
6
|
+
from fastapi import Depends, HTTPException, Security, status
|
|
7
|
+
from fastapi.security import APIKeyHeader
|
|
8
|
+
|
|
9
|
+
from gsuite_calendar import Calendar
|
|
10
|
+
from gsuite_core import GoogleAuth, Settings, SQLiteTokenStore, get_settings
|
|
11
|
+
from gsuite_gmail import Gmail
|
|
12
|
+
|
|
13
|
+
# API Key security
|
|
14
|
+
api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_api_key(
|
|
18
|
+
api_key: Annotated[str | None, Security(api_key_header)],
|
|
19
|
+
settings: Annotated[Settings, Depends(get_settings)],
|
|
20
|
+
) -> str | None:
|
|
21
|
+
"""Validate API key if configured."""
|
|
22
|
+
if not settings.api_key:
|
|
23
|
+
return None
|
|
24
|
+
|
|
25
|
+
if not api_key or api_key != settings.api_key:
|
|
26
|
+
raise HTTPException(
|
|
27
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
28
|
+
detail="Invalid or missing API key",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
return api_key
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@lru_cache
|
|
35
|
+
def get_auth() -> GoogleAuth:
|
|
36
|
+
"""Get shared GoogleAuth instance."""
|
|
37
|
+
settings = get_settings()
|
|
38
|
+
|
|
39
|
+
if settings.token_storage == "secretmanager":
|
|
40
|
+
settings.validate_for_secretmanager()
|
|
41
|
+
from gsuite_core import SecretManagerTokenStore
|
|
42
|
+
|
|
43
|
+
token_store = SecretManagerTokenStore(
|
|
44
|
+
project_id=settings.gcp_project_id,
|
|
45
|
+
secret_name=settings.token_secret_name,
|
|
46
|
+
)
|
|
47
|
+
else:
|
|
48
|
+
token_store = SQLiteTokenStore(settings.token_db_path)
|
|
49
|
+
|
|
50
|
+
return GoogleAuth(token_store=token_store)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def get_gmail(
|
|
54
|
+
auth: Annotated[GoogleAuth, Depends(get_auth)],
|
|
55
|
+
_api_key: Annotated[str | None, Depends(get_api_key)],
|
|
56
|
+
) -> Gmail:
|
|
57
|
+
"""Get authenticated Gmail client."""
|
|
58
|
+
if not auth.is_authenticated():
|
|
59
|
+
if auth.needs_refresh():
|
|
60
|
+
if not auth.refresh():
|
|
61
|
+
raise HTTPException(
|
|
62
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
63
|
+
detail="Token expired. Re-authenticate required.",
|
|
64
|
+
)
|
|
65
|
+
else:
|
|
66
|
+
raise HTTPException(
|
|
67
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
68
|
+
detail="Not authenticated. Run OAuth flow first.",
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
return Gmail(auth)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def get_calendar(
|
|
75
|
+
auth: Annotated[GoogleAuth, Depends(get_auth)],
|
|
76
|
+
_api_key: Annotated[str | None, Depends(get_api_key)],
|
|
77
|
+
) -> Calendar:
|
|
78
|
+
"""Get authenticated Calendar client."""
|
|
79
|
+
if not auth.is_authenticated():
|
|
80
|
+
if auth.needs_refresh():
|
|
81
|
+
if not auth.refresh():
|
|
82
|
+
raise HTTPException(
|
|
83
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
84
|
+
detail="Token expired. Re-authenticate required.",
|
|
85
|
+
)
|
|
86
|
+
else:
|
|
87
|
+
raise HTTPException(
|
|
88
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
89
|
+
detail="Not authenticated. Run OAuth flow first.",
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
return Calendar(auth)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# Type aliases
|
|
96
|
+
GmailDep = Annotated[Gmail, Depends(get_gmail)]
|
|
97
|
+
CalendarDep = Annotated[Calendar, Depends(get_calendar)]
|
|
98
|
+
AuthDep = Annotated[GoogleAuth, Depends(get_auth)]
|
|
99
|
+
ApiKeyDep = Annotated[str | None, Depends(get_api_key)]
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""FastAPI application - Unified Google Suite API Gateway."""
|
|
2
|
+
|
|
3
|
+
from contextlib import asynccontextmanager
|
|
4
|
+
|
|
5
|
+
from fastapi import FastAPI
|
|
6
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
7
|
+
|
|
8
|
+
from gsuite_api.routes import calendar, drive, gmail, health, sheets
|
|
9
|
+
from gsuite_core import get_settings
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@asynccontextmanager
|
|
13
|
+
async def lifespan(app: FastAPI):
|
|
14
|
+
"""Application lifespan handler."""
|
|
15
|
+
print("🚀 Google Suite API starting up...")
|
|
16
|
+
yield
|
|
17
|
+
print("👋 Google Suite API shutting down...")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def create_app() -> FastAPI:
|
|
21
|
+
"""Create and configure FastAPI application."""
|
|
22
|
+
settings = get_settings()
|
|
23
|
+
|
|
24
|
+
app = FastAPI(
|
|
25
|
+
title="Google Suite API",
|
|
26
|
+
description="Unified REST API for Google Workspace - Gmail, Calendar, Drive, Sheets",
|
|
27
|
+
version="0.1.0",
|
|
28
|
+
docs_url="/docs",
|
|
29
|
+
redoc_url="/redoc",
|
|
30
|
+
lifespan=lifespan,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# CORS
|
|
34
|
+
app.add_middleware(
|
|
35
|
+
CORSMiddleware,
|
|
36
|
+
allow_origins=["*"],
|
|
37
|
+
allow_credentials=True,
|
|
38
|
+
allow_methods=["*"],
|
|
39
|
+
allow_headers=["*"],
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# Routes
|
|
43
|
+
app.include_router(health.router)
|
|
44
|
+
app.include_router(gmail.router, prefix="/gmail", tags=["Gmail"])
|
|
45
|
+
app.include_router(calendar.router, prefix="/calendar", tags=["Calendar"])
|
|
46
|
+
app.include_router(drive.router, prefix="/drive", tags=["Drive"])
|
|
47
|
+
app.include_router(sheets.router, prefix="/sheets", tags=["Sheets"])
|
|
48
|
+
|
|
49
|
+
return app
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
app = create_app()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def run():
|
|
56
|
+
"""Run the API server."""
|
|
57
|
+
import uvicorn
|
|
58
|
+
|
|
59
|
+
settings = get_settings()
|
|
60
|
+
uvicorn.run(
|
|
61
|
+
"gsuite_api.main:app",
|
|
62
|
+
host=settings.host,
|
|
63
|
+
port=settings.port,
|
|
64
|
+
reload=True,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
if __name__ == "__main__":
|
|
69
|
+
run()
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Calendar API routes."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter, HTTPException, Query
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
|
|
8
|
+
from gsuite_api.dependencies import CalendarDep
|
|
9
|
+
|
|
10
|
+
router = APIRouter()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class EventResponse(BaseModel):
|
|
14
|
+
id: str
|
|
15
|
+
summary: str
|
|
16
|
+
description: str | None
|
|
17
|
+
location: str | None
|
|
18
|
+
start: str | None
|
|
19
|
+
end: str | None
|
|
20
|
+
all_day: bool
|
|
21
|
+
html_link: str | None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class CreateEventRequest(BaseModel):
|
|
25
|
+
summary: str
|
|
26
|
+
start: str # ISO format
|
|
27
|
+
end: str | None = None
|
|
28
|
+
description: str | None = None
|
|
29
|
+
location: str | None = None
|
|
30
|
+
all_day: bool = False
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@router.get("/events")
|
|
34
|
+
async def list_events(
|
|
35
|
+
calendar: CalendarDep,
|
|
36
|
+
days: int = Query(7, le=365),
|
|
37
|
+
calendar_id: str | None = None,
|
|
38
|
+
limit: int = Query(100, le=500),
|
|
39
|
+
):
|
|
40
|
+
"""Get upcoming events."""
|
|
41
|
+
events = calendar.get_upcoming(days=days, calendar_id=calendar_id, max_results=limit)
|
|
42
|
+
return {
|
|
43
|
+
"events": [
|
|
44
|
+
EventResponse(
|
|
45
|
+
id=e.id,
|
|
46
|
+
summary=e.summary,
|
|
47
|
+
description=e.description,
|
|
48
|
+
location=e.location,
|
|
49
|
+
start=e.start.isoformat() if e.start else None,
|
|
50
|
+
end=e.end.isoformat() if e.end else None,
|
|
51
|
+
all_day=e.all_day,
|
|
52
|
+
html_link=e.html_link,
|
|
53
|
+
)
|
|
54
|
+
for e in events
|
|
55
|
+
],
|
|
56
|
+
"count": len(events),
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@router.get("/events/today")
|
|
61
|
+
async def list_today(calendar: CalendarDep, calendar_id: str | None = None):
|
|
62
|
+
"""Get today's events."""
|
|
63
|
+
events = calendar.get_today(calendar_id=calendar_id)
|
|
64
|
+
return {
|
|
65
|
+
"events": [
|
|
66
|
+
EventResponse(
|
|
67
|
+
id=e.id,
|
|
68
|
+
summary=e.summary,
|
|
69
|
+
description=e.description,
|
|
70
|
+
location=e.location,
|
|
71
|
+
start=e.start.isoformat() if e.start else None,
|
|
72
|
+
end=e.end.isoformat() if e.end else None,
|
|
73
|
+
all_day=e.all_day,
|
|
74
|
+
html_link=e.html_link,
|
|
75
|
+
)
|
|
76
|
+
for e in events
|
|
77
|
+
],
|
|
78
|
+
"count": len(events),
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@router.get("/events/{event_id}")
|
|
83
|
+
async def get_event(event_id: str, calendar: CalendarDep, calendar_id: str | None = None):
|
|
84
|
+
"""Get a specific event."""
|
|
85
|
+
event = calendar.get_event(event_id, calendar_id=calendar_id)
|
|
86
|
+
if not event:
|
|
87
|
+
raise HTTPException(status_code=404, detail="Event not found")
|
|
88
|
+
|
|
89
|
+
return EventResponse(
|
|
90
|
+
id=event.id,
|
|
91
|
+
summary=event.summary,
|
|
92
|
+
description=event.description,
|
|
93
|
+
location=event.location,
|
|
94
|
+
start=event.start.isoformat() if event.start else None,
|
|
95
|
+
end=event.end.isoformat() if event.end else None,
|
|
96
|
+
all_day=event.all_day,
|
|
97
|
+
html_link=event.html_link,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@router.post("/events")
|
|
102
|
+
async def create_event(request: CreateEventRequest, calendar: CalendarDep):
|
|
103
|
+
"""Create a new event."""
|
|
104
|
+
start = datetime.fromisoformat(request.start)
|
|
105
|
+
end = datetime.fromisoformat(request.end) if request.end else None
|
|
106
|
+
|
|
107
|
+
event = calendar.create_event(
|
|
108
|
+
summary=request.summary,
|
|
109
|
+
start=start,
|
|
110
|
+
end=end,
|
|
111
|
+
description=request.description,
|
|
112
|
+
location=request.location,
|
|
113
|
+
all_day=request.all_day,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
"id": event.id,
|
|
118
|
+
"summary": event.summary,
|
|
119
|
+
"status": "created",
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@router.delete("/events/{event_id}")
|
|
124
|
+
async def delete_event(event_id: str, calendar: CalendarDep, calendar_id: str | None = None):
|
|
125
|
+
"""Delete an event."""
|
|
126
|
+
success = calendar.delete_event(event_id, calendar_id=calendar_id)
|
|
127
|
+
return {"status": "deleted" if success else "failed"}
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@router.get("/calendars")
|
|
131
|
+
async def list_calendars(calendar: CalendarDep):
|
|
132
|
+
"""List all accessible calendars."""
|
|
133
|
+
calendars = calendar.get_calendars()
|
|
134
|
+
return {
|
|
135
|
+
"calendars": [
|
|
136
|
+
{
|
|
137
|
+
"id": c.id,
|
|
138
|
+
"summary": c.summary,
|
|
139
|
+
"primary": c.primary,
|
|
140
|
+
"access_role": c.access_role,
|
|
141
|
+
}
|
|
142
|
+
for c in calendars
|
|
143
|
+
],
|
|
144
|
+
"count": len(calendars),
|
|
145
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Drive API routes."""
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter, File, Query, UploadFile
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
|
|
6
|
+
router = APIRouter()
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class FileResponse(BaseModel):
|
|
10
|
+
id: str
|
|
11
|
+
name: str
|
|
12
|
+
mime_type: str
|
|
13
|
+
size: int
|
|
14
|
+
web_view_link: str | None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CreateFolderRequest(BaseModel):
|
|
18
|
+
name: str
|
|
19
|
+
parent_id: str | None = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@router.get("/files")
|
|
23
|
+
async def list_files(
|
|
24
|
+
query: str | None = Query(None, description="Drive search query"),
|
|
25
|
+
parent_id: str | None = Query(None, description="Parent folder ID"),
|
|
26
|
+
limit: int = Query(100, le=1000),
|
|
27
|
+
):
|
|
28
|
+
"""
|
|
29
|
+
List Drive files.
|
|
30
|
+
|
|
31
|
+
Note: Drive routes are placeholders. Install gsuite-drive and configure auth.
|
|
32
|
+
"""
|
|
33
|
+
return {
|
|
34
|
+
"status": "placeholder",
|
|
35
|
+
"message": "Drive API coming soon. Install gsuite-drive package.",
|
|
36
|
+
"files": [],
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@router.get("/files/{file_id}")
|
|
41
|
+
async def get_file(file_id: str):
|
|
42
|
+
"""Get a specific file."""
|
|
43
|
+
return {
|
|
44
|
+
"status": "placeholder",
|
|
45
|
+
"message": "Drive API coming soon",
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@router.post("/files/upload")
|
|
50
|
+
async def upload_file(
|
|
51
|
+
file: UploadFile = File(...),
|
|
52
|
+
parent_id: str | None = None,
|
|
53
|
+
):
|
|
54
|
+
"""Upload a file."""
|
|
55
|
+
return {
|
|
56
|
+
"status": "placeholder",
|
|
57
|
+
"message": "Drive API coming soon",
|
|
58
|
+
"filename": file.filename,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@router.post("/folders")
|
|
63
|
+
async def create_folder(request: CreateFolderRequest):
|
|
64
|
+
"""Create a folder."""
|
|
65
|
+
return {
|
|
66
|
+
"status": "placeholder",
|
|
67
|
+
"message": "Drive API coming soon",
|
|
68
|
+
"name": request.name,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@router.delete("/files/{file_id}")
|
|
73
|
+
async def delete_file(file_id: str):
|
|
74
|
+
"""Delete a file."""
|
|
75
|
+
return {
|
|
76
|
+
"status": "placeholder",
|
|
77
|
+
"message": "Drive API coming soon",
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@router.post("/files/{file_id}/share")
|
|
82
|
+
async def share_file(
|
|
83
|
+
file_id: str,
|
|
84
|
+
email: str = Query(...),
|
|
85
|
+
role: str = Query("reader"),
|
|
86
|
+
):
|
|
87
|
+
"""Share a file."""
|
|
88
|
+
return {
|
|
89
|
+
"status": "placeholder",
|
|
90
|
+
"message": "Drive API coming soon",
|
|
91
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""Gmail API routes."""
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter, HTTPException, Query
|
|
4
|
+
from pydantic import BaseModel, EmailStr
|
|
5
|
+
|
|
6
|
+
from gsuite_api.dependencies import GmailDep
|
|
7
|
+
|
|
8
|
+
router = APIRouter()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class MessageResponse(BaseModel):
|
|
12
|
+
id: str
|
|
13
|
+
thread_id: str
|
|
14
|
+
subject: str
|
|
15
|
+
sender: str
|
|
16
|
+
recipient: str
|
|
17
|
+
date: str | None
|
|
18
|
+
snippet: str
|
|
19
|
+
is_unread: bool
|
|
20
|
+
is_starred: bool
|
|
21
|
+
labels: list[str]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class SendRequest(BaseModel):
|
|
25
|
+
to: list[EmailStr]
|
|
26
|
+
subject: str
|
|
27
|
+
body: str
|
|
28
|
+
cc: list[EmailStr] | None = None
|
|
29
|
+
html: bool = False
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@router.get("/messages")
|
|
33
|
+
async def list_messages(
|
|
34
|
+
gmail: GmailDep,
|
|
35
|
+
query: str | None = Query(None),
|
|
36
|
+
limit: int = Query(25, le=100),
|
|
37
|
+
):
|
|
38
|
+
"""List messages with optional search."""
|
|
39
|
+
messages = gmail.get_messages(query=query, max_results=limit)
|
|
40
|
+
return {
|
|
41
|
+
"messages": [
|
|
42
|
+
MessageResponse(
|
|
43
|
+
id=m.id,
|
|
44
|
+
thread_id=m.thread_id,
|
|
45
|
+
subject=m.subject,
|
|
46
|
+
sender=m.sender,
|
|
47
|
+
recipient=m.recipient,
|
|
48
|
+
date=m.date.isoformat() if m.date else None,
|
|
49
|
+
snippet=m.snippet,
|
|
50
|
+
is_unread=m.is_unread,
|
|
51
|
+
is_starred=m.is_starred,
|
|
52
|
+
labels=m.labels,
|
|
53
|
+
)
|
|
54
|
+
for m in messages
|
|
55
|
+
],
|
|
56
|
+
"count": len(messages),
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@router.get("/messages/unread")
|
|
61
|
+
async def list_unread(gmail: GmailDep, limit: int = Query(25, le=100)):
|
|
62
|
+
"""Get unread messages."""
|
|
63
|
+
messages = gmail.get_unread(max_results=limit)
|
|
64
|
+
return {
|
|
65
|
+
"messages": [
|
|
66
|
+
MessageResponse(
|
|
67
|
+
id=m.id,
|
|
68
|
+
thread_id=m.thread_id,
|
|
69
|
+
subject=m.subject,
|
|
70
|
+
sender=m.sender,
|
|
71
|
+
recipient=m.recipient,
|
|
72
|
+
date=m.date.isoformat() if m.date else None,
|
|
73
|
+
snippet=m.snippet,
|
|
74
|
+
is_unread=m.is_unread,
|
|
75
|
+
is_starred=m.is_starred,
|
|
76
|
+
labels=m.labels,
|
|
77
|
+
)
|
|
78
|
+
for m in messages
|
|
79
|
+
],
|
|
80
|
+
"count": len(messages),
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@router.get("/messages/{message_id}")
|
|
85
|
+
async def get_message(message_id: str, gmail: GmailDep):
|
|
86
|
+
"""Get a specific message."""
|
|
87
|
+
message = gmail.get_message(message_id)
|
|
88
|
+
if not message:
|
|
89
|
+
raise HTTPException(status_code=404, detail="Message not found")
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
"id": message.id,
|
|
93
|
+
"thread_id": message.thread_id,
|
|
94
|
+
"subject": message.subject,
|
|
95
|
+
"sender": message.sender,
|
|
96
|
+
"recipient": message.recipient,
|
|
97
|
+
"date": message.date.isoformat() if message.date else None,
|
|
98
|
+
"snippet": message.snippet,
|
|
99
|
+
"body": message.body,
|
|
100
|
+
"labels": message.labels,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@router.post("/messages/send")
|
|
105
|
+
async def send_message(request: SendRequest, gmail: GmailDep):
|
|
106
|
+
"""Send an email."""
|
|
107
|
+
message = gmail.send(
|
|
108
|
+
to=[str(e) for e in request.to],
|
|
109
|
+
subject=request.subject,
|
|
110
|
+
body=request.body,
|
|
111
|
+
cc=[str(e) for e in request.cc] if request.cc else None,
|
|
112
|
+
html=request.html,
|
|
113
|
+
)
|
|
114
|
+
return {"id": message.id, "status": "sent"}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@router.post("/messages/{message_id}/read")
|
|
118
|
+
async def mark_as_read(message_id: str, gmail: GmailDep):
|
|
119
|
+
"""Mark message as read."""
|
|
120
|
+
message = gmail.get_message(message_id)
|
|
121
|
+
if not message:
|
|
122
|
+
raise HTTPException(status_code=404, detail="Message not found")
|
|
123
|
+
message.mark_as_read()
|
|
124
|
+
return {"status": "success"}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@router.get("/labels")
|
|
128
|
+
async def list_labels(gmail: GmailDep):
|
|
129
|
+
"""List all labels."""
|
|
130
|
+
labels = gmail.get_labels()
|
|
131
|
+
return {
|
|
132
|
+
"labels": [
|
|
133
|
+
{
|
|
134
|
+
"id": l.id,
|
|
135
|
+
"name": l.name,
|
|
136
|
+
"type": l.type.value,
|
|
137
|
+
"messages_unread": l.messages_unread,
|
|
138
|
+
}
|
|
139
|
+
for l in labels
|
|
140
|
+
],
|
|
141
|
+
"count": len(labels),
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@router.get("/profile")
|
|
146
|
+
async def get_profile(gmail: GmailDep):
|
|
147
|
+
"""Get user profile."""
|
|
148
|
+
return gmail.get_profile()
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""Health check routes."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from datetime import UTC, datetime, timedelta
|
|
5
|
+
|
|
6
|
+
from fastapi import APIRouter, HTTPException, Query, Security
|
|
7
|
+
from fastapi.security import APIKeyHeader
|
|
8
|
+
|
|
9
|
+
from gsuite_api.dependencies import AuthDep
|
|
10
|
+
from gsuite_core import get_settings
|
|
11
|
+
|
|
12
|
+
router = APIRouter(tags=["Health"])
|
|
13
|
+
|
|
14
|
+
# Admin API key for logs endpoint
|
|
15
|
+
admin_key_header = APIKeyHeader(name="X-Admin-Key", auto_error=False)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def verify_admin_key(
|
|
19
|
+
admin_key: str | None = Security(admin_key_header),
|
|
20
|
+
api_key: str | None = Query(None, alias="api_key"),
|
|
21
|
+
) -> str:
|
|
22
|
+
"""Verify admin API key from header or query param."""
|
|
23
|
+
key = admin_key or api_key
|
|
24
|
+
expected = os.environ.get("ADMIN_API_KEY")
|
|
25
|
+
|
|
26
|
+
if not expected:
|
|
27
|
+
raise HTTPException(status_code=503, detail="Admin API key not configured")
|
|
28
|
+
|
|
29
|
+
if not key or key != expected:
|
|
30
|
+
raise HTTPException(status_code=401, detail="Invalid admin key")
|
|
31
|
+
|
|
32
|
+
return key
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@router.get("/health")
|
|
36
|
+
async def health_check():
|
|
37
|
+
"""Health check endpoint."""
|
|
38
|
+
settings = get_settings()
|
|
39
|
+
return {
|
|
40
|
+
"status": "healthy",
|
|
41
|
+
"service": "gsuite-api",
|
|
42
|
+
"version": settings.version,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@router.get("/health/auth")
|
|
47
|
+
async def auth_status(auth: AuthDep):
|
|
48
|
+
"""Check authentication status."""
|
|
49
|
+
return {
|
|
50
|
+
"authenticated": auth.is_authenticated(),
|
|
51
|
+
"needs_refresh": auth.needs_refresh(),
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@router.get("/health/admin/logs")
|
|
56
|
+
async def get_logs(
|
|
57
|
+
_admin: str = Security(verify_admin_key),
|
|
58
|
+
severity: str = Query("ERROR", description="Minimum severity: DEBUG, INFO, WARNING, ERROR"),
|
|
59
|
+
limit: int = Query(50, ge=1, le=500),
|
|
60
|
+
hours: int = Query(24, ge=1, le=168, description="Hours to look back"),
|
|
61
|
+
):
|
|
62
|
+
"""
|
|
63
|
+
Fetch recent logs from Cloud Logging.
|
|
64
|
+
|
|
65
|
+
Requires X-Admin-Key header or api_key query param.
|
|
66
|
+
"""
|
|
67
|
+
try:
|
|
68
|
+
from google.cloud import logging as cloud_logging
|
|
69
|
+
except ImportError:
|
|
70
|
+
raise HTTPException(status_code=503, detail="google-cloud-logging not installed")
|
|
71
|
+
|
|
72
|
+
project_id = os.environ.get("GOOGLE_CLOUD_PROJECT") or os.environ.get("GCP_PROJECT")
|
|
73
|
+
service_name = os.environ.get("K_SERVICE", "google-suite")
|
|
74
|
+
|
|
75
|
+
if not project_id:
|
|
76
|
+
raise HTTPException(status_code=503, detail="GCP project not configured")
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
client = cloud_logging.Client(project=project_id)
|
|
80
|
+
|
|
81
|
+
# Calculate time filter
|
|
82
|
+
now = datetime.now(UTC)
|
|
83
|
+
start_time = now - timedelta(hours=hours)
|
|
84
|
+
|
|
85
|
+
# Build filter - severity uses name not number
|
|
86
|
+
severity_upper = severity.upper()
|
|
87
|
+
if severity_upper not in ("DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"):
|
|
88
|
+
severity_upper = "ERROR"
|
|
89
|
+
|
|
90
|
+
filter_str = f"""
|
|
91
|
+
resource.type="cloud_run_revision"
|
|
92
|
+
resource.labels.service_name="{service_name}"
|
|
93
|
+
timestamp>="{start_time.isoformat()}"
|
|
94
|
+
severity>={severity_upper}
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
entries = list(
|
|
98
|
+
client.list_entries(
|
|
99
|
+
filter_=filter_str,
|
|
100
|
+
order_by=cloud_logging.DESCENDING,
|
|
101
|
+
max_results=limit,
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
logs = []
|
|
106
|
+
for entry in entries:
|
|
107
|
+
payload = entry.payload
|
|
108
|
+
if isinstance(payload, dict):
|
|
109
|
+
message = payload.get("message", str(payload))
|
|
110
|
+
else:
|
|
111
|
+
message = str(payload) if payload else ""
|
|
112
|
+
|
|
113
|
+
logs.append(
|
|
114
|
+
{
|
|
115
|
+
"timestamp": entry.timestamp.isoformat() if entry.timestamp else None,
|
|
116
|
+
"severity": entry.severity or "DEFAULT",
|
|
117
|
+
"message": message[:2000], # Truncate long messages
|
|
118
|
+
}
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
"service": service_name,
|
|
123
|
+
"project": project_id,
|
|
124
|
+
"filter": {
|
|
125
|
+
"severity": severity,
|
|
126
|
+
"hours": hours,
|
|
127
|
+
"limit": limit,
|
|
128
|
+
},
|
|
129
|
+
"count": len(logs),
|
|
130
|
+
"logs": logs,
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
except Exception as e:
|
|
134
|
+
raise HTTPException(status_code=500, detail=f"Failed to fetch logs: {str(e)}")
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""Sheets API routes."""
|
|
2
|
+
|
|
3
|
+
from typing import Annotated, Any
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
|
|
8
|
+
from gsuite_api.dependencies import get_auth
|
|
9
|
+
from gsuite_core import GoogleAuth
|
|
10
|
+
from gsuite_sheets import Sheets
|
|
11
|
+
|
|
12
|
+
router = APIRouter()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_sheets(auth: Annotated[GoogleAuth, Depends(get_auth)]) -> Sheets:
|
|
16
|
+
"""Get authenticated Sheets client."""
|
|
17
|
+
if not auth.is_authenticated():
|
|
18
|
+
if auth.needs_refresh():
|
|
19
|
+
if not auth.refresh():
|
|
20
|
+
raise HTTPException(
|
|
21
|
+
status_code=401,
|
|
22
|
+
detail="Token expired. Re-authenticate required.",
|
|
23
|
+
)
|
|
24
|
+
else:
|
|
25
|
+
raise HTTPException(
|
|
26
|
+
status_code=401,
|
|
27
|
+
detail="Not authenticated. Run OAuth flow first.",
|
|
28
|
+
)
|
|
29
|
+
return Sheets(auth)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
SheetsDep = Annotated[Sheets, Depends(get_sheets)]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class UpdateRequest(BaseModel):
|
|
36
|
+
range: str
|
|
37
|
+
values: list[list[Any]]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class AppendRequest(BaseModel):
|
|
41
|
+
values: list[list[Any]]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class BatchUpdateRequest(BaseModel):
|
|
45
|
+
data: list[UpdateRequest]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@router.get("/list")
|
|
49
|
+
async def list_spreadsheets(
|
|
50
|
+
sheets: SheetsDep,
|
|
51
|
+
limit: int = Query(50, le=100),
|
|
52
|
+
):
|
|
53
|
+
"""List all spreadsheets accessible to the user."""
|
|
54
|
+
try:
|
|
55
|
+
spreadsheets = sheets.list_spreadsheets(max_results=limit)
|
|
56
|
+
return {
|
|
57
|
+
"count": len(spreadsheets),
|
|
58
|
+
"spreadsheets": spreadsheets,
|
|
59
|
+
}
|
|
60
|
+
except Exception as e:
|
|
61
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@router.get("/{spreadsheet_id}")
|
|
65
|
+
async def get_spreadsheet(
|
|
66
|
+
sheets: SheetsDep,
|
|
67
|
+
spreadsheet_id: str,
|
|
68
|
+
):
|
|
69
|
+
"""Get spreadsheet metadata and worksheets."""
|
|
70
|
+
try:
|
|
71
|
+
doc = sheets.open_by_key(spreadsheet_id)
|
|
72
|
+
return {
|
|
73
|
+
"id": doc.id,
|
|
74
|
+
"title": doc.title,
|
|
75
|
+
"url": doc.url,
|
|
76
|
+
"locale": doc.locale,
|
|
77
|
+
"time_zone": doc.time_zone,
|
|
78
|
+
"worksheets": [
|
|
79
|
+
{
|
|
80
|
+
"id": ws.id,
|
|
81
|
+
"title": ws.title,
|
|
82
|
+
"index": ws.index,
|
|
83
|
+
"row_count": ws.row_count,
|
|
84
|
+
"column_count": ws.column_count,
|
|
85
|
+
}
|
|
86
|
+
for ws in doc.worksheets
|
|
87
|
+
],
|
|
88
|
+
}
|
|
89
|
+
except Exception as e:
|
|
90
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@router.get("/{spreadsheet_id}/values/{range:path}")
|
|
94
|
+
async def get_values(
|
|
95
|
+
sheets: SheetsDep,
|
|
96
|
+
spreadsheet_id: str,
|
|
97
|
+
range: str,
|
|
98
|
+
):
|
|
99
|
+
"""Get values from a range (e.g., Sheet1!A1:D10)."""
|
|
100
|
+
try:
|
|
101
|
+
values = sheets.get_values(spreadsheet_id, range)
|
|
102
|
+
return {
|
|
103
|
+
"spreadsheet_id": spreadsheet_id,
|
|
104
|
+
"range": range,
|
|
105
|
+
"values": values,
|
|
106
|
+
}
|
|
107
|
+
except Exception as e:
|
|
108
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@router.put("/{spreadsheet_id}/values/{range:path}")
|
|
112
|
+
async def update_values(
|
|
113
|
+
sheets: SheetsDep,
|
|
114
|
+
spreadsheet_id: str,
|
|
115
|
+
range: str,
|
|
116
|
+
request: UpdateRequest,
|
|
117
|
+
):
|
|
118
|
+
"""Update values in a range."""
|
|
119
|
+
try:
|
|
120
|
+
result = sheets.update_values(spreadsheet_id, range, request.values)
|
|
121
|
+
return {
|
|
122
|
+
"spreadsheet_id": spreadsheet_id,
|
|
123
|
+
"range": range,
|
|
124
|
+
"updated_cells": result.get("updatedCells", 0),
|
|
125
|
+
"updated_rows": result.get("updatedRows", 0),
|
|
126
|
+
"updated_columns": result.get("updatedColumns", 0),
|
|
127
|
+
}
|
|
128
|
+
except Exception as e:
|
|
129
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@router.post("/{spreadsheet_id}/values/{sheet_name}:append")
|
|
133
|
+
async def append_values(
|
|
134
|
+
sheets: SheetsDep,
|
|
135
|
+
spreadsheet_id: str,
|
|
136
|
+
sheet_name: str,
|
|
137
|
+
request: AppendRequest,
|
|
138
|
+
):
|
|
139
|
+
"""Append values to a sheet."""
|
|
140
|
+
try:
|
|
141
|
+
result = sheets.append_values(spreadsheet_id, sheet_name, request.values)
|
|
142
|
+
return {
|
|
143
|
+
"spreadsheet_id": spreadsheet_id,
|
|
144
|
+
"sheet": sheet_name,
|
|
145
|
+
"appended_rows": len(request.values),
|
|
146
|
+
"updates": result.get("updates", {}),
|
|
147
|
+
}
|
|
148
|
+
except Exception as e:
|
|
149
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@router.post("/{spreadsheet_id}/values:batchUpdate")
|
|
153
|
+
async def batch_update(
|
|
154
|
+
sheets: SheetsDep,
|
|
155
|
+
spreadsheet_id: str,
|
|
156
|
+
request: BatchUpdateRequest,
|
|
157
|
+
):
|
|
158
|
+
"""Batch update multiple ranges."""
|
|
159
|
+
try:
|
|
160
|
+
data = [{"range": d.range, "values": d.values} for d in request.data]
|
|
161
|
+
result = sheets.batch_update(spreadsheet_id, data)
|
|
162
|
+
return {
|
|
163
|
+
"spreadsheet_id": spreadsheet_id,
|
|
164
|
+
"updated_ranges": len(request.data),
|
|
165
|
+
"responses": result.get("responses", []),
|
|
166
|
+
}
|
|
167
|
+
except Exception as e:
|
|
168
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@router.delete("/{spreadsheet_id}/values/{range:path}")
|
|
172
|
+
async def clear_values(
|
|
173
|
+
sheets: SheetsDep,
|
|
174
|
+
spreadsheet_id: str,
|
|
175
|
+
range: str,
|
|
176
|
+
):
|
|
177
|
+
"""Clear values from a range."""
|
|
178
|
+
try:
|
|
179
|
+
sheets.clear_values(spreadsheet_id, range)
|
|
180
|
+
return {
|
|
181
|
+
"spreadsheet_id": spreadsheet_id,
|
|
182
|
+
"range": range,
|
|
183
|
+
"cleared": True,
|
|
184
|
+
}
|
|
185
|
+
except Exception as e:
|
|
186
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@router.post("/create")
|
|
190
|
+
async def create_spreadsheet(
|
|
191
|
+
sheets: SheetsDep,
|
|
192
|
+
title: str = Query(...),
|
|
193
|
+
):
|
|
194
|
+
"""Create a new spreadsheet."""
|
|
195
|
+
try:
|
|
196
|
+
doc = sheets.create(title)
|
|
197
|
+
return {
|
|
198
|
+
"id": doc.id,
|
|
199
|
+
"title": doc.title,
|
|
200
|
+
"url": doc.url,
|
|
201
|
+
}
|
|
202
|
+
except Exception as e:
|
|
203
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gsuite-sdk
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Unified Google Workspace SDK - Gmail, Calendar, Drive, Sheets
|
|
5
5
|
Author-email: Pablo Alaniz <alanizpablo@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -30,11 +30,17 @@ Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
|
30
30
|
Requires-Dist: mypy>=1.8.0; extra == "dev"
|
|
31
31
|
Provides-Extra: cloudrun
|
|
32
32
|
Requires-Dist: google-cloud-secret-manager>=2.18.0; extra == "cloudrun"
|
|
33
|
+
Requires-Dist: google-cloud-logging>=3.9.0; extra == "cloudrun"
|
|
34
|
+
Provides-Extra: api
|
|
35
|
+
Requires-Dist: fastapi>=0.109.0; extra == "api"
|
|
36
|
+
Requires-Dist: uvicorn[standard]>=0.27.0; extra == "api"
|
|
37
|
+
Requires-Dist: python-multipart>=0.0.6; extra == "api"
|
|
38
|
+
Requires-Dist: email-validator>=2.1.0; extra == "api"
|
|
39
|
+
Provides-Extra: cli
|
|
40
|
+
Requires-Dist: typer>=0.9.0; extra == "cli"
|
|
41
|
+
Requires-Dist: rich>=13.7.0; extra == "cli"
|
|
33
42
|
Provides-Extra: all
|
|
34
|
-
Requires-Dist:
|
|
35
|
-
Requires-Dist: uvicorn>=0.27.0; extra == "all"
|
|
36
|
-
Requires-Dist: typer>=0.9.0; extra == "all"
|
|
37
|
-
Requires-Dist: rich>=13.7.0; extra == "all"
|
|
43
|
+
Requires-Dist: gsuite-sdk[api,cli,cloudrun]; extra == "all"
|
|
38
44
|
Dynamic: license-file
|
|
39
45
|
|
|
40
46
|
# Google Suite
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
LICENSE
|
|
2
2
|
README.md
|
|
3
3
|
pyproject.toml
|
|
4
|
+
api/src/gsuite_api/__init__.py
|
|
5
|
+
api/src/gsuite_api/dependencies.py
|
|
6
|
+
api/src/gsuite_api/main.py
|
|
7
|
+
api/src/gsuite_api/routes/__init__.py
|
|
8
|
+
api/src/gsuite_api/routes/calendar.py
|
|
9
|
+
api/src/gsuite_api/routes/drive.py
|
|
10
|
+
api/src/gsuite_api/routes/gmail.py
|
|
11
|
+
api/src/gsuite_api/routes/health.py
|
|
12
|
+
api/src/gsuite_api/routes/sheets.py
|
|
4
13
|
gsuite_sdk.egg-info/PKG-INFO
|
|
5
14
|
gsuite_sdk.egg-info/SOURCES.txt
|
|
6
15
|
gsuite_sdk.egg-info/dependency_links.txt
|
|
@@ -5,13 +5,21 @@ pydantic>=2.5.0
|
|
|
5
5
|
pydantic-settings>=2.1.0
|
|
6
6
|
|
|
7
7
|
[all]
|
|
8
|
+
gsuite-sdk[api,cli,cloudrun]
|
|
9
|
+
|
|
10
|
+
[api]
|
|
8
11
|
fastapi>=0.109.0
|
|
9
|
-
uvicorn>=0.27.0
|
|
12
|
+
uvicorn[standard]>=0.27.0
|
|
13
|
+
python-multipart>=0.0.6
|
|
14
|
+
email-validator>=2.1.0
|
|
15
|
+
|
|
16
|
+
[cli]
|
|
10
17
|
typer>=0.9.0
|
|
11
18
|
rich>=13.7.0
|
|
12
19
|
|
|
13
20
|
[cloudrun]
|
|
14
21
|
google-cloud-secret-manager>=2.18.0
|
|
22
|
+
google-cloud-logging>=3.9.0
|
|
15
23
|
|
|
16
24
|
[dev]
|
|
17
25
|
pytest>=7.4.0
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "gsuite-sdk"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.2"
|
|
8
8
|
description = "Unified Google Workspace SDK - Gmail, Calendar, Drive, Sheets"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|
|
@@ -39,13 +39,21 @@ dev = [
|
|
|
39
39
|
]
|
|
40
40
|
cloudrun = [
|
|
41
41
|
"google-cloud-secret-manager>=2.18.0",
|
|
42
|
+
"google-cloud-logging>=3.9.0",
|
|
42
43
|
]
|
|
43
|
-
|
|
44
|
+
api = [
|
|
44
45
|
"fastapi>=0.109.0",
|
|
45
|
-
"uvicorn>=0.27.0",
|
|
46
|
+
"uvicorn[standard]>=0.27.0",
|
|
47
|
+
"python-multipart>=0.0.6",
|
|
48
|
+
"email-validator>=2.1.0",
|
|
49
|
+
]
|
|
50
|
+
cli = [
|
|
46
51
|
"typer>=0.9.0",
|
|
47
52
|
"rich>=13.7.0",
|
|
48
53
|
]
|
|
54
|
+
all = [
|
|
55
|
+
"gsuite-sdk[api,cli,cloudrun]",
|
|
56
|
+
]
|
|
49
57
|
|
|
50
58
|
[project.urls]
|
|
51
59
|
Homepage = "https://github.com/PabloAlaniz/google-suite"
|
|
@@ -85,7 +93,7 @@ testpaths = ["tests", "packages/*/tests"]
|
|
|
85
93
|
addopts = "-v --tb=short --import-mode=importlib"
|
|
86
94
|
|
|
87
95
|
[tool.setuptools.packages.find]
|
|
88
|
-
where = ["packages/core/src", "packages/gmail/src", "packages/calendar/src", "packages/drive/src", "packages/sheets/src"]
|
|
96
|
+
where = ["packages/core/src", "packages/gmail/src", "packages/calendar/src", "packages/drive/src", "packages/sheets/src", "api/src"]
|
|
89
97
|
include = ["gsuite_*"]
|
|
90
98
|
|
|
91
99
|
[tool.setuptools.package-data]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gsuite_sdk-0.1.1 → gsuite_sdk-0.1.2}/packages/calendar/src/gsuite_calendar/calendar_entity.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gsuite_sdk-0.1.1 → gsuite_sdk-0.1.2}/packages/core/src/gsuite_core/storage/secretmanager.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|