gsuite-sdk 0.1.0__py3-none-any.whl → 0.1.2__py3-none-any.whl

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_api/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """Google Suite Unified REST API Gateway."""
2
+
3
+ __version__ = "0.1.0"
@@ -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)]
gsuite_api/main.py ADDED
@@ -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,5 @@
1
+ """API routes."""
2
+
3
+ from gsuite_api.routes import calendar, drive, gmail, health, sheets
4
+
5
+ __all__ = ["health", "gmail", "calendar", "drive", "sheets"]
@@ -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.0
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: fastapi>=0.109.0; extra == "all"
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,3 +1,12 @@
1
+ gsuite_api/__init__.py,sha256=NteihQi3x0ke8bZuQJuyVWfVsWILNtO7-ul1KdzNPZQ,68
2
+ gsuite_api/dependencies.py,sha256=UvGbHoAO7_VbTTlZOI_LtKA4FwvCZcML0zup_ne-woQ,3043
3
+ gsuite_api/main.py,sha256=h86UuxgAyKQb2Vi8CA8CznpdDVL4qrqA30wrndEkc5I,1704
4
+ gsuite_api/routes/__init__.py,sha256=OekJCAJzrjbfbOHT3OGnS4wRv13IPCNPGAF-g3UalfM,150
5
+ gsuite_api/routes/calendar.py,sha256=siRzSIQb0wbQ7uK5OhLOteDo0WJnV4qB2PryOcWUYpo,4075
6
+ gsuite_api/routes/drive.py,sha256=2tAcS0bdMrvQ36sfL9hg1Pu2xdBQAnY29Uta4nREDYA,2015
7
+ gsuite_api/routes/gmail.py,sha256=lqHuaF3Q6fJvE-OC-Jwb9PGfq8BnoRnVz635qjf-4Zo,3971
8
+ gsuite_api/routes/health.py,sha256=KiMD8OG4CYMAAfyOYC3OORsb7kPkvhS0TJYgMtTxS-Y,4077
9
+ gsuite_api/routes/sheets.py,sha256=1SVe3amI1xXvGQpkgLAq-xcN-Vj2mM-jUoTm1TXxGnM,5617
1
10
  gsuite_calendar/__init__.py,sha256=bv5jfO0_cCq6Al7trpeE4qAlvghn9zweTYUEZN0g1TI,291
2
11
  gsuite_calendar/calendar_entity.py,sha256=banQQ4LpCgwnWmdBh_3cjduDTzp2fmBhsIqjZzU5ybw,755
3
12
  gsuite_calendar/client.py,sha256=fQQ6mxpjnIS2a6X6CO3Ff8wTcw8o-R4Ske--gVb-geI,8402
@@ -29,14 +38,14 @@ gsuite_gmail/parser.py,sha256=JeEfNkgpJdJFIQ2LXLrd7Wmr3xl2M7-uFM901o_84v8,4802
29
38
  gsuite_gmail/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
30
39
  gsuite_gmail/query.py,sha256=9UHSm6agDbQ-gAJcEd-CG7DucQ83eOMqr-xY2toCzf8,5812
31
40
  gsuite_gmail/thread.py,sha256=MiWsKgrtlvBIET3DEwZgEYVl_6L3qPRQfo3iRKYUyJw,1366
32
- gsuite_sdk-0.1.0.dist-info/licenses/LICENSE,sha256=25xFe8kmZ3TZzs93SbzlDP_OHHkT5lsANRqQ_Ao2A0M,1069
41
+ gsuite_sdk-0.1.2.dist-info/licenses/LICENSE,sha256=25xFe8kmZ3TZzs93SbzlDP_OHHkT5lsANRqQ_Ao2A0M,1069
33
42
  gsuite_sheets/__init__.py,sha256=llrLdudKQqoVQKhJE5GVMT_ibJr30vdjXZqrNjqQnXU,279
34
43
  gsuite_sheets/client.py,sha256=m1PLd0fgTHIJYWnmLldR4SCnAQtEAWs0XeqUu087mqk,10662
35
44
  gsuite_sheets/parser.py,sha256=jQcTcfwG6uYnlyXKOE5yv6Oc6nNVrgEs2nck1xLTxOQ,2267
36
45
  gsuite_sheets/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
37
46
  gsuite_sheets/spreadsheet.py,sha256=3YFQHEVw2U9KaV-fsBzkXy-mTCygbJA1HZ2edgtxY-U,2674
38
47
  gsuite_sheets/worksheet.py,sha256=stD8ZUArs3-h4yYsS_XnjcKBvjUTikz3pLvxOHyET-c,5789
39
- gsuite_sdk-0.1.0.dist-info/METADATA,sha256=D5yY2KzdCM6tnixIzsV3hiZze5pw05cU70vfyurmMOk,10775
40
- gsuite_sdk-0.1.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
41
- gsuite_sdk-0.1.0.dist-info/top_level.txt,sha256=owr7_TnX3SwC7hcmPZ0rmMqvKY16AiyfWf2HTJFf3Gs,68
42
- gsuite_sdk-0.1.0.dist-info/RECORD,,
48
+ gsuite_sdk-0.1.2.dist-info/METADATA,sha256=NkmkE5uRtpZU5umvWuWmfrxw7qLSaMKvJ-59hhJN6_I,11058
49
+ gsuite_sdk-0.1.2.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
50
+ gsuite_sdk-0.1.2.dist-info/top_level.txt,sha256=LYM5pT8_lZMNAP--cPGd4Lb8pj_tgrFlFz8UYLVQSUs,79
51
+ gsuite_sdk-0.1.2.dist-info/RECORD,,
@@ -1,3 +1,4 @@
1
+ gsuite_api
1
2
  gsuite_calendar
2
3
  gsuite_core
3
4
  gsuite_drive