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:
+6
-5
@@ -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)
|
||||
|
||||
@@ -12,6 +12,7 @@ wheels/
|
||||
# Environment files
|
||||
.env
|
||||
credentials.json
|
||||
token.json
|
||||
|
||||
# Database files
|
||||
database/
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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()
|
||||
@@ -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 ""
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user