Add Google Calendar and Gmail integration via Python SDK

Replaces the gws CLI subprocess approach with google-api-python-client.
Adds get_calendar_events, create_calendar_event, gmail_search_emails, and
gmail_get_email as LangChain tools; all admin-gated and auto-enabled when
token.json is present. Includes uv-runnable scripts/google_auth.py for
one-time OAuth2 setup.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-09-16 08:43:18 -04:00
parent 7912c83ca3
commit 82f16bfd6a
8 changed files with 625 additions and 38 deletions
+6 -5
View File
@@ -114,11 +114,12 @@ ALLOWED_IMESSAGE_NUMBERS=
# IMESSAGE_RATE_LIMIT_MAX=10
# IMESSAGE_RATE_LIMIT_WINDOW=60
# Google Calendar Configuration (via gws CLI)
GOOGLE_CALENDAR_ENABLED=true
# Export credentials: gws auth export --unmasked > credentials.json
# The file is mounted into the container at /app/config/gws-credentials.json
GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE=/app/config/gws-credentials.json
# Google Calendar + Gmail Configuration (Python SDK)
# Run scripts/google_auth.py to generate token.json from your OAuth2 credentials:
# python scripts/google_auth.py --credentials credentials.json --token token.json
# Then mount token.json into the container (see docker-compose.yml).
# If GOOGLE_TOKEN_FILE points to a valid token, both Calendar and Gmail are enabled.
GOOGLE_TOKEN_FILE=/app/config/google-token.json
# SimbaKit / PetKit Configuration
# PetKit account credentials (used by SimbaKit to poll device data)
+1
View File
@@ -12,6 +12,7 @@ wheels/
# Environment files
.env
credentials.json
token.json
# Database files
database/
+165 -27
View File
@@ -12,6 +12,7 @@ from tavily import AsyncTavilyClient
from blueprints.conversation.memory import save_memory
from blueprints.rag.logic import query_vector_store
from utils.google_service import GoogleService
from utils.mealie_service import MealieService
from utils.obsidian_service import ObsidianService
from utils.simbakit_service import SimbaKitService
@@ -79,6 +80,17 @@ except (ValueError, Exception) as e:
print(f"Obsidian service not initialized: {e}")
obsidian_enabled = False
# Initialize Google service (Calendar + Gmail) if token file is present
try:
google_service = GoogleService()
google_calendar_enabled = True
google_gmail_enabled = True
except (ValueError, FileNotFoundError, Exception) as e:
print(f"Google service not initialized: {e}")
google_service = None
google_calendar_enabled = False
google_gmail_enabled = False
@tool
def get_current_date() -> str:
@@ -886,12 +898,11 @@ async def get_calendar_events(
calendar_id: Calendar ID to query (default: "primary")
Returns:
Calendar events as JSON
Formatted list of calendar events with times, titles, and locations.
"""
if not config["configurable"].get("is_admin"):
return "Calendar access is restricted to admin users."
import asyncio
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
@@ -902,36 +913,161 @@ async def get_calendar_events(
if days > 0:
end = start + timedelta(days=days)
elif time_range == "tomorrow":
start = start + timedelta(days=1)
start += timedelta(days=1)
end = start + timedelta(days=1)
elif time_range == "week":
end = start + timedelta(days=7)
else:
end = start + timedelta(days=1)
cmd = [
"gws",
"calendar",
"events",
"list",
"--calendarId",
calendar_id,
"--timeMin",
start.isoformat(),
"--timeMax",
end.isoformat(),
"--singleEvents",
"true",
"--orderBy",
"startTime",
]
proc = await asyncio.create_subprocess_exec(
*cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
events = await google_service.get_calendar_events(
time_min=start.isoformat(),
time_max=end.isoformat(),
calendar_id=calendar_id,
)
if not events:
return f"No events found for {start.date()} to {end.date()}."
lines = [f"Calendar events ({start.date()} to {end.date()}):"]
for event in events:
event_start = event.get("start", {})
event_end = event.get("end", {})
start_str = event_start.get("dateTime", event_start.get("date", ""))
end_str = event_end.get("dateTime", event_end.get("date", ""))
summary = event.get("summary", "Untitled")
location = event.get("location", "")
description = (event.get("description") or "")[:80]
line = f"- {summary} ({start_str}"
if end_str and end_str != start_str:
line += f" – {end_str}"
line += ")"
if location:
line += f" @ {location}"
if description:
line += f"\n {description}"
lines.append(line)
return "\n".join(lines)
@tool
async def create_calendar_event(
summary: str,
start_time: str,
end_time: str,
description: str = "",
location: str = "",
calendar_id: str = "primary",
*,
config: RunnableConfig,
) -> str:
"""Create a new Google Calendar event.
Use this tool when the user wants to:
- Schedule a meeting or appointment
- Add an event to their calendar
- Block time on their calendar
Args:
summary: Event title
start_time: Start time in RFC3339 format (e.g. "2026-09-16T14:00:00-04:00")
end_time: End time in RFC3339 format
description: Event description (optional)
location: Event location (optional)
calendar_id: Calendar to add event to (default: "primary")
Returns:
Confirmation with the event link.
"""
if not config["configurable"].get("is_admin"):
return "Calendar access is restricted to admin users."
event = await google_service.create_calendar_event(
summary=summary,
start_time=start_time,
end_time=end_time,
description=description,
location=location,
calendar_id=calendar_id,
)
link = event.get("htmlLink", "")
return (
f"Created event '{event.get('summary')}' on {event.get('start', {}).get('dateTime', '')}."
+ (f" Link: {link}" if link else "")
)
@tool
async def gmail_search_emails(
query: str,
max_results: int = 10,
*,
config: RunnableConfig,
) -> str:
"""Search Gmail messages using Gmail search syntax.
Use this tool when the user asks about:
- Finding emails from a specific sender or about a topic
- Recent emails, unread messages, or emails in a label
- Whether they received an email about something
Args:
query: Gmail search query (e.g. "from:bank unread", "subject:invoice", "after:2026/09/01")
max_results: Maximum number of results to return (default: 10)
Returns:
List of matching emails with sender, subject, date, and snippet.
"""
if not config["configurable"].get("is_admin"):
return "Gmail access is restricted to admin users."
emails = await google_service.search_emails(query=query, max_results=max_results)
if not emails:
return f"No emails found for query: {query}"
lines = [f"Found {len(emails)} email(s) for '{query}':"]
for email in emails:
lines.append(
f"\n[{email['id']}] {email['subject']}\n"
f" From: {email['from']} | {email['date']}\n"
f" {email['snippet']}"
)
return "\n".join(lines)
@tool
async def gmail_get_email(
message_id: str,
*,
config: RunnableConfig,
) -> str:
"""Get the full content of a Gmail message by ID.
Use this tool when the user wants to:
- Read a specific email
- Get full details of an email found by gmail_search_emails
Args:
message_id: The message ID from gmail_search_emails results
Returns:
Full email content including headers and body.
"""
if not config["configurable"].get("is_admin"):
return "Gmail access is restricted to admin users."
email = await google_service.get_email(message_id=message_id)
return (
f"From: {email['from']}\n"
f"To: {email['to']}\n"
f"Subject: {email['subject']}\n"
f"Date: {email['date']}\n"
f"\n{email['body']}"
)
stdout, stderr = await proc.communicate()
if proc.returncode != 0:
return f"Calendar error: {stderr.decode()}"
return stdout.decode()
@tool
@@ -1087,8 +1223,10 @@ if obsidian_enabled:
journal_complete_task,
]
)
if os.getenv("GOOGLE_CALENDAR_ENABLED"):
tools.append(get_calendar_events)
if google_calendar_enabled:
tools.extend([get_calendar_events, create_calendar_event])
if google_gmail_enabled:
tools.extend([gmail_search_emails, gmail_get_email])
# Llama 3.1 supports native function calling via OpenAI-compatible API
main_agent = create_agent(model=model_with_fallback, tools=tools)
+2 -3
View File
@@ -46,8 +46,7 @@ x-raggr-common: &raggr-common
- S3_REGION=${S3_REGION:-garage}
- OLLAMA_HOST=${OLLAMA_HOST:-http://localhost:11434}
- FERNET_KEY=${FERNET_KEY}
- GOOGLE_CALENDAR_ENABLED=${GOOGLE_CALENDAR_ENABLED:-}
- GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE=${GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE:-/app/config/gws-credentials.json}
- GOOGLE_TOKEN_FILE=${GOOGLE_TOKEN_FILE:-/app/config/google-token.json}
- MAILGUN_API_KEY=${MAILGUN_API_KEY}
- MAILGUN_DOMAIN=${MAILGUN_DOMAIN}
- MAILGUN_WEBHOOK_SIGNING_KEY=${MAILGUN_WEBHOOK_SIGNING_KEY}
@@ -66,7 +65,7 @@ x-raggr-common: &raggr-common
condition: service_healthy
volumes:
- ./obvault:/app/data/obsidian
- ./credentials.json:/app/config/gws-credentials.json:ro
- ./token.json:/app/config/google-token.json:ro
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/api/health"]
interval: 5s
+3
View File
@@ -41,6 +41,9 @@ dependencies = [
"twilio>=9.10.2",
"aioboto3>=13.0.0",
"prometheus-client>=0.26.0",
"google-api-python-client>=2.0.0",
"google-auth-oauthlib>=1.0.0",
"google-auth-httplib2>=0.2.0",
]
[project.optional-dependencies]
+62
View File
@@ -0,0 +1,62 @@
# /// script
# requires-python = ">=3.13"
# dependencies = [
# "google-auth-oauthlib>=1.0.0",
# ]
# ///
"""One-time OAuth2 setup to generate token.json for Google Calendar and Gmail.
Usage:
uv run scripts/google_auth.py [--credentials credentials.json] [--token token.json]
Requires credentials.json downloaded from Google Cloud Console
(APIs & Services → Credentials → OAuth 2.0 Client ID → Desktop app).
"""
import argparse
import sys
from pathlib import Path
from google_auth_oauthlib.flow import InstalledAppFlow
SCOPES = [
"https://www.googleapis.com/auth/calendar.events",
"https://www.googleapis.com/auth/gmail.readonly",
]
def main():
parser = argparse.ArgumentParser(description="Authorize Google APIs for raggr")
parser.add_argument(
"--credentials",
default="credentials.json",
help="Path to OAuth2 credentials JSON from Google Cloud Console",
)
parser.add_argument(
"--token",
default="token.json",
help="Output path for the generated token file",
)
args = parser.parse_args()
creds_path = Path(args.credentials)
if not creds_path.exists():
print(f"Error: credentials file not found: {creds_path}")
print(
"Download it from Google Cloud Console → APIs & Services → Credentials → OAuth 2.0 Client ID"
)
sys.exit(1)
flow = InstalledAppFlow.from_client_secrets_file(str(creds_path), SCOPES)
creds = flow.run_local_server(port=0)
token_path = Path(args.token)
token_path.write_text(creds.to_json())
print(f"Token saved to {token_path}")
print(
f"Mount this file into Docker: - ./{token_path}:/app/config/google-token.json:ro"
)
if __name__ == "__main__":
main()
+177
View File
@@ -0,0 +1,177 @@
"""Google Calendar and Gmail service using the Python SDK."""
import asyncio
import base64
import os
from typing import Any
from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from googleapiclient.discovery import build
SCOPES = [
"https://www.googleapis.com/auth/calendar.events",
"https://www.googleapis.com/auth/gmail.readonly",
]
class GoogleService:
def __init__(self):
token_path = os.getenv("GOOGLE_TOKEN_FILE", "/app/config/google-token.json")
if not os.path.exists(token_path):
raise FileNotFoundError(f"Google token file not found: {token_path}")
self._token_path = token_path
creds = Credentials.from_authorized_user_file(token_path, SCOPES)
if creds.expired and creds.refresh_token:
creds.refresh(Request())
with open(token_path, "w") as f:
f.write(creds.to_json())
self._creds = creds
self._calendar = build("calendar", "v3", credentials=creds)
self._gmail = build("gmail", "v1", credentials=creds)
async def get_calendar_events(
self,
time_min: str,
time_max: str,
calendar_id: str = "primary",
max_results: int = 20,
) -> list[dict[str, Any]]:
def _fetch():
result = (
self._calendar.events()
.list(
calendarId=calendar_id,
timeMin=time_min,
timeMax=time_max,
maxResults=max_results,
singleEvents=True,
orderBy="startTime",
)
.execute()
)
return result.get("items", [])
return await asyncio.to_thread(_fetch)
async def create_calendar_event(
self,
summary: str,
start_time: str,
end_time: str,
description: str = "",
location: str = "",
calendar_id: str = "primary",
) -> dict[str, Any]:
def _create():
body: dict[str, Any] = {
"summary": summary,
"start": {"dateTime": start_time},
"end": {"dateTime": end_time},
}
if description:
body["description"] = description
if location:
body["location"] = location
return (
self._calendar.events()
.insert(calendarId=calendar_id, body=body)
.execute()
)
return await asyncio.to_thread(_create)
async def search_emails(
self,
query: str,
max_results: int = 10,
) -> list[dict[str, Any]]:
def _fetch():
result = (
self._gmail.users()
.messages()
.list(userId="me", q=query, maxResults=max_results)
.execute()
)
messages = result.get("messages", [])
emails = []
for msg in messages:
msg_data = (
self._gmail.users()
.messages()
.get(
userId="me",
id=msg["id"],
format="metadata",
metadataHeaders=["From", "To", "Subject", "Date"],
)
.execute()
)
headers = {
h["name"]: h["value"]
for h in msg_data.get("payload", {}).get("headers", [])
}
emails.append(
{
"id": msg_data["id"],
"from": headers.get("From", ""),
"to": headers.get("To", ""),
"subject": headers.get("Subject", ""),
"date": headers.get("Date", ""),
"snippet": msg_data.get("snippet", ""),
}
)
return emails
return await asyncio.to_thread(_fetch)
async def get_email(self, message_id: str) -> dict[str, Any]:
def _fetch():
msg_data = (
self._gmail.users()
.messages()
.get(userId="me", id=message_id, format="full")
.execute()
)
headers = {
h["name"]: h["value"]
for h in msg_data.get("payload", {}).get("headers", [])
}
body = _extract_body(msg_data.get("payload", {}))
return {
"id": msg_data["id"],
"from": headers.get("From", ""),
"to": headers.get("To", ""),
"subject": headers.get("Subject", ""),
"date": headers.get("Date", ""),
"body": body[:4000],
"snippet": msg_data.get("snippet", ""),
}
return await asyncio.to_thread(_fetch)
def _extract_body(payload: dict[str, Any]) -> str:
mime = payload.get("mimeType", "")
data = payload.get("body", {}).get("data", "")
if mime == "text/plain" and data:
return base64.urlsafe_b64decode(data).decode("utf-8", errors="replace")
if mime == "text/html" and data:
import html2text
html = base64.urlsafe_b64decode(data).decode("utf-8", errors="replace")
return html2text.html2text(html)
for part in payload.get("parts", []):
result = _extract_body(part)
if result:
return result
return ""
Generated
+209 -3
View File
@@ -1,6 +1,10 @@
version = 1
revision = 2
requires-python = ">=3.13"
resolution-markers = [
"python_full_version >= '3.14'",
"python_full_version < '3.14'",
]
[[package]]
name = "aerich"
@@ -810,6 +814,90 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" },
]
[[package]]
name = "google-api-core"
version = "2.37.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "google-auth" },
{ name = "googleapis-common-protos" },
{ name = "opentelemetry-api" },
{ name = "proto-plus" },
{ name = "protobuf" },
{ name = "requests" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8d/19/0a08cdd1198b3b74550464b8108f6976e873f0b4b035e1be3c3872345153/google_api_core-2.37.0.tar.gz", hash = "sha256:cf58f220aa797f1ffdda52194c4c7d72efeced09297c2976529f8135f6d85b9a", size = 203563, upload-time = "2026-09-14T18:52:47.064Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3a/35/6db89f5aeac2f38815105e5dab66f48a0ff035aed6e4a86c81ab0ff483bc/google_api_core-2.37.0-py3-none-any.whl", hash = "sha256:d84042a0034cce9c4304e17d2e5e9966c2fcb1a851b73799291f687a67470c8b", size = 186342, upload-time = "2026-09-14T18:52:24.064Z" },
]
[[package]]
name = "google-api-python-client"
version = "2.200.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "google-api-core" },
{ name = "google-auth" },
{ name = "google-auth-httplib2" },
{ name = "httplib2" },
{ name = "uritemplate" },
]
sdist = { url = "https://files.pythonhosted.org/packages/fd/e5/12024a0ae2fd39a54ff47a3868c47344ffff4ff1cd5edf4dff523c1a9fc1/google_api_python_client-2.200.0.tar.gz", hash = "sha256:82aa18b851328ea04867fd51c5a0c8da2e1b86ec45ce08487e902e7726d4ee50", size = 15488849, upload-time = "2026-09-01T00:04:25.545Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/13/5a/5242d5a8a6077ae73bd8a2bd7e3161f2aab476f0868fb6f85cf88b1c8835/google_api_python_client-2.200.0-py3-none-any.whl", hash = "sha256:c0c2c4072a110b7fcc30ac151744d4410cddaba99c53cb4e815463308952b638", size = 16082149, upload-time = "2026-09-01T00:04:22.839Z" },
]
[[package]]
name = "google-auth"
version = "2.58.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cryptography" },
{ name = "pyasn1-modules" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ac/ca/f398a483ce5aad18ca2f735646e45ccee2439bd94a41a4ad0cfa646bd495/google_auth-2.58.0.tar.gz", hash = "sha256:55e30cf15e737de92c5323d78cda8a83fcd57e7ffbaf900c4600039fd60a80fd", size = 380018, upload-time = "2026-09-09T20:49:38.043Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/59/13/477d90d09591b3938b45c4e11f4d8a51291682112cb5efcac961e815d562/google_auth-2.58.0-py3-none-any.whl", hash = "sha256:8a9c4645bb4c8e91668fb1934b95ae6a8687084232753639220ba9bf04a1610d", size = 262404, upload-time = "2026-09-09T20:49:33.951Z" },
]
[[package]]
name = "google-auth-httplib2"
version = "0.4.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "google-auth" },
{ name = "httplib2" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d4/74/0c8177b73734dfbd89420c162ac8754257fa0f9007fb49569493d83a17db/google_auth_httplib2-0.4.2.tar.gz", hash = "sha256:916225a6367e613c9af44d83f41688a599d3f687777846b8b91bec65085ed1f1", size = 11211, upload-time = "2026-08-25T19:18:24.611Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/20/33/e697a69f13bfd448d33a374231b89cbbb203dedfb4aae9d26126be6f6c72/google_auth_httplib2-0.4.2-py3-none-any.whl", hash = "sha256:4298dd6b1415972d0c464b7177e6a69a595e7aec5b972222ecdca342f6009647", size = 9529, upload-time = "2026-08-24T21:55:05.843Z" },
]
[[package]]
name = "google-auth-oauthlib"
version = "1.4.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "google-auth" },
{ name = "requests-oauthlib" },
]
sdist = { url = "https://files.pythonhosted.org/packages/dd/fb/e8def92f788410d96d1aff0cadfadb3f044bbffbe3d2560a1ad8fa0d9466/google_auth_oauthlib-1.4.1.tar.gz", hash = "sha256:1a83f5f2a8421dedadaa3caf25b3a710dddf85a33a63144be41c2fc79174b106", size = 22436, upload-time = "2026-08-25T19:18:25.424Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6f/32/ebf82c6ae8c1caf975b3153d763f3b765dddafa01d2a6c2a142fde28bda7/google_auth_oauthlib-1.4.1-py3-none-any.whl", hash = "sha256:a1be43ec69fe563ac9b2c4d6fc4334b323b21cbdc59a638b5fa34dd4d5a2a348", size = 19540, upload-time = "2026-08-24T21:55:06.96Z" },
]
[[package]]
name = "googleapis-common-protos"
version = "1.75.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "protobuf" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8a/c5/4353a188e2c335aee33269e8b654af228278cca8e5f0b4b5f11e5d0e9adb/googleapis_common_protos-1.75.3.tar.gz", hash = "sha256:57c435ac2c68b108999b6db075d9053e4d7a936ba57b4a3d45667b1346f1738a", size = 153905, upload-time = "2026-09-03T22:31:21.869Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1a/7a/7d79170c6ce6f12e109df2b3879d6b934010cf4f99aea8de8b7e5408c174/googleapis_common_protos-1.75.3-py3-none-any.whl", hash = "sha256:a018d2bf098ca9fb6faa08d5bb780e2a2c2f73c566f069761331386c9596d3f2", size = 306984, upload-time = "2026-09-03T22:30:45.133Z" },
]
[[package]]
name = "greenlet"
version = "3.3.0"
@@ -894,6 +982,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
]
[[package]]
name = "httplib2"
version = "0.32.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyparsing" },
]
sdist = { url = "https://files.pythonhosted.org/packages/84/f5/ccf58de92d61e3ad921119668f54ed36ca1d0cf5dcc5c1657dfb164fd78b/httplib2-0.32.0.tar.gz", hash = "sha256:48a0ef30a42db65d8f3399045e1d09ab0ba66e3b9efc360d07f80ea55d286025", size = 254283, upload-time = "2026-06-26T10:13:56.265Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/33/a0/550eec327e5f5c7b732531c489f5307efec41f047b0d703bd4ca1e5ad2db/httplib2-0.32.0-py3-none-any.whl", hash = "sha256:dc6705cacdf3fb0a2aba7629fa33c90fd93e30035db0c157325826be177e4816", size = 93148, upload-time = "2026-06-26T10:13:54.985Z" },
]
[[package]]
name = "httpx"
version = "0.28.1"
@@ -1491,6 +1591,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/06/b9/33bba5ff6fb679aa0b1f8a07e853f002a6b04b9394db3069a1270a7784ca/numpy-2.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:78c9f6560dc7e6b3990e32df7ea1a50bbd0e2a111e05209963f5ddcab7073b0b", size = 10545953, upload-time = "2025-09-09T15:58:40.576Z" },
]
[[package]]
name = "oauthlib"
version = "3.3.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/0b/5f/19930f824ffeb0ad4372da4812c50edbd1434f678c90c2733e1188edfc63/oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9", size = 185918, upload-time = "2025-06-19T22:48:08.269Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1", size = 160065, upload-time = "2025-06-19T22:48:06.508Z" },
]
[[package]]
name = "ollama"
version = "0.6.1"
@@ -1523,6 +1632,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/4d/5d/7b8dc822de474a283a190fe222d9a074e2fdecfbcb4a14ff49ad4d555404/openai-2.0.1-py3-none-any.whl", hash = "sha256:f0671423666cfd24c15010fd4732738f89f1b6d4f21c47f5c82db411cc2648d5", size = 956304, upload-time = "2025-10-01T19:49:07.497Z" },
]
[[package]]
name = "opentelemetry-api"
version = "1.44.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ee/8b/aa9e2d8b8dfa7c946f7dec5d1f8f6ba8eca062f43509a06bdb5ce93d26c0/opentelemetry_api-1.44.0.tar.gz", hash = "sha256:67647e5e9566edcf421166fdf022b3537f818635daa852b289e34604dc6fb33a", size = 72406, upload-time = "2026-07-16T15:25:32.678Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ca/6f/a04e900f465ff3221ccc395522503e2d10e79fa21f2723c8e177aae1e0d1/opentelemetry_api-1.44.0-py3-none-any.whl", hash = "sha256:94b98c893a91b88657eaac1e3ba89618cdb85be6918196705354f34728b2cdef", size = 60018, upload-time = "2026-07-16T15:25:11.657Z" },
]
[[package]]
name = "orjson"
version = "3.11.3"
@@ -1826,6 +1947,33 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" },
]
[[package]]
name = "proto-plus"
version = "1.28.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "protobuf" },
]
sdist = { url = "https://files.pythonhosted.org/packages/40/a6/4fbadcc2044034449b3f8f0ce82dcf3005d53f37c136642103fd4836a31c/proto_plus-1.28.4.tar.gz", hash = "sha256:5ff7ecad828e032a491fcb86947801768e32237f99dd049b649965b892ae9a63", size = 58679, upload-time = "2026-08-25T19:19:15.102Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/41/5d/0f04b85dafdc3250ced7f2592efc17dce7f40712e941e9632202481e600d/proto_plus-1.28.4-py3-none-any.whl", hash = "sha256:4b01341272f8a348db3f003b6143109f83ab43091019d5181b3fcdf500ab32aa", size = 50797, upload-time = "2026-08-25T19:18:12.338Z" },
]
[[package]]
name = "protobuf"
version = "7.36.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/86/73/f66c748df06e7fe24e658eddd600d19c4b40bad836c97ce2d0ad9851fb6b/protobuf-7.36.1.tar.gz", hash = "sha256:d0f6470f0ce2b84e3feaea2d4b816378b37ba4d4aa08a274305373de93e2d524", size = 512499, upload-time = "2026-08-31T22:40:04.667Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f7/6c/3a54a58f2948b0f485df9ecdd06590f15d0a7abf46a89d50c3de709ff4ff/protobuf-7.36.1-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:3cf2ee25d006cee57294a1196ea43b37feb78e0dcd1e8af5c1aeddb777655aca", size = 456046, upload-time = "2026-08-31T22:39:56.865Z" },
{ url = "https://files.pythonhosted.org/packages/6e/08/9f9548793c771095245c0eeaf0c84b76b16a5ef26158043d456f551344a0/protobuf-7.36.1-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:43d3d37b1eb24c113b9b7d02008cac44e423f00b611b7781ae998d7623972969", size = 344226, upload-time = "2026-08-31T22:39:58.263Z" },
{ url = "https://files.pythonhosted.org/packages/fe/51/1bdbd612fa3c51e42ea6f45d05e84dc748ddcd2663e1aa1e89a00a33facd/protobuf-7.36.1-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:39c518c05586c016d7874ff6079ee115bcec1ea5fbb1d177fbf7867ef4c67e44", size = 357229, upload-time = "2026-08-31T22:39:59.198Z" },
{ url = "https://files.pythonhosted.org/packages/22/df/c799fe7a05ef16ba853a59db01f3a2c5f7d0676469589ccc4874f76a2a88/protobuf-7.36.1-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:97198b77e369a0abd8e262b8f6c7266c55ddb796a3a12c76d7b8881188ed83aa", size = 343228, upload-time = "2026-08-31T22:40:00.179Z" },
{ url = "https://files.pythonhosted.org/packages/3d/33/d4724ec5d86d496fe4108e220618aa0837ad3423a7af7ccdd9684ccc77c8/protobuf-7.36.1-cp310-abi3-win32.whl", hash = "sha256:0b53ce95272aad50ad25d7ff03373743209822e8ba42ea7fad27d2bee1547d00", size = 443002, upload-time = "2026-08-31T22:40:01.32Z" },
{ url = "https://files.pythonhosted.org/packages/db/37/155788a0d8daded960375af604202805308169f9b859419ea0aa370946e2/protobuf-7.36.1-cp310-abi3-win_amd64.whl", hash = "sha256:51139351435d9b43d88a55eaa49fb6f737fbb478fb0cbf2cf694d1a04a9d3363", size = 456518, upload-time = "2026-08-31T22:40:02.494Z" },
{ url = "https://files.pythonhosted.org/packages/39/ca/c47f91d3cab175b01fd8c4f0d80fdf8613be876cc616e66ad281a59c5ddf/protobuf-7.36.1-py3-none-any.whl", hash = "sha256:7d951e46b3f963d6c264c367c437921de9d5aedd9c3f9612b9077736b4e3ad5c", size = 179813, upload-time = "2026-08-31T22:40:03.54Z" },
]
[[package]]
name = "psycopg"
version = "3.3.3"
@@ -1884,6 +2032,27 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e7/c3/26b8a0908a9db249de3b4169692e1c7c19048a9bc41a4d3209cee7dbb758/psycopg_pool-3.3.0-py3-none-any.whl", hash = "sha256:2e44329155c410b5e8666372db44276a8b1ebd8c90f1c3026ebba40d4bc81063", size = 39995, upload-time = "2025-12-01T11:34:29.761Z" },
]
[[package]]
name = "pyasn1"
version = "0.6.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a4/9a/23310166d960def5897e91fe20e5b724601b02a22e84ba1f94232c0b7f67/pyasn1-0.6.4.tar.gz", hash = "sha256:9c447d8431c947fe4c8febc4ed9e760bc29011a5b01e5c74b67025bd9fb8ce81", size = 151262, upload-time = "2026-07-09T01:12:33.988Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9a/3b/6163796d69c3977d1e4287bea4a6979161cbbdd170ebb430511e8e1999ce/pyasn1-0.6.4-py3-none-any.whl", hash = "sha256:deda9277cfd454080ec40b207fb6df82206a3a2688735233cdcd8d3d565f088b", size = 84410, upload-time = "2026-07-09T01:12:32.92Z" },
]
[[package]]
name = "pyasn1-modules"
version = "0.4.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyasn1" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" },
]
[[package]]
name = "pycparser"
version = "2.23"
@@ -1983,6 +2152,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/c4/87d27b108c2f6d773aa5183c5ae367b2a99296ea4bc16eb79f453c679e30/pymupdf-1.26.4-cp39-abi3-win_amd64.whl", hash = "sha256:0b6345a93a9afd28de2567e433055e873205c52e6b920b129ca50e836a3aeec6", size = 18743491, upload-time = "2025-08-25T14:19:01.104Z" },
]
[[package]]
name = "pyparsing"
version = "3.3.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" },
]
[[package]]
name = "pypika-tortoise"
version = "0.6.2"
@@ -2158,6 +2336,9 @@ dependencies = [
{ name = "flask" },
{ name = "flask-jwt-extended" },
{ name = "flask-login" },
{ name = "google-api-python-client" },
{ name = "google-auth-httplib2" },
{ name = "google-auth-oauthlib" },
{ name = "html2text" },
{ name = "httpx" },
{ name = "jq" },
@@ -2205,6 +2386,9 @@ requires-dist = [
{ name = "flask", specifier = ">=3.1.2" },
{ name = "flask-jwt-extended", specifier = ">=4.7.1" },
{ name = "flask-login", specifier = ">=0.6.3" },
{ name = "google-api-python-client", specifier = ">=2.0.0" },
{ name = "google-auth-httplib2", specifier = ">=0.2.0" },
{ name = "google-auth-oauthlib", specifier = ">=1.0.0" },
{ name = "html2text", specifier = ">=2025.4.15" },
{ name = "httpx", specifier = ">=0.28.1" },
{ name = "jq", specifier = ">=1.10.0" },
@@ -2303,7 +2487,7 @@ wheels = [
[[package]]
name = "requests"
version = "2.32.5"
version = "2.34.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
@@ -2311,9 +2495,22 @@ dependencies = [
{ name = "idna" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" }
sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
{ url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" },
]
[[package]]
name = "requests-oauthlib"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "oauthlib" },
{ name = "requests" },
]
sdist = { url = "https://files.pythonhosted.org/packages/42/f2/05f29bc3913aea15eb670be136045bf5c5bbf4b99ecb839da9b422bb2c85/requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9", size = 55650, upload-time = "2024-03-22T20:32:29.939Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179, upload-time = "2024-03-22T20:32:28.055Z" },
]
[[package]]
@@ -2561,6 +2758,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b0/70/d460bd685a170790ec89317e9bd33047988e4bce507b831f5db771e142de/tzdata-2026.1-py2.py3-none-any.whl", hash = "sha256:4b1d2be7ac37ceafd7327b961aa3a54e467efbdb563a23655fbfe0d39cfc42a9", size = 348952, upload-time = "2026-04-03T11:25:20.313Z" },
]
[[package]]
name = "uritemplate"
version = "4.2.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/98/60/f174043244c5306c9988380d2cb10009f91563fc4b31293d27e17201af56/uritemplate-4.2.0.tar.gz", hash = "sha256:480c2ed180878955863323eea31b0ede668795de182617fef9c6ca09e6ec9d0e", size = 33267, upload-time = "2025-06-02T15:12:06.318Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a9/99/3ae339466c9183ea5b8ae87b34c0b897eda475d2aec2307cae60e5cd4f29/uritemplate-4.2.0-py3-none-any.whl", hash = "sha256:962201ba1c4edcab02e60f9a0d3821e82dfc5d2d6662a21abd533879bdb8a686", size = 11488, upload-time = "2025-06-02T15:12:03.405Z" },
]
[[package]]
name = "urllib3"
version = "2.3.0"