diff --git a/.env.example b/.env.example index 549c4c5..3c2ee98 100644 --- a/.env.example +++ b/.env.example @@ -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) diff --git a/.gitignore b/.gitignore index 50f3e66..a09bbf6 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ wheels/ # Environment files .env credentials.json +token.json # Database files database/ diff --git a/blueprints/conversation/agents.py b/blueprints/conversation/agents.py index b8b062e..8e7a2aa 100644 --- a/blueprints/conversation/agents.py +++ b/blueprints/conversation/agents.py @@ -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) diff --git a/docker-compose.yml b/docker-compose.yml index c078f00..fe85505 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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 diff --git a/pyproject.toml b/pyproject.toml index f71b154..dbd467c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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] diff --git a/scripts/google_auth.py b/scripts/google_auth.py new file mode 100644 index 0000000..7482b38 --- /dev/null +++ b/scripts/google_auth.py @@ -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() diff --git a/utils/google_service.py b/utils/google_service.py new file mode 100644 index 0000000..0859cfc --- /dev/null +++ b/utils/google_service.py @@ -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 "" diff --git a/uv.lock b/uv.lock index 0bece3f..6084f20 100644 --- a/uv.lock +++ b/uv.lock @@ -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"