mirror of
https://github.com/open-webui/open-webui.git
synced 2026-09-11 22:52:54 +00:00
feat: google drive folder sync
This commit is contained in:
parent
f122525310
commit
6e69da36f7
18 changed files with 2297 additions and 277 deletions
|
|
@ -2503,6 +2503,19 @@ GOOGLE_DRIVE_API_KEY = PersistentConfig(
|
|||
os.environ.get('GOOGLE_DRIVE_API_KEY', ''),
|
||||
)
|
||||
|
||||
GOOGLE_DRIVE_SERVICE_ACCOUNT_JSON = PersistentConfig(
|
||||
"GOOGLE_DRIVE_SERVICE_ACCOUNT_JSON",
|
||||
"google_drive.service_account_json",
|
||||
os.environ.get("GOOGLE_DRIVE_SERVICE_ACCOUNT_JSON", ""),
|
||||
)
|
||||
|
||||
# If configured, Google Drive folder sync will be available for knowledge bases.
|
||||
ENABLE_GOOGLE_DRIVE_FOLDER_SYNC = PersistentConfig(
|
||||
"ENABLE_GOOGLE_DRIVE_FOLDER_SYNC",
|
||||
"google_drive.enable_folder_sync",
|
||||
os.getenv("ENABLE_GOOGLE_DRIVE_FOLDER_SYNC", "False").lower() == "true",
|
||||
)
|
||||
|
||||
ENABLE_ONEDRIVE_INTEGRATION = PersistentConfig(
|
||||
'ENABLE_ONEDRIVE_INTEGRATION',
|
||||
'onedrive.enable',
|
||||
|
|
|
|||
|
|
@ -344,6 +344,8 @@ from open_webui.config import (
|
|||
GOOGLE_PSE_ENGINE_ID,
|
||||
GOOGLE_DRIVE_CLIENT_ID,
|
||||
GOOGLE_DRIVE_API_KEY,
|
||||
GOOGLE_DRIVE_SERVICE_ACCOUNT_JSON,
|
||||
ENABLE_GOOGLE_DRIVE_FOLDER_SYNC,
|
||||
ENABLE_ONEDRIVE_INTEGRATION,
|
||||
ONEDRIVE_CLIENT_ID_PERSONAL,
|
||||
ONEDRIVE_CLIENT_ID_BUSINESS,
|
||||
|
|
@ -472,6 +474,7 @@ from open_webui.env import (
|
|||
AUDIT_INCLUDED_PATHS,
|
||||
AUDIT_LOG_LEVEL,
|
||||
CHANGELOG,
|
||||
ENV,
|
||||
REDIS_URL,
|
||||
REDIS_CLUSTER,
|
||||
REDIS_KEY_PREFIX,
|
||||
|
|
@ -561,6 +564,8 @@ from open_webui.tasks import (
|
|||
list_tasks,
|
||||
) # Import from tasks.py
|
||||
|
||||
from open_webui.services.google_drive_scheduler import google_drive_scheduler
|
||||
|
||||
from open_webui.utils.redis import get_sentinels_from_env
|
||||
|
||||
|
||||
|
|
@ -702,11 +707,31 @@ async def lifespan(app: FastAPI):
|
|||
# Mark application as ready to accept traffic from a startup perspective.
|
||||
app.state.startup_complete = True
|
||||
|
||||
# Start Google Drive sync scheduler if enabled
|
||||
if app.state.config.ENABLE_GOOGLE_DRIVE_FOLDER_SYNC:
|
||||
try:
|
||||
await google_drive_scheduler.start()
|
||||
log.info("Google Drive sync scheduler started successfully")
|
||||
except Exception as e:
|
||||
log.error(f"Failed to start Google Drive sync scheduler: {e}")
|
||||
# Non-critical failure - continue application startup
|
||||
|
||||
yield
|
||||
|
||||
if hasattr(app.state, 'redis_task_command_listener'):
|
||||
app.state.redis_task_command_listener.cancel()
|
||||
|
||||
# Stop Google Drive sync scheduler
|
||||
if (
|
||||
hasattr(app.state, "config")
|
||||
and app.state.config.ENABLE_GOOGLE_DRIVE_FOLDER_SYNC
|
||||
):
|
||||
try:
|
||||
await google_drive_scheduler.stop()
|
||||
log.info("Google Drive sync scheduler stopped")
|
||||
except Exception as e:
|
||||
log.error(f"Error stopping Google Drive sync scheduler: {e}")
|
||||
|
||||
|
||||
app = FastAPI(
|
||||
title='Open WebUI',
|
||||
|
|
@ -1046,6 +1071,10 @@ app.state.config.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL = BYPASS_WEB_SEARCH_E
|
|||
app.state.config.BYPASS_WEB_SEARCH_WEB_LOADER = BYPASS_WEB_SEARCH_WEB_LOADER
|
||||
|
||||
app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION = ENABLE_GOOGLE_DRIVE_INTEGRATION
|
||||
app.state.config.GOOGLE_DRIVE_API_KEY = GOOGLE_DRIVE_API_KEY
|
||||
app.state.config.GOOGLE_DRIVE_CLIENT_ID = GOOGLE_DRIVE_CLIENT_ID
|
||||
app.state.config.GOOGLE_DRIVE_SERVICE_ACCOUNT_JSON = GOOGLE_DRIVE_SERVICE_ACCOUNT_JSON
|
||||
app.state.config.ENABLE_GOOGLE_DRIVE_FOLDER_SYNC = ENABLE_GOOGLE_DRIVE_FOLDER_SYNC
|
||||
app.state.config.ENABLE_ONEDRIVE_INTEGRATION = ENABLE_ONEDRIVE_INTEGRATION
|
||||
|
||||
app.state.config.OLLAMA_CLOUD_WEB_SEARCH_API_KEY = OLLAMA_CLOUD_WEB_SEARCH_API_KEY
|
||||
|
|
@ -2076,6 +2105,7 @@ async def get_app_config(request: Request):
|
|||
'enable_admin_chat_access': ENABLE_ADMIN_CHAT_ACCESS,
|
||||
'enable_admin_analytics': ENABLE_ADMIN_ANALYTICS,
|
||||
'enable_google_drive_integration': app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION,
|
||||
'enable_google_drive_folder_sync': app.state.config.ENABLE_GOOGLE_DRIVE_FOLDER_SYNC,
|
||||
'enable_onedrive_integration': app.state.config.ENABLE_ONEDRIVE_INTEGRATION,
|
||||
'enable_memories': app.state.config.ENABLE_MEMORIES,
|
||||
**(
|
||||
|
|
@ -2124,6 +2154,9 @@ async def get_app_config(request: Request):
|
|||
'client_id': GOOGLE_DRIVE_CLIENT_ID.value,
|
||||
'api_key': GOOGLE_DRIVE_API_KEY.value,
|
||||
},
|
||||
'google_drive_folder_sync': {
|
||||
'service_account_json': GOOGLE_DRIVE_SERVICE_ACCOUNT_JSON.value,
|
||||
},
|
||||
'onedrive': {
|
||||
'client_id_personal': ONEDRIVE_CLIENT_ID_PERSONAL,
|
||||
'client_id_business': ONEDRIVE_CLIENT_ID_BUSINESS,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ from fastapi import APIRouter, Depends, HTTPException, status, Request, Query
|
|||
from fastapi.responses import StreamingResponse
|
||||
from fastapi.concurrency import run_in_threadpool
|
||||
import logging
|
||||
import time
|
||||
import io
|
||||
import zipfile
|
||||
from urllib.parse import quote
|
||||
|
|
@ -19,6 +20,7 @@ from open_webui.models.knowledge import (
|
|||
KnowledgeUserResponse,
|
||||
)
|
||||
from open_webui.models.files import Files, FileModel, FileMetadataResponse
|
||||
from open_webui.models.users import UserModel
|
||||
from open_webui.retrieval.vector.factory import VECTOR_DB_CLIENT
|
||||
from open_webui.routers.retrieval import (
|
||||
process_file,
|
||||
|
|
@ -26,6 +28,7 @@ from open_webui.routers.retrieval import (
|
|||
process_files_batch,
|
||||
BatchProcessFilesForm,
|
||||
)
|
||||
from open_webui.services.google_drive import google_drive_service
|
||||
from open_webui.storage.provider import Storage
|
||||
|
||||
from open_webui.constants import ERROR_MESSAGES
|
||||
|
|
@ -1103,3 +1106,305 @@ async def export_knowledge_by_id(id: str, user=Depends(get_admin_user), db: Sess
|
|||
media_type='application/zip',
|
||||
headers={'Content-Disposition': content_disposition},
|
||||
)
|
||||
|
||||
|
||||
############################
|
||||
# Google Drive Sync
|
||||
############################
|
||||
|
||||
|
||||
class GoogleDriveSyncForm(BaseModel):
|
||||
folder_id: str
|
||||
include_nested: bool = True
|
||||
sync_interval_days: float = 1.0 # Float to support fractional days
|
||||
|
||||
|
||||
class GoogleDriveServiceAccountResponse(BaseModel):
|
||||
email: str
|
||||
|
||||
|
||||
@router.get(
|
||||
"/google-drive/service-account-email",
|
||||
response_model=GoogleDriveServiceAccountResponse,
|
||||
)
|
||||
async def get_google_drive_service_account_email(
|
||||
user: UserModel = Depends(get_verified_user),
|
||||
) -> GoogleDriveServiceAccountResponse:
|
||||
"""Get the Google Drive service account email for sharing folders."""
|
||||
if not google_drive_service.is_configured():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Google Drive service account not configured",
|
||||
)
|
||||
|
||||
email = google_drive_service.get_service_account_email()
|
||||
if not email:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Failed to get service account email",
|
||||
)
|
||||
|
||||
return {"email": email}
|
||||
|
||||
|
||||
@router.post("/{id}/google-drive/sync", response_model=Optional[KnowledgeFilesResponse])
|
||||
async def sync_google_drive_folder(
|
||||
id: str,
|
||||
form_data: GoogleDriveSyncForm,
|
||||
request: Request,
|
||||
user: UserModel = Depends(get_verified_user),
|
||||
) -> Optional[KnowledgeFilesResponse]:
|
||||
"""Sync a Google Drive folder with the knowledge base."""
|
||||
knowledge = Knowledges.get_knowledge_by_id(id=id)
|
||||
if not knowledge:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||
)
|
||||
|
||||
if (
|
||||
knowledge.user_id != user.id
|
||||
and not has_access(user.id, "write", knowledge.access_control)
|
||||
and user.role != "admin"
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||
)
|
||||
|
||||
if not google_drive_service.is_configured():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Google Drive service account not configured",
|
||||
)
|
||||
|
||||
try:
|
||||
# Get current files in knowledge base
|
||||
current_file_ids = knowledge.data.get("file_ids", []) if knowledge.data else []
|
||||
current_files = Files.get_files_by_ids(current_file_ids)
|
||||
|
||||
# Create a map of Google Drive file IDs to local file IDs
|
||||
gdrive_file_map = {}
|
||||
for file in current_files:
|
||||
if file.meta and file.meta.get("google_drive_id"):
|
||||
gdrive_file_map[file.meta["google_drive_id"]] = file.id
|
||||
|
||||
# Get files from Google Drive folder
|
||||
gdrive_files = google_drive_service.list_folder_files(
|
||||
form_data.folder_id, form_data.include_nested
|
||||
)
|
||||
|
||||
log.info(
|
||||
f"Google Drive sync: Found {len(gdrive_files)} files in folder {form_data.folder_id}"
|
||||
)
|
||||
for gdrive_file in gdrive_files:
|
||||
log.info(
|
||||
f"Google Drive file: {gdrive_file.get('name', 'Unknown')} (ID: {gdrive_file.get('id', 'Unknown')}, Type: {gdrive_file.get('mimeType', 'Unknown')})"
|
||||
)
|
||||
|
||||
# Track files to keep and files to add
|
||||
files_to_keep = set()
|
||||
files_to_add = []
|
||||
|
||||
for gdrive_file in gdrive_files:
|
||||
gdrive_id = gdrive_file["id"]
|
||||
|
||||
if gdrive_id in gdrive_file_map:
|
||||
# File exists, check if it needs updating
|
||||
local_file_id = gdrive_file_map[gdrive_id]
|
||||
local_file = Files.get_file_by_id(local_file_id)
|
||||
|
||||
if local_file and local_file.meta:
|
||||
local_modified = local_file.meta.get("google_drive_modified")
|
||||
gdrive_modified = gdrive_file["modifiedTime"]
|
||||
|
||||
if local_modified != gdrive_modified:
|
||||
# File was modified, re-download and update
|
||||
files_to_add.append(gdrive_file)
|
||||
# Remove old file
|
||||
Files.delete_file_by_id(local_file_id)
|
||||
else:
|
||||
# File is up to date, keep it
|
||||
files_to_keep.add(local_file_id)
|
||||
else:
|
||||
# Local file metadata is missing, re-download
|
||||
files_to_add.append(gdrive_file)
|
||||
else:
|
||||
# New file, add it
|
||||
files_to_add.append(gdrive_file)
|
||||
|
||||
# Remove files that are no longer in Google Drive
|
||||
files_to_remove = set(current_file_ids) - files_to_keep
|
||||
for file_id in files_to_remove:
|
||||
Files.delete_file_by_id(file_id)
|
||||
|
||||
# Download and add new/updated files
|
||||
new_file_ids = list(files_to_keep)
|
||||
|
||||
log.info(
|
||||
f"Google Drive sync: Processing {len(files_to_add)} files to add/update"
|
||||
)
|
||||
|
||||
for gdrive_file in files_to_add:
|
||||
try:
|
||||
log.info(
|
||||
f"Google Drive sync: Processing file '{gdrive_file.get('name', 'Unknown')}' (ID: {gdrive_file.get('id', 'Unknown')})"
|
||||
)
|
||||
|
||||
# Download file from Google Drive
|
||||
file_content, filename = google_drive_service.download_file(
|
||||
gdrive_file["id"], gdrive_file
|
||||
)
|
||||
|
||||
log.info(
|
||||
f"Google Drive sync: Downloaded file '{filename}', size: {len(file_content)} bytes"
|
||||
)
|
||||
|
||||
# Debug: Check if content is actually text
|
||||
try:
|
||||
content_preview = (
|
||||
file_content.decode("utf-8")[:200]
|
||||
if isinstance(file_content, bytes)
|
||||
else str(file_content)[:200]
|
||||
)
|
||||
log.info(
|
||||
f"Google Drive sync: File content preview: {content_preview}..."
|
||||
)
|
||||
except Exception as e:
|
||||
log.error(f"Google Drive sync: Could not decode file content: {e}")
|
||||
|
||||
# Create file object
|
||||
file_obj = io.BytesIO(file_content)
|
||||
file_obj.name = filename
|
||||
|
||||
# Upload to storage
|
||||
import uuid
|
||||
|
||||
file_id = str(uuid.uuid4())
|
||||
tags = {
|
||||
"OpenWebUI-User-Email": user.email,
|
||||
"OpenWebUI-User-Id": user.id,
|
||||
"OpenWebUI-User-Name": user.name,
|
||||
"OpenWebUI-File-Id": file_id,
|
||||
}
|
||||
|
||||
contents, file_path = Storage.upload_file(
|
||||
file_obj, f"{file_id}_{filename}", tags
|
||||
)
|
||||
log.info(
|
||||
f"Google Drive sync: Uploaded to storage, contents size: {len(contents)} bytes"
|
||||
)
|
||||
|
||||
# Create file record with content in data field
|
||||
from open_webui.models.files import FileForm
|
||||
|
||||
# Convert bytes to string for text content
|
||||
text_content = (
|
||||
file_content.decode("utf-8")
|
||||
if isinstance(file_content, bytes)
|
||||
else str(file_content)
|
||||
)
|
||||
|
||||
file_item = Files.insert_new_file(
|
||||
user.id,
|
||||
FileForm(
|
||||
id=file_id,
|
||||
filename=filename,
|
||||
path=file_path,
|
||||
data={
|
||||
"content": text_content, # Store the actual text content
|
||||
},
|
||||
meta={
|
||||
"name": filename,
|
||||
"content_type": "text/plain", # Changed from application/octet-stream
|
||||
"size": len(contents),
|
||||
"google_drive_id": gdrive_file["id"],
|
||||
"google_drive_modified": gdrive_file["modifiedTime"],
|
||||
"google_drive_path": gdrive_file["path"],
|
||||
"collection_name": id,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
if file_item:
|
||||
new_file_ids.append(file_id)
|
||||
log.info(
|
||||
f"Google Drive sync: Created file record for '{filename}' (File ID: {file_id})"
|
||||
)
|
||||
|
||||
# Process file for vector storage
|
||||
try:
|
||||
from open_webui.routers.retrieval import process_file
|
||||
|
||||
process_file(
|
||||
request,
|
||||
ProcessFileForm(
|
||||
file_id=file_id,
|
||||
collection_name=id,
|
||||
),
|
||||
user,
|
||||
)
|
||||
log.info(
|
||||
f"Google Drive sync: Successfully processed file '{filename}' for vector storage"
|
||||
)
|
||||
except Exception as e:
|
||||
log.error(
|
||||
f"Failed to process file {file_id} ('{filename}') for vector storage: {e}"
|
||||
)
|
||||
else:
|
||||
log.error(
|
||||
f"Google Drive sync: Failed to create file record for '{filename}'"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
log.error(f"Failed to sync file {gdrive_file['name']}: {e}")
|
||||
continue
|
||||
|
||||
# Update knowledge base data
|
||||
sync_data = {
|
||||
"google_drive_folder_id": form_data.folder_id,
|
||||
"google_drive_include_nested": form_data.include_nested,
|
||||
"google_drive_sync_interval_days": form_data.sync_interval_days,
|
||||
"google_drive_last_sync": int(time.time()),
|
||||
}
|
||||
|
||||
updated_data = knowledge.data.copy() if knowledge.data else {}
|
||||
updated_data.update(sync_data)
|
||||
updated_data["file_ids"] = new_file_ids
|
||||
|
||||
knowledge = Knowledges.update_knowledge_data_by_id(id=id, data=updated_data)
|
||||
|
||||
log.info(
|
||||
f"Google Drive sync completed: {len(new_file_ids)} total files in knowledge base"
|
||||
)
|
||||
log.info(f"Google Drive sync: Knowledge object type: {type(knowledge)}")
|
||||
log.info(f"Google Drive sync: Knowledge object: {knowledge}")
|
||||
|
||||
if knowledge:
|
||||
files = Files.get_file_metadatas_by_ids(new_file_ids)
|
||||
log.info(f"Google Drive sync: Returning {len(files)} file metadata records")
|
||||
|
||||
# Handle both dict and model cases
|
||||
if hasattr(knowledge, "model_dump"):
|
||||
knowledge_dict = knowledge.model_dump()
|
||||
else:
|
||||
knowledge_dict = (
|
||||
knowledge if isinstance(knowledge, dict) else knowledge.__dict__
|
||||
)
|
||||
|
||||
return KnowledgeFilesResponse(
|
||||
**knowledge_dict,
|
||||
files=files,
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Failed to update knowledge base",
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
log.error(f"Google Drive sync error: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Failed to sync Google Drive folder: {str(e)}",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -97,6 +97,7 @@ from open_webui.utils.misc import (
|
|||
)
|
||||
from open_webui.utils.auth import get_admin_user, get_verified_user
|
||||
from open_webui.utils.access_control import has_permission
|
||||
from open_webui.services.google_drive import google_drive_service
|
||||
|
||||
from open_webui.config import (
|
||||
ENV,
|
||||
|
|
@ -503,6 +504,10 @@ async def get_rag_config(request: Request, user=Depends(get_admin_user)):
|
|||
'ALLOWED_FILE_EXTENSIONS': request.app.state.config.ALLOWED_FILE_EXTENSIONS,
|
||||
# Integration settings
|
||||
'ENABLE_GOOGLE_DRIVE_INTEGRATION': request.app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION,
|
||||
'GOOGLE_DRIVE_API_KEY': request.app.state.config.GOOGLE_DRIVE_API_KEY,
|
||||
'GOOGLE_DRIVE_CLIENT_ID': request.app.state.config.GOOGLE_DRIVE_CLIENT_ID,
|
||||
'GOOGLE_DRIVE_SERVICE_ACCOUNT_JSON': request.app.state.config.GOOGLE_DRIVE_SERVICE_ACCOUNT_JSON,
|
||||
'ENABLE_GOOGLE_DRIVE_FOLDER_SYNC': request.app.state.config.ENABLE_GOOGLE_DRIVE_FOLDER_SYNC,
|
||||
'ENABLE_ONEDRIVE_INTEGRATION': request.app.state.config.ENABLE_ONEDRIVE_INTEGRATION,
|
||||
# Web search settings
|
||||
'web': {
|
||||
|
|
@ -713,6 +718,10 @@ class ConfigForm(BaseModel):
|
|||
|
||||
# Integration settings
|
||||
ENABLE_GOOGLE_DRIVE_INTEGRATION: Optional[bool] = None
|
||||
GOOGLE_DRIVE_API_KEY: Optional[str] = None
|
||||
GOOGLE_DRIVE_CLIENT_ID: Optional[str] = None
|
||||
GOOGLE_DRIVE_SERVICE_ACCOUNT_JSON: Optional[str] = None
|
||||
ENABLE_GOOGLE_DRIVE_FOLDER_SYNC: Optional[bool] = None
|
||||
ENABLE_ONEDRIVE_INTEGRATION: Optional[bool] = None
|
||||
|
||||
# Web search settings
|
||||
|
|
@ -1026,12 +1035,36 @@ async def update_rag_config(request: Request, form_data: ConfigForm, user=Depend
|
|||
if form_data.ENABLE_GOOGLE_DRIVE_INTEGRATION is not None
|
||||
else request.app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION
|
||||
)
|
||||
request.app.state.config.GOOGLE_DRIVE_API_KEY = (
|
||||
form_data.GOOGLE_DRIVE_API_KEY
|
||||
if form_data.GOOGLE_DRIVE_API_KEY is not None
|
||||
else request.app.state.config.GOOGLE_DRIVE_API_KEY
|
||||
)
|
||||
request.app.state.config.GOOGLE_DRIVE_CLIENT_ID = (
|
||||
form_data.GOOGLE_DRIVE_CLIENT_ID
|
||||
if form_data.GOOGLE_DRIVE_CLIENT_ID is not None
|
||||
else request.app.state.config.GOOGLE_DRIVE_CLIENT_ID
|
||||
)
|
||||
request.app.state.config.GOOGLE_DRIVE_SERVICE_ACCOUNT_JSON = (
|
||||
form_data.GOOGLE_DRIVE_SERVICE_ACCOUNT_JSON
|
||||
if form_data.GOOGLE_DRIVE_SERVICE_ACCOUNT_JSON is not None
|
||||
else request.app.state.config.GOOGLE_DRIVE_SERVICE_ACCOUNT_JSON
|
||||
)
|
||||
request.app.state.config.ENABLE_GOOGLE_DRIVE_FOLDER_SYNC = (
|
||||
form_data.ENABLE_GOOGLE_DRIVE_FOLDER_SYNC
|
||||
if form_data.ENABLE_GOOGLE_DRIVE_FOLDER_SYNC is not None
|
||||
else request.app.state.config.ENABLE_GOOGLE_DRIVE_FOLDER_SYNC
|
||||
)
|
||||
request.app.state.config.ENABLE_ONEDRIVE_INTEGRATION = (
|
||||
form_data.ENABLE_ONEDRIVE_INTEGRATION
|
||||
if form_data.ENABLE_ONEDRIVE_INTEGRATION is not None
|
||||
else request.app.state.config.ENABLE_ONEDRIVE_INTEGRATION
|
||||
)
|
||||
|
||||
# Refresh Google Drive service if service account JSON was updated
|
||||
if form_data.GOOGLE_DRIVE_SERVICE_ACCOUNT_JSON is not None:
|
||||
google_drive_service.refresh_configuration()
|
||||
|
||||
if form_data.web is not None:
|
||||
# Web search settings
|
||||
request.app.state.config.ENABLE_WEB_SEARCH = form_data.web.ENABLE_WEB_SEARCH
|
||||
|
|
@ -1166,6 +1199,10 @@ async def update_rag_config(request: Request, form_data: ConfigForm, user=Depend
|
|||
'ALLOWED_FILE_EXTENSIONS': request.app.state.config.ALLOWED_FILE_EXTENSIONS,
|
||||
# Integration settings
|
||||
'ENABLE_GOOGLE_DRIVE_INTEGRATION': request.app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION,
|
||||
'GOOGLE_DRIVE_API_KEY': request.app.state.config.GOOGLE_DRIVE_API_KEY,
|
||||
'GOOGLE_DRIVE_CLIENT_ID': request.app.state.config.GOOGLE_DRIVE_CLIENT_ID,
|
||||
'GOOGLE_DRIVE_SERVICE_ACCOUNT_JSON': request.app.state.config.GOOGLE_DRIVE_SERVICE_ACCOUNT_JSON,
|
||||
'ENABLE_GOOGLE_DRIVE_FOLDER_SYNC': request.app.state.config.ENABLE_GOOGLE_DRIVE_FOLDER_SYNC,
|
||||
'ENABLE_ONEDRIVE_INTEGRATION': request.app.state.config.ENABLE_ONEDRIVE_INTEGRATION,
|
||||
# Web search settings
|
||||
'web': {
|
||||
|
|
|
|||
1
backend/open_webui/services/__init__.py
Normal file
1
backend/open_webui/services/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
# Services module for Open WebUI
|
||||
523
backend/open_webui/services/google_drive.py
Normal file
523
backend/open_webui/services/google_drive.py
Normal file
|
|
@ -0,0 +1,523 @@
|
|||
import json
|
||||
import logging
|
||||
from typing import List, Dict, Optional, Tuple, TypedDict, NotRequired
|
||||
from datetime import datetime
|
||||
import io
|
||||
|
||||
from google.oauth2 import service_account # type: ignore
|
||||
from googleapiclient.discovery import build, Resource # type: ignore
|
||||
from googleapiclient.errors import HttpError # type: ignore
|
||||
from googleapiclient.http import MediaIoBaseDownload # type: ignore
|
||||
|
||||
from open_webui.config import GOOGLE_DRIVE_SERVICE_ACCOUNT_JSON
|
||||
|
||||
|
||||
# Type definitions for Google Drive API responses
|
||||
class GoogleDriveFile(TypedDict):
|
||||
"""Type definition for Google Drive file information."""
|
||||
|
||||
id: str
|
||||
name: str
|
||||
mimeType: str
|
||||
modifiedTime: str
|
||||
size: NotRequired[str] # Optional field, not present for some files
|
||||
parents: NotRequired[List[str]] # Optional, may not be present
|
||||
path: str # Custom field added by our service
|
||||
|
||||
|
||||
class GoogleDriveFolder(TypedDict):
|
||||
"""Type definition for Google Drive folder information."""
|
||||
|
||||
id: str
|
||||
name: str
|
||||
|
||||
|
||||
class GoogleDriveAPIResponse(TypedDict):
|
||||
"""Type definition for Google Drive API list response."""
|
||||
|
||||
files: List[Dict[str, str]] # Raw API response uses generic dict
|
||||
nextPageToken: NotRequired[str]
|
||||
|
||||
|
||||
class FileDownloadResult(TypedDict):
|
||||
"""Type definition for file download result."""
|
||||
|
||||
content: bytes
|
||||
filename: str
|
||||
|
||||
|
||||
class GoogleDriveSyncConfig(TypedDict):
|
||||
"""Type definition for Google Drive sync configuration stored in knowledge base data."""
|
||||
|
||||
google_drive_folder_id: str
|
||||
google_drive_include_nested: bool
|
||||
google_drive_sync_interval_days: float # Float to support fractional days
|
||||
google_drive_last_sync: int # Unix timestamp
|
||||
file_ids: List[str] # List of file IDs in the knowledge base
|
||||
|
||||
|
||||
class GoogleDriveFileMetadata(TypedDict):
|
||||
"""Type definition for Google Drive file metadata stored in file records."""
|
||||
|
||||
name: str
|
||||
content_type: str
|
||||
size: int
|
||||
google_drive_id: str
|
||||
google_drive_modified: str # ISO timestamp from Google Drive API
|
||||
google_drive_path: str
|
||||
collection_name: str
|
||||
# Additional standard file metadata fields that may be present
|
||||
source: NotRequired[str] # Usually "google_drive"
|
||||
original_filename: NotRequired[str] # May differ from processed filename
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GoogleDriveService:
|
||||
"""Service for interacting with Google Drive using service account authentication."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.service: Optional[Resource] = None
|
||||
self._initialize_service()
|
||||
|
||||
def _initialize_service(self) -> None:
|
||||
"""Initialize Google Drive service with service account credentials."""
|
||||
try:
|
||||
if not GOOGLE_DRIVE_SERVICE_ACCOUNT_JSON.value:
|
||||
log.warning("Google Drive service account JSON not configured")
|
||||
return
|
||||
|
||||
# Parse service account JSON
|
||||
service_account_info = json.loads(GOOGLE_DRIVE_SERVICE_ACCOUNT_JSON.value)
|
||||
|
||||
# Create credentials
|
||||
credentials = service_account.Credentials.from_service_account_info(
|
||||
service_account_info,
|
||||
scopes=["https://www.googleapis.com/auth/drive.readonly"],
|
||||
)
|
||||
|
||||
# Build service
|
||||
self.service = build("drive", "v3", credentials=credentials)
|
||||
log.info("Google Drive service initialized successfully")
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
log.error(f"Invalid service account JSON: {e}")
|
||||
except Exception as e:
|
||||
log.error(f"Failed to initialize Google Drive service: {e}")
|
||||
|
||||
def is_configured(self) -> bool:
|
||||
"""Check if Google Drive service is properly configured."""
|
||||
return self.service is not None
|
||||
|
||||
def refresh_configuration(self) -> None:
|
||||
"""Refresh the service configuration. Call this when config is updated."""
|
||||
self._initialize_service()
|
||||
|
||||
def get_service_account_email(self) -> Optional[str]:
|
||||
"""Get the service account email address."""
|
||||
try:
|
||||
if not GOOGLE_DRIVE_SERVICE_ACCOUNT_JSON.value:
|
||||
return None
|
||||
|
||||
service_account_info = json.loads(GOOGLE_DRIVE_SERVICE_ACCOUNT_JSON.value)
|
||||
return service_account_info.get("client_email")
|
||||
except Exception as e:
|
||||
log.error(f"Failed to get service account email: {e}")
|
||||
return None
|
||||
|
||||
def list_folder_files(
|
||||
self, folder_id: str, include_nested: bool = True
|
||||
) -> List[GoogleDriveFile]:
|
||||
"""
|
||||
List all files in a Google Drive folder.
|
||||
|
||||
Args:
|
||||
folder_id: Google Drive folder ID
|
||||
include_nested: Whether to include files from nested folders
|
||||
|
||||
Returns:
|
||||
List of file information dictionaries
|
||||
"""
|
||||
if not self.service:
|
||||
raise Exception("Google Drive service not configured")
|
||||
|
||||
try:
|
||||
files = []
|
||||
|
||||
if include_nested:
|
||||
# Get all files recursively
|
||||
files = self._get_files_recursive(folder_id)
|
||||
else:
|
||||
# Get files only from the specified folder
|
||||
files = self._get_files_in_folder(folder_id)
|
||||
|
||||
# Filter supported file types
|
||||
log.info(f"Google Drive: Found {len(files)} total files before filtering")
|
||||
for file_info in files:
|
||||
log.info(
|
||||
f"Google Drive file found: '{file_info.get('name', 'Unknown')}' (Type: {file_info.get('mimeType', 'Unknown')})"
|
||||
)
|
||||
|
||||
supported_files = []
|
||||
for file_info in files:
|
||||
if self._is_supported_file(file_info):
|
||||
supported_files.append(file_info)
|
||||
log.info(
|
||||
f"Google Drive: File '{file_info.get('name', 'Unknown')}' is SUPPORTED"
|
||||
)
|
||||
else:
|
||||
log.info(
|
||||
f"Google Drive: File '{file_info.get('name', 'Unknown')}' is NOT SUPPORTED (Type: {file_info.get('mimeType', 'Unknown')})"
|
||||
)
|
||||
|
||||
log.info(f"Google Drive: {len(supported_files)} files passed filtering")
|
||||
return supported_files
|
||||
|
||||
except HttpError as e:
|
||||
log.error(f"HTTP error listing folder files: {e}")
|
||||
raise Exception(f"Failed to access Google Drive folder: {e}")
|
||||
except Exception as e:
|
||||
log.error(f"Error listing folder files: {e}")
|
||||
raise
|
||||
|
||||
def _get_files_recursive(
|
||||
self, folder_id: str, path: str = ""
|
||||
) -> List[GoogleDriveFile]:
|
||||
"""Recursively get all files from a folder and its subfolders."""
|
||||
files = []
|
||||
|
||||
# Get files in current folder
|
||||
files.extend(self._get_files_in_folder(folder_id, path))
|
||||
|
||||
# Get subfolders and process them recursively
|
||||
subfolders = self._get_subfolders(folder_id)
|
||||
for subfolder in subfolders:
|
||||
subfolder_path = (
|
||||
f"{path}/{subfolder['name']}" if path else subfolder["name"]
|
||||
)
|
||||
files.extend(self._get_files_recursive(subfolder["id"], subfolder_path))
|
||||
|
||||
return files
|
||||
|
||||
def _get_files_in_folder(
|
||||
self, folder_id: str, path: str = ""
|
||||
) -> List[GoogleDriveFile]:
|
||||
"""Get files directly in a specific folder."""
|
||||
files = []
|
||||
page_token = None
|
||||
|
||||
log.info(f"Google Drive: Querying folder {folder_id} for files")
|
||||
|
||||
# Debug: Test if we can access any files at all
|
||||
try:
|
||||
assert self.service is not None # Type guard for mypy
|
||||
test_response = (
|
||||
self.service.files()
|
||||
.list(
|
||||
q="", spaces="drive", pageSize=5, fields="files(id, name, mimeType)"
|
||||
)
|
||||
.execute()
|
||||
)
|
||||
log.info(
|
||||
f"Google Drive: Test query found {len(test_response.get('files', []))} total accessible files"
|
||||
)
|
||||
except Exception as e:
|
||||
log.error(f"Google Drive: Test query failed: {e}")
|
||||
|
||||
while True:
|
||||
# Use official Google Drive API query syntax
|
||||
query = f"'{folder_id}' in parents"
|
||||
log.info(f"Google Drive API query: {query}")
|
||||
|
||||
try:
|
||||
# Follow official Google Drive API example pattern with Shared Drive support
|
||||
assert self.service is not None # Type guard for mypy
|
||||
response = (
|
||||
self.service.files()
|
||||
.list(
|
||||
q=query,
|
||||
spaces="drive",
|
||||
corpora="allDrives",
|
||||
includeItemsFromAllDrives=True,
|
||||
supportsAllDrives=True,
|
||||
pageSize=100,
|
||||
pageToken=page_token,
|
||||
fields="nextPageToken, files(id, name, mimeType, modifiedTime, size, parents)",
|
||||
)
|
||||
.execute()
|
||||
)
|
||||
|
||||
log.info(
|
||||
f"Google Drive API response: {len(response.get('files', []))} items returned"
|
||||
)
|
||||
|
||||
# Log all items found (including folders)
|
||||
all_items = response.get("files", [])
|
||||
for item in all_items:
|
||||
log.info(
|
||||
f"Google Drive item: '{item.get('name', 'Unknown')}' (Type: {item.get('mimeType', 'Unknown')})"
|
||||
)
|
||||
|
||||
results = response # Keep compatibility with existing code
|
||||
|
||||
except Exception as e:
|
||||
log.error(f"Google Drive API error: {e}")
|
||||
return []
|
||||
|
||||
items = results.get("files", [])
|
||||
|
||||
# Filter out folders and only keep files
|
||||
for item in items:
|
||||
if item.get("mimeType") != "application/vnd.google-apps.folder":
|
||||
file_info: GoogleDriveFile = {
|
||||
"id": item["id"],
|
||||
"name": item["name"],
|
||||
"mimeType": item["mimeType"],
|
||||
"modifiedTime": item["modifiedTime"],
|
||||
"path": f"{path}/{item['name']}" if path else item["name"],
|
||||
}
|
||||
# Add optional fields if present
|
||||
if "size" in item:
|
||||
file_info["size"] = item["size"]
|
||||
if "parents" in item:
|
||||
file_info["parents"] = item["parents"]
|
||||
files.append(file_info)
|
||||
log.info(f"Google Drive: Added file '{item['name']}' to results")
|
||||
|
||||
page_token = results.get("nextPageToken")
|
||||
if not page_token:
|
||||
break
|
||||
|
||||
return files
|
||||
|
||||
def _get_subfolders(self, folder_id: str) -> List[Dict[str, str]]:
|
||||
"""Get subfolders in a specific folder."""
|
||||
subfolders = []
|
||||
page_token = None
|
||||
|
||||
while True:
|
||||
query = f"'{folder_id}' in parents and mimeType='application/vnd.google-apps.folder' and trashed=false"
|
||||
|
||||
assert self.service is not None # Type guard for mypy
|
||||
results = (
|
||||
self.service.files()
|
||||
.list(
|
||||
q=query,
|
||||
spaces="drive",
|
||||
corpora="allDrives",
|
||||
includeItemsFromAllDrives=True,
|
||||
supportsAllDrives=True,
|
||||
pageSize=100,
|
||||
pageToken=page_token,
|
||||
fields="nextPageToken, files(id, name)",
|
||||
)
|
||||
.execute()
|
||||
)
|
||||
|
||||
items = results.get("files", [])
|
||||
subfolders.extend(items)
|
||||
|
||||
page_token = results.get("nextPageToken")
|
||||
if not page_token:
|
||||
break
|
||||
|
||||
return subfolders
|
||||
|
||||
def _is_supported_file(self, file_info: GoogleDriveFile) -> bool:
|
||||
"""Check if a file type is supported by the knowledge base."""
|
||||
mime_type = file_info.get("mimeType", "")
|
||||
file_name = file_info.get("name", "")
|
||||
|
||||
# Supported document file extensions for knowledge base
|
||||
# Focus on actual document types rather than code files
|
||||
supported_extensions = [
|
||||
# Text and markdown documents
|
||||
"txt",
|
||||
"md",
|
||||
"markdown",
|
||||
"rst",
|
||||
|
||||
# Microsoft Office documents
|
||||
"doc",
|
||||
"docx",
|
||||
"xls",
|
||||
"xlsx",
|
||||
"ppt",
|
||||
"pptx",
|
||||
"msg",
|
||||
|
||||
# OpenDocument formats
|
||||
"odt",
|
||||
"ods",
|
||||
"odp",
|
||||
|
||||
# PDF documents
|
||||
"pdf",
|
||||
|
||||
# Data files
|
||||
"csv",
|
||||
"tsv",
|
||||
"json",
|
||||
"xml",
|
||||
|
||||
# Web documents
|
||||
"html",
|
||||
"htm",
|
||||
|
||||
# Rich text
|
||||
"rtf",
|
||||
|
||||
# eBooks
|
||||
"epub",
|
||||
"mobi",
|
||||
|
||||
# Other document formats
|
||||
"pages", # Apple Pages
|
||||
"numbers", # Apple Numbers
|
||||
"key", # Apple Keynote
|
||||
]
|
||||
|
||||
# Check file extension
|
||||
if "." in file_name:
|
||||
extension = file_name.split(".")[-1].lower()
|
||||
if extension in supported_extensions:
|
||||
return True
|
||||
|
||||
# Check Google Workspace files
|
||||
google_workspace_types = [
|
||||
"application/vnd.google-apps.document",
|
||||
"application/vnd.google-apps.spreadsheet",
|
||||
"application/vnd.google-apps.presentation",
|
||||
]
|
||||
|
||||
if mime_type in google_workspace_types:
|
||||
return True
|
||||
|
||||
# Check other supported MIME types for documents
|
||||
supported_mime_types = [
|
||||
# Text documents
|
||||
"text/plain",
|
||||
"text/markdown",
|
||||
"text/x-markdown",
|
||||
"text/csv",
|
||||
"text/tab-separated-values",
|
||||
"text/xml",
|
||||
"text/html",
|
||||
"text/rtf",
|
||||
|
||||
# Microsoft Office documents
|
||||
"application/msword", # .doc
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document", # .docx
|
||||
"application/vnd.ms-excel", # .xls
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", # .xlsx
|
||||
"application/vnd.ms-powerpoint", # .ppt
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.presentation", # .pptx
|
||||
"application/vnd.ms-outlook", # .msg
|
||||
|
||||
# OpenDocument formats
|
||||
"application/vnd.oasis.opendocument.text", # .odt
|
||||
"application/vnd.oasis.opendocument.spreadsheet", # .ods
|
||||
"application/vnd.oasis.opendocument.presentation", # .odp
|
||||
|
||||
# PDF
|
||||
"application/pdf",
|
||||
|
||||
# eBooks
|
||||
"application/epub+zip",
|
||||
"application/x-mobipocket-ebook",
|
||||
|
||||
# Data formats
|
||||
"application/json",
|
||||
|
||||
# Apple iWork formats (when exported)
|
||||
"application/x-iwork-pages-sffpages", # .pages
|
||||
"application/x-iwork-numbers-sffnumbers", # .numbers
|
||||
"application/x-iwork-keynote-sffkey", # .key
|
||||
]
|
||||
|
||||
return mime_type in supported_mime_types
|
||||
|
||||
def download_file(
|
||||
self, file_id: str, file_info: GoogleDriveFile
|
||||
) -> Tuple[bytes, str]:
|
||||
"""
|
||||
Download a file from Google Drive.
|
||||
|
||||
Args:
|
||||
file_id: Google Drive file ID
|
||||
file_info: File information dictionary
|
||||
|
||||
Returns:
|
||||
Tuple of (file_content_bytes, filename)
|
||||
"""
|
||||
if not self.service:
|
||||
raise Exception("Google Drive service not configured")
|
||||
|
||||
try:
|
||||
mime_type = file_info.get("mimeType", "")
|
||||
file_name = file_info.get("name", "")
|
||||
|
||||
# Handle Google Workspace files (export)
|
||||
if mime_type.startswith("application/vnd.google-apps"):
|
||||
return self._export_google_workspace_file(file_id, mime_type, file_name)
|
||||
else:
|
||||
# Handle regular files (download)
|
||||
return self._download_regular_file(file_id, file_name)
|
||||
|
||||
except HttpError as e:
|
||||
log.error(f"HTTP error downloading file {file_id}: {e}")
|
||||
raise Exception(f"Failed to download file: {e}")
|
||||
except Exception as e:
|
||||
log.error(f"Error downloading file {file_id}: {e}")
|
||||
raise
|
||||
|
||||
def _export_google_workspace_file(
|
||||
self, file_id: str, mime_type: str, file_name: str
|
||||
) -> Tuple[bytes, str]:
|
||||
"""Export Google Workspace files to supported formats."""
|
||||
export_mime_type = None
|
||||
export_extension = None
|
||||
|
||||
if "document" in mime_type:
|
||||
export_mime_type = "text/plain"
|
||||
export_extension = ".txt"
|
||||
elif "spreadsheet" in mime_type:
|
||||
export_mime_type = "text/csv"
|
||||
export_extension = ".csv"
|
||||
elif "presentation" in mime_type:
|
||||
export_mime_type = "text/plain"
|
||||
export_extension = ".txt"
|
||||
else:
|
||||
export_mime_type = "application/pdf"
|
||||
export_extension = ".pdf"
|
||||
|
||||
assert self.service is not None # Type guard for mypy
|
||||
request = self.service.files().export_media(
|
||||
fileId=file_id, mimeType=export_mime_type
|
||||
)
|
||||
file_content = request.execute()
|
||||
|
||||
# Update filename with appropriate extension
|
||||
if not file_name.endswith(export_extension):
|
||||
file_name = f"{file_name}{export_extension}"
|
||||
|
||||
return file_content, file_name
|
||||
|
||||
def _download_regular_file(self, file_id: str, file_name: str) -> Tuple[bytes, str]:
|
||||
"""Download regular files."""
|
||||
assert self.service is not None # Type guard for mypy
|
||||
request = self.service.files().get_media(fileId=file_id)
|
||||
file_io = io.BytesIO()
|
||||
downloader = MediaIoBaseDownload(file_io, request)
|
||||
|
||||
done = False
|
||||
while done is False:
|
||||
_, done = downloader.next_chunk()
|
||||
|
||||
file_content = file_io.getvalue()
|
||||
return file_content, file_name
|
||||
|
||||
|
||||
# Global instance
|
||||
google_drive_service = GoogleDriveService()
|
||||
238
backend/open_webui/services/google_drive_scheduler.py
Normal file
238
backend/open_webui/services/google_drive_scheduler.py
Normal file
|
|
@ -0,0 +1,238 @@
|
|||
import asyncio
|
||||
import logging
|
||||
import time
|
||||
from typing import Dict, List, Any, Optional
|
||||
from datetime import datetime, timedelta
|
||||
import uuid
|
||||
import io
|
||||
|
||||
from open_webui.env import ENV
|
||||
from open_webui.models.knowledge import Knowledges, KnowledgeModel
|
||||
from open_webui.services.google_drive import google_drive_service, GoogleDriveFile
|
||||
from open_webui.models.files import Files, FileModel, FileForm
|
||||
from open_webui.routers.retrieval import process_file, ProcessFileForm
|
||||
from open_webui.storage.provider import Storage
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GoogleDriveSyncScheduler:
|
||||
"""Background scheduler for automatic Google Drive folder sync."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.running: bool = False
|
||||
self.sync_tasks: Dict[str, Any] = {}
|
||||
# In dev environment, check every minute for faster testing
|
||||
# In production, check every hour
|
||||
self.check_interval: int = 60 if ENV == "dev" else 3600
|
||||
|
||||
async def start(self) -> None:
|
||||
"""Start the background sync scheduler."""
|
||||
if self.running:
|
||||
return
|
||||
|
||||
self.running = True
|
||||
log.info(f"Google Drive sync scheduler started (ENV: {ENV}, check_interval: {self.check_interval}s)")
|
||||
|
||||
# Start the background task
|
||||
asyncio.create_task(self._sync_loop())
|
||||
|
||||
async def stop(self) -> None:
|
||||
"""Stop the background sync scheduler."""
|
||||
self.running = False
|
||||
log.info("Google Drive sync scheduler stopped")
|
||||
|
||||
async def _sync_loop(self) -> None:
|
||||
"""Main sync loop that runs in the background."""
|
||||
while self.running:
|
||||
try:
|
||||
await self._check_and_sync_knowledge_bases()
|
||||
await asyncio.sleep(self.check_interval)
|
||||
except Exception as e:
|
||||
log.error(f"Error in sync loop: {e}")
|
||||
await asyncio.sleep(self.check_interval)
|
||||
|
||||
async def _check_and_sync_knowledge_bases(self) -> None:
|
||||
"""Check all knowledge bases for Google Drive sync requirements."""
|
||||
if not google_drive_service.is_configured():
|
||||
return
|
||||
|
||||
try:
|
||||
# Get all knowledge bases
|
||||
knowledge_bases = Knowledges.get_knowledge_bases()
|
||||
|
||||
for kb in knowledge_bases:
|
||||
if not kb.data:
|
||||
continue
|
||||
|
||||
# Check if this knowledge base has Google Drive sync configured
|
||||
folder_id = kb.data.get("google_drive_folder_id")
|
||||
sync_interval_days = kb.data.get("google_drive_sync_interval_days", 1)
|
||||
last_sync = kb.data.get("google_drive_last_sync", 0)
|
||||
|
||||
if not folder_id:
|
||||
continue
|
||||
|
||||
# Check if sync is due
|
||||
current_time = int(time.time())
|
||||
sync_interval_seconds = sync_interval_days * 24 * 3600
|
||||
|
||||
if current_time - last_sync >= sync_interval_seconds:
|
||||
log.info(
|
||||
f"Auto-syncing knowledge base {kb.id} with Google Drive folder {folder_id}"
|
||||
)
|
||||
await self._sync_knowledge_base(kb)
|
||||
|
||||
except Exception as e:
|
||||
log.error(f"Error checking knowledge bases for sync: {e}")
|
||||
|
||||
async def _sync_knowledge_base(self, knowledge_base: KnowledgeModel) -> None:
|
||||
"""Sync a specific knowledge base with its Google Drive folder."""
|
||||
try:
|
||||
if not knowledge_base.data:
|
||||
return
|
||||
|
||||
# Type guard for mypy
|
||||
assert knowledge_base.data is not None
|
||||
|
||||
folder_id = knowledge_base.data.get("google_drive_folder_id")
|
||||
include_nested = knowledge_base.data.get(
|
||||
"google_drive_include_nested", True
|
||||
)
|
||||
|
||||
if not folder_id:
|
||||
return
|
||||
|
||||
# Get current files in knowledge base
|
||||
current_file_ids = knowledge_base.data.get("file_ids", [])
|
||||
current_files = Files.get_files_by_ids(current_file_ids)
|
||||
|
||||
# Create a map of Google Drive file IDs to local file IDs
|
||||
gdrive_file_map = {}
|
||||
for file in current_files:
|
||||
if file.meta and file.meta.get("google_drive_id"):
|
||||
gdrive_file_map[file.meta["google_drive_id"]] = file.id
|
||||
|
||||
# Get files from Google Drive folder
|
||||
gdrive_files = google_drive_service.list_folder_files(
|
||||
folder_id, include_nested
|
||||
)
|
||||
|
||||
# Track files to keep and files to add
|
||||
files_to_keep = set()
|
||||
files_to_add = []
|
||||
|
||||
for gdrive_file in gdrive_files:
|
||||
gdrive_id = gdrive_file["id"]
|
||||
|
||||
if gdrive_id in gdrive_file_map:
|
||||
# File exists, check if it needs updating
|
||||
local_file_id = gdrive_file_map[gdrive_id]
|
||||
local_file = Files.get_file_by_id(local_file_id)
|
||||
|
||||
if local_file and local_file.meta:
|
||||
local_modified = local_file.meta.get("google_drive_modified")
|
||||
gdrive_modified = gdrive_file["modifiedTime"]
|
||||
|
||||
if local_modified != gdrive_modified:
|
||||
# File was modified, re-download and update
|
||||
files_to_add.append(gdrive_file)
|
||||
# Remove old file
|
||||
Files.delete_file_by_id(local_file_id)
|
||||
else:
|
||||
# File is up to date, keep it
|
||||
files_to_keep.add(local_file_id)
|
||||
else:
|
||||
# Local file metadata is missing, re-download
|
||||
files_to_add.append(gdrive_file)
|
||||
else:
|
||||
# New file, add it
|
||||
files_to_add.append(gdrive_file)
|
||||
|
||||
# Remove files that are no longer in Google Drive
|
||||
files_to_remove = set(current_file_ids) - files_to_keep
|
||||
for file_id in files_to_remove:
|
||||
Files.delete_file_by_id(file_id)
|
||||
|
||||
# Download and add new/updated files
|
||||
new_file_ids = list(files_to_keep)
|
||||
|
||||
for gdrive_file in files_to_add:
|
||||
try:
|
||||
# Download file from Google Drive
|
||||
file_content, filename = google_drive_service.download_file(
|
||||
gdrive_file["id"], gdrive_file
|
||||
)
|
||||
|
||||
# Create file object
|
||||
file_obj = io.BytesIO(file_content)
|
||||
file_obj.name = filename
|
||||
|
||||
# Upload to storage
|
||||
file_id = str(uuid.uuid4())
|
||||
tags = {
|
||||
"OpenWebUI-User-Email": "system",
|
||||
"OpenWebUI-User-Id": "system",
|
||||
"OpenWebUI-User-Name": "Google Drive Sync",
|
||||
"OpenWebUI-File-Id": file_id,
|
||||
}
|
||||
|
||||
contents, file_path = Storage.upload_file(
|
||||
file_obj, f"{file_id}_{filename}", tags
|
||||
)
|
||||
|
||||
# Create file record
|
||||
file_form = FileForm(
|
||||
id=file_id,
|
||||
filename=filename,
|
||||
path=file_path,
|
||||
meta={
|
||||
"name": filename,
|
||||
"content_type": "application/octet-stream",
|
||||
"size": len(contents),
|
||||
"google_drive_id": gdrive_file["id"],
|
||||
"google_drive_modified": gdrive_file["modifiedTime"],
|
||||
"google_drive_path": gdrive_file["path"],
|
||||
"collection_name": knowledge_base.id,
|
||||
},
|
||||
)
|
||||
file_item = Files.insert_new_file(knowledge_base.user_id, file_form)
|
||||
|
||||
if file_item:
|
||||
new_file_ids.append(file_id)
|
||||
|
||||
# Process file for vector storage
|
||||
try:
|
||||
process_file(
|
||||
ProcessFileForm(
|
||||
file_id=file_id,
|
||||
collection_name=knowledge_base.id,
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
log.error(f"Failed to process file {file_id}: {e}")
|
||||
|
||||
except Exception as e:
|
||||
log.error(f"Failed to sync file {gdrive_file['name']}: {e}")
|
||||
continue
|
||||
|
||||
# Update knowledge base data
|
||||
assert knowledge_base.data is not None # Type guard for mypy
|
||||
updated_data = knowledge_base.data.copy()
|
||||
updated_data["file_ids"] = new_file_ids
|
||||
updated_data["google_drive_last_sync"] = int(time.time())
|
||||
|
||||
Knowledges.update_knowledge_data_by_id(
|
||||
id=knowledge_base.id, data=updated_data
|
||||
)
|
||||
|
||||
log.info(
|
||||
f"Successfully synced knowledge base {knowledge_base.id} with {len(files_to_add)} new/updated files"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
log.error(f"Error syncing knowledge base {knowledge_base.id}: {e}")
|
||||
|
||||
|
||||
# Global scheduler instance
|
||||
google_drive_scheduler = GoogleDriveSyncScheduler()
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
||||
import type { GoogleDriveServiceAccount, GoogleDriveSyncResponse } from '$lib/types/google-drive';
|
||||
|
||||
export const createNewKnowledge = async (
|
||||
token: string,
|
||||
|
|
@ -491,6 +492,81 @@ export const deleteKnowledgeById = async (token: string, id: string) => {
|
|||
return res;
|
||||
};
|
||||
|
||||
export const getGoogleDriveServiceAccountEmail = async (
|
||||
token: string
|
||||
): Promise<GoogleDriveServiceAccount | null> => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/knowledge/google-drive/service-account-email`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.then((json) => {
|
||||
return json;
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err.detail;
|
||||
console.error(err);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const syncGoogleDriveFolder = async (
|
||||
token: string,
|
||||
knowledgeId: string,
|
||||
folderId: string,
|
||||
includeNested: boolean = true,
|
||||
syncIntervalDays: number = 1
|
||||
): Promise<GoogleDriveSyncResponse | null> => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/knowledge/${knowledgeId}/google-drive/sync`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
folder_id: folderId,
|
||||
include_nested: includeNested,
|
||||
sync_interval_days: syncIntervalDays
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.then((json) => {
|
||||
return json;
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err.detail;
|
||||
console.error(err);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const reindexKnowledgeFiles = async (token: string) => {
|
||||
let error = null;
|
||||
|
||||
|
|
|
|||
|
|
@ -194,6 +194,34 @@
|
|||
return;
|
||||
}
|
||||
|
||||
// Google Drive validation
|
||||
if (RAGConfig.ENABLE_GOOGLE_DRIVE_INTEGRATION && !RAGConfig.GOOGLE_DRIVE_API_KEY) {
|
||||
toast.error($i18n.t('Google Drive API Key is required when integration is enabled.'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Folder sync specific validation
|
||||
if (RAGConfig.ENABLE_GOOGLE_DRIVE_FOLDER_SYNC) {
|
||||
if (!RAGConfig.GOOGLE_DRIVE_CLIENT_ID) {
|
||||
toast.error($i18n.t('Google Drive Client ID is required when folder sync is enabled.'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!RAGConfig.GOOGLE_DRIVE_SERVICE_ACCOUNT_JSON) {
|
||||
toast.error($i18n.t('Service Account JSON is required when folder sync is enabled.'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate Service Account JSON format
|
||||
try {
|
||||
JSON.parse(RAGConfig.GOOGLE_DRIVE_SERVICE_ACCOUNT_JSON);
|
||||
} catch (e) {
|
||||
toast.error($i18n.t('Invalid Service Account JSON format.'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (!RAGConfig.BYPASS_EMBEDDING_AND_RETRIEVAL) {
|
||||
await embeddingModelUpdateHandler();
|
||||
}
|
||||
|
|
@ -1460,7 +1488,115 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class=" mb-2.5 flex w-full justify-between">
|
||||
{#if RAGConfig.ENABLE_GOOGLE_DRIVE_INTEGRATION}
|
||||
<div class="space-y-2.5 mt-2">
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="text-xs font-medium mb-1">
|
||||
<Tooltip
|
||||
content={$i18n.t('API Key for Google Drive Picker - Required for basic file selection')}
|
||||
placement="top-start"
|
||||
>
|
||||
{$i18n.t('API Key')} <span class="text-red-500">*</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<SensitiveInput
|
||||
placeholder={$i18n.t('Enter Google Drive API Key')}
|
||||
bind:value={RAGConfig.GOOGLE_DRIVE_API_KEY}
|
||||
required={true}
|
||||
/>
|
||||
<div class="text-xs text-gray-400 dark:text-gray-500">
|
||||
{$i18n.t('Required for basic Google Drive file picker functionality')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="text-xs font-medium mb-1">
|
||||
<Tooltip
|
||||
content={$i18n.t('OAuth 2.0 Client ID for Google Drive Picker - Allows users to select files through the UI')}
|
||||
placement="top-start"
|
||||
>
|
||||
{$i18n.t('Client ID')}
|
||||
</Tooltip>
|
||||
</div>
|
||||
<input
|
||||
class="w-full text-sm bg-transparent outline-hidden"
|
||||
placeholder={$i18n.t('Enter Google Drive Client ID')}
|
||||
bind:value={RAGConfig.GOOGLE_DRIVE_CLIENT_ID}
|
||||
/>
|
||||
<div class="text-xs text-gray-400 dark:text-gray-500">
|
||||
{$i18n.t('Required for OAuth authentication when using the file picker')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="border-gray-100 dark:border-gray-800 my-3" />
|
||||
|
||||
<div class="flex w-full justify-between">
|
||||
<div class="self-center text-xs font-medium">
|
||||
<Tooltip
|
||||
content={$i18n.t('Enable automatic folder synchronization for knowledge bases (requires additional setup)')}
|
||||
placement="top-start"
|
||||
>
|
||||
{$i18n.t('Enable Folder Sync')}
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div class="flex items-center relative">
|
||||
<Switch bind:state={RAGConfig.ENABLE_GOOGLE_DRIVE_FOLDER_SYNC} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if RAGConfig.ENABLE_GOOGLE_DRIVE_FOLDER_SYNC}
|
||||
<div class="space-y-2.5 mt-2">
|
||||
<div class="text-xs text-orange-600 dark:text-orange-400 font-medium mb-2">
|
||||
{$i18n.t('Folder Sync Configuration')}
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="text-xs font-medium mb-1">
|
||||
<Tooltip
|
||||
content={$i18n.t('Service Account JSON for server-side sync operations')}
|
||||
placement="top-start"
|
||||
>
|
||||
{$i18n.t('Service Account JSON')} <span class="text-red-500">*</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Textarea
|
||||
bind:value={RAGConfig.GOOGLE_DRIVE_SERVICE_ACCOUNT_JSON}
|
||||
placeholder={$i18n.t('Paste your Google Service Account JSON here')}
|
||||
rows="4"
|
||||
/>
|
||||
<div class="text-xs text-gray-400 dark:text-gray-500">
|
||||
{$i18n.t('Required for server-side folder sync. Create a service account in Google Cloud Console.')}
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-xs text-gray-400 dark:text-gray-500">
|
||||
{$i18n.t('Follow these steps to set up Google Drive folder sync')}:<br />
|
||||
{$i18n.t('1. Create or select a project in Google Cloud Console and ensure Google Drive API is enabled')}<br />
|
||||
{$i18n.t('2. Create a service account and download the JSON key')}<br />
|
||||
{$i18n.t('3. Share your Google Drive folder with the service account email')}
|
||||
</div>
|
||||
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
|
||||
<a
|
||||
class="hover:underline dark:text-gray-200 text-gray-800"
|
||||
href="https://console.cloud.google.com/apis/credentials"
|
||||
target="_blank"
|
||||
>
|
||||
{$i18n.t('Open Google Cloud Console')}
|
||||
</a>
|
||||
{' • '}
|
||||
<a
|
||||
class="hover:underline dark:text-gray-200 text-gray-800"
|
||||
href="https://developers.google.com/drive/api/guides/about-auth"
|
||||
target="_blank"
|
||||
>
|
||||
{$i18n.t('View documentation')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="mb-2.5 flex w-full justify-between mt-4">
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('OneDrive')}</div>
|
||||
<div class="flex items-center relative">
|
||||
<Switch bind:state={RAGConfig.ENABLE_ONEDRIVE_INTEGRATION} />
|
||||
|
|
|
|||
|
|
@ -2,30 +2,18 @@
|
|||
import Fuse from 'fuse.js';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { PaneGroup, Pane, PaneResizer } from 'paneforge';
|
||||
|
||||
import { onMount, getContext, onDestroy, tick } from 'svelte';
|
||||
import { getContext, onDestroy, onMount } from 'svelte';
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import {
|
||||
mobile,
|
||||
showSidebar,
|
||||
knowledge as _knowledge,
|
||||
config,
|
||||
user,
|
||||
settings
|
||||
} from '$lib/stores';
|
||||
import { knowledge as _knowledge, config, showSidebar, user, settings } from '$lib/stores';
|
||||
|
||||
import {
|
||||
updateFileDataContentById,
|
||||
uploadFile,
|
||||
deleteFileById,
|
||||
getFileById
|
||||
} from '$lib/apis/files';
|
||||
import { getFileById, updateFileDataContentById, uploadFile } from '$lib/apis/files';
|
||||
import {
|
||||
addFileToKnowledgeById,
|
||||
getKnowledgeBases,
|
||||
getKnowledgeById,
|
||||
removeFileFromKnowledgeById,
|
||||
resetKnowledgeById,
|
||||
|
|
@ -38,23 +26,26 @@
|
|||
|
||||
import { blobToFile, isYoutubeUrl } from '$lib/utils';
|
||||
|
||||
import AddFilesPlaceholder from '$lib/components/AddFilesPlaceholder.svelte';
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import Files from './KnowledgeBase/Files.svelte';
|
||||
import AddFilesPlaceholder from '$lib/components/AddFilesPlaceholder.svelte';
|
||||
|
||||
import AddContentMenu from './KnowledgeBase/AddContentMenu.svelte';
|
||||
import AddTextContentModal from './KnowledgeBase/AddTextContentModal.svelte';
|
||||
|
||||
import SyncConfirmDialog from '../../common/ConfirmDialog.svelte';
|
||||
|
||||
import Drawer from '$lib/components/common/Drawer.svelte';
|
||||
import RichTextInput from '$lib/components/common/RichTextInput.svelte';
|
||||
import ChevronLeft from '$lib/components/icons/ChevronLeft.svelte';
|
||||
import LockClosed from '$lib/components/icons/LockClosed.svelte';
|
||||
import SyncConfirmDialog from '../../common/ConfirmDialog.svelte';
|
||||
import AccessControlModal from '../common/AccessControlModal.svelte';
|
||||
import Search from '$lib/components/icons/Search.svelte';
|
||||
import FilesOverlay from '$lib/components/chat/MessageInput/FilesOverlay.svelte';
|
||||
import DropdownOptions from '$lib/components/common/DropdownOptions.svelte';
|
||||
import Pagination from '$lib/components/common/Pagination.svelte';
|
||||
import AttachWebpageModal from '$lib/components/chat/MessageInput/AttachWebpageModal.svelte';
|
||||
import GoogleDriveSyncModal from './KnowledgeBase/GoogleDriveSyncModal.svelte';
|
||||
|
||||
let largeScreen = true;
|
||||
|
||||
|
|
@ -87,6 +78,7 @@
|
|||
let selectedFileId = null;
|
||||
let selectedFile = null;
|
||||
let selectedFileContent = '';
|
||||
let showGoogleDriveSyncModal = false;
|
||||
|
||||
let inputFiles = null;
|
||||
|
||||
|
|
@ -806,6 +798,16 @@
|
|||
}}
|
||||
/>
|
||||
|
||||
<GoogleDriveSyncModal
|
||||
bind:show={showGoogleDriveSyncModal}
|
||||
knowledgeId={id}
|
||||
knowledgeData={knowledge}
|
||||
on:sync={(e) => {
|
||||
knowledge = e.detail;
|
||||
toast.success($i18n.t('Google Drive folder synced successfully'));
|
||||
}}
|
||||
/>
|
||||
|
||||
<input
|
||||
id="files-input"
|
||||
bind:files={inputFiles}
|
||||
|
|
|
|||
|
|
@ -4,11 +4,13 @@
|
|||
|
||||
import Dropdown from '$lib/components/common/Dropdown.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import ArrowPath from '$lib/components/icons/ArrowPath.svelte';
|
||||
import ArrowUpCircle from '$lib/components/icons/ArrowUpCircle.svelte';
|
||||
import BarsArrowUp from '$lib/components/icons/BarsArrowUp.svelte';
|
||||
import FolderOpen from '$lib/components/icons/FolderOpen.svelte';
|
||||
import ArrowPath from '$lib/components/icons/ArrowPath.svelte';
|
||||
import GlobeAlt from '$lib/components/icons/GlobeAlt.svelte';
|
||||
import { config } from '$lib/stores';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
|
|
@ -110,6 +112,27 @@
|
|||
<BarsArrowUp strokeWidth="2" />
|
||||
<div class="flex items-center">{$i18n.t('Add text content')}</div>
|
||||
</button>
|
||||
|
||||
{#if $config?.features?.enable_google_drive_folder_sync}
|
||||
<button
|
||||
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||
on:click={() => {
|
||||
dispatch('sync', { type: 'google-drive' });
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
d="M6.28 3l5.72 10H22l-5.72-10H6.28zm0 0L.56 13H10.28l5.72-10H6.28zm5.72 10l-5.72 10H16.28L22 13H12z"
|
||||
/>
|
||||
</svg>
|
||||
<div class="flex items-center">{$i18n.t('Sync Google Drive folder')}</div>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</Dropdown>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,255 @@
|
|||
<script lang="ts">
|
||||
import { createEventDispatcher, getContext, onMount } from 'svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
import { getGoogleDriveServiceAccountEmail, syncGoogleDriveFolder } from '$lib/apis/knowledge';
|
||||
import { config } from '$lib/stores';
|
||||
import Modal from '$lib/components/common/Modal.svelte';
|
||||
import type {
|
||||
KnowledgeDataWithGoogleDrive,
|
||||
GoogleDriveSyncResponse
|
||||
} from '$lib/types/google-drive';
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
sync: GoogleDriveSyncResponse;
|
||||
}>();
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let show = false;
|
||||
export let knowledgeId: string;
|
||||
export let knowledgeData: KnowledgeDataWithGoogleDrive | null = null;
|
||||
|
||||
let loading: boolean = false;
|
||||
let serviceAccountEmail: string = '';
|
||||
let folderId: string = '';
|
||||
let includeNested: boolean = true;
|
||||
let syncIntervalDays: number = 1;
|
||||
|
||||
// Note: Backend expects sync_interval_days as a float representing days
|
||||
// For dev mode, we convert minutes to fractional days (1 day = 1440 minutes)
|
||||
$: syncIntervalOptions = [
|
||||
...($config?.environment === 'dev' ? [
|
||||
{ value: 1/1440, label: '1 minute' }, // 1 minute = 0.000694... days
|
||||
{ value: 5/1440, label: '5 minutes' } // 5 minutes = 0.00347... days
|
||||
] : []),
|
||||
{ value: 1, label: '1 day' },
|
||||
{ value: 2, label: '2 days' },
|
||||
{ value: 3, label: '3 days' }
|
||||
];
|
||||
|
||||
onMount(async () => {
|
||||
if (show) {
|
||||
await loadServiceAccountEmail();
|
||||
loadExistingConfiguration();
|
||||
}
|
||||
});
|
||||
|
||||
const loadExistingConfiguration = (): void => {
|
||||
if (knowledgeData) {
|
||||
// Load existing Google Drive configuration from knowledge base data
|
||||
if (knowledgeData.google_drive_folder_id) {
|
||||
folderId = knowledgeData.google_drive_folder_id;
|
||||
}
|
||||
if (knowledgeData.google_drive_include_nested !== undefined) {
|
||||
includeNested = knowledgeData.google_drive_include_nested;
|
||||
}
|
||||
if (knowledgeData.google_drive_sync_interval_days) {
|
||||
syncIntervalDays = knowledgeData.google_drive_sync_interval_days;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Watch for changes to show prop and reload configuration
|
||||
$: if (show) {
|
||||
loadServiceAccountEmail();
|
||||
loadExistingConfiguration();
|
||||
}
|
||||
|
||||
const loadServiceAccountEmail = async (): Promise<void> => {
|
||||
try {
|
||||
const response = await getGoogleDriveServiceAccountEmail(localStorage.token);
|
||||
if (response?.email) {
|
||||
serviceAccountEmail = response.email;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to get service account email:', error);
|
||||
toast.error($i18n.t('Failed to get Google Drive service account email'));
|
||||
}
|
||||
};
|
||||
|
||||
const extractFolderIdFromUrl = (url: string): string => {
|
||||
// Extract folder ID from Google Drive URL
|
||||
const patterns = [/\/folders\/([a-zA-Z0-9-_]+)/, /id=([a-zA-Z0-9-_]+)/, /^([a-zA-Z0-9-_]+)$/];
|
||||
|
||||
for (const pattern of patterns) {
|
||||
const match = url.match(pattern);
|
||||
if (match) {
|
||||
return match[1];
|
||||
}
|
||||
}
|
||||
|
||||
return url; // Return as-is if no pattern matches
|
||||
};
|
||||
|
||||
const handleSync = async (): Promise<void> => {
|
||||
if (!folderId.trim()) {
|
||||
toast.error($i18n.t('Please enter a Google Drive folder URL or ID'));
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
|
||||
try {
|
||||
const extractedFolderId = extractFolderIdFromUrl(folderId.trim());
|
||||
|
||||
const result = await syncGoogleDriveFolder(
|
||||
localStorage.token,
|
||||
knowledgeId,
|
||||
extractedFolderId,
|
||||
includeNested,
|
||||
syncIntervalDays
|
||||
);
|
||||
|
||||
if (result) {
|
||||
toast.success($i18n.t('Google Drive folder synced successfully'));
|
||||
dispatch('sync', result);
|
||||
show = false;
|
||||
} else {
|
||||
toast.error($i18n.t('Failed to sync Google Drive folder'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Sync error:', error);
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
toast.error($i18n.t('Error syncing Google Drive folder: {{error}}', { error: errorMessage }));
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
};
|
||||
|
||||
const copyServiceAccountEmail = (): void => {
|
||||
navigator.clipboard.writeText(serviceAccountEmail);
|
||||
toast.success($i18n.t('Service account email copied to clipboard'));
|
||||
};
|
||||
</script>
|
||||
|
||||
<Modal bind:show>
|
||||
<div>
|
||||
<div class=" flex justify-between dark:text-gray-300 px-5 pt-4 pb-2">
|
||||
<div class=" text-lg font-medium self-center">{$i18n.t('Sync Google Drive Folder')}</div>
|
||||
<button
|
||||
class="self-center"
|
||||
on:click={() => {
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<hr class=" border-gray-100 dark:border-gray-850" />
|
||||
|
||||
<div class="flex flex-col space-y-4 px-5 pt-4 pb-5">
|
||||
<!-- Service Account Email Section -->
|
||||
{#if serviceAccountEmail}
|
||||
<div class="space-y-2">
|
||||
<div class="text-sm font-medium">
|
||||
{$i18n.t('Step 1: Share folder with service account')}
|
||||
</div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{$i18n.t(
|
||||
'Copy the service account email below and share your Google Drive folder with this email address:'
|
||||
)}
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<input
|
||||
class="flex-1 text-sm bg-gray-50 dark:bg-gray-850 rounded-lg px-3 py-2"
|
||||
type="text"
|
||||
value={serviceAccountEmail}
|
||||
readonly
|
||||
/>
|
||||
<button
|
||||
class="px-3 py-2 bg-blue-500 hover:bg-blue-600 text-white text-sm rounded-lg"
|
||||
on:click={copyServiceAccountEmail}
|
||||
>
|
||||
{$i18n.t('Copy')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Folder ID/URL Input -->
|
||||
<div class="space-y-2">
|
||||
<div class="text-sm font-medium">{$i18n.t('Step 2: Enter folder URL or ID')}</div>
|
||||
<input
|
||||
class="w-full text-sm bg-gray-50 dark:bg-gray-850 rounded-lg px-3 py-2"
|
||||
type="text"
|
||||
placeholder={$i18n.t('Enter Google Drive folder URL or ID')}
|
||||
bind:value={folderId}
|
||||
/>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{$i18n.t('You can paste the full Google Drive folder URL or just the folder ID')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sync Options -->
|
||||
<div class="space-y-3">
|
||||
<div class="text-sm font-medium">{$i18n.t('Sync Options')}</div>
|
||||
|
||||
<!-- Include Nested Folders -->
|
||||
<div class="flex items-center space-x-2">
|
||||
<input type="checkbox" id="includeNested" bind:checked={includeNested} class="rounded" />
|
||||
<label for="includeNested" class="text-sm">
|
||||
{$i18n.t('Include files from nested folders')}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Sync Interval -->
|
||||
<div class="space-y-2">
|
||||
<label class="text-sm">{$i18n.t('Automatic sync interval')}</label>
|
||||
<select
|
||||
bind:value={syncIntervalDays}
|
||||
class="w-full text-sm bg-gray-50 dark:bg-gray-850 rounded-lg px-3 py-2"
|
||||
>
|
||||
{#each syncIntervalOptions as option}
|
||||
<option value={option.value}>{$i18n.t(option.label)}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex justify-end space-x-2 pt-4">
|
||||
<button
|
||||
class="px-4 py-2 text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
|
||||
on:click={() => {
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
{$i18n.t('Cancel')}
|
||||
</button>
|
||||
<button
|
||||
class="px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white text-sm rounded-lg disabled:opacity-50"
|
||||
disabled={loading || !folderId.trim()}
|
||||
on:click={handleSync}
|
||||
>
|
||||
{#if loading}
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
|
||||
<span>{$i18n.t('Syncing...')}</span>
|
||||
</div>
|
||||
{:else}
|
||||
{$i18n.t('Sync Folder')}
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
|
@ -2181,5 +2181,47 @@
|
|||
"Your usage stats have been successfully synced.": "",
|
||||
"YouTube": "",
|
||||
"Youtube Language": "",
|
||||
"Youtube Proxy URL": ""
|
||||
"Youtube Proxy URL": "",
|
||||
"Sync Google Drive folder": "",
|
||||
"API Key for Google Drive Picker - Required for basic file selection": "",
|
||||
"Enter Google Drive API Key": "",
|
||||
"Required for basic Google Drive file picker functionality": "",
|
||||
"OAuth 2.0 Client ID for Google Drive Picker - Allows users to select files through the UI": "",
|
||||
"Enter Google Drive Client ID": "",
|
||||
"Follow these steps to set up Google Drive folder sync": "",
|
||||
"1. Create or select a project in Google Cloud Console and ensure Google Drive API is enabled": "",
|
||||
"2. Create a service account and download the JSON key": "",
|
||||
"3. Share your Google Drive folder with the service account email": "",
|
||||
"Failed to get Google Drive service account email": "",
|
||||
"Please enter a Google Drive folder URL or ID": "",
|
||||
"Google Drive folder synced successfully": "",
|
||||
"Failed to sync Google Drive folder": "",
|
||||
"Error syncing Google Drive folder: {{error}}": "",
|
||||
"Sync Google Drive Folder": "",
|
||||
"Enter Google Drive folder URL or ID": "",
|
||||
"You can paste the full Google Drive folder URL or just the folder ID": "",
|
||||
"Step 1: Share folder with service account": "",
|
||||
"Step 2: Enter folder URL or ID": "",
|
||||
"Service account email copied to clipboard": "",
|
||||
"Sync Options": "",
|
||||
"Include files from nested folders": "",
|
||||
"Automatic sync interval": "",
|
||||
"Syncing...": "",
|
||||
"Sync Folder": "",
|
||||
"1 minute": "",
|
||||
"5 minutes": "",
|
||||
"1 day": "",
|
||||
"2 days": "",
|
||||
"3 days": "",
|
||||
"Service Account JSON is required when folder sync is enabled.": "",
|
||||
"Invalid Service Account JSON format.": "",
|
||||
"Service Account JSON for server-side sync operations": "",
|
||||
"Service Account JSON": "",
|
||||
"Paste your Google Service Account JSON here": "",
|
||||
"Enable automatic folder synchronization for knowledge bases (requires additional setup)": "",
|
||||
"Required for server-side folder sync. Create a service account in Google Cloud Console.": "",
|
||||
"Enable Folder Sync": "",
|
||||
"Google Drive API Key is required when integration is enabled.": "",
|
||||
"Google Drive Client ID is required when folder sync is enabled.": "",
|
||||
"Required for OAuth authentication when using the file picker": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -266,6 +266,7 @@ type Config = {
|
|||
name: string;
|
||||
version: string;
|
||||
default_locale: string;
|
||||
environment?: string;
|
||||
default_models: string;
|
||||
default_prompt_suggestions: PromptSuggestion[];
|
||||
features: {
|
||||
|
|
@ -276,6 +277,7 @@ type Config = {
|
|||
enable_login_form: boolean;
|
||||
enable_web_search?: boolean;
|
||||
enable_google_drive_integration: boolean;
|
||||
enable_google_drive_folder_sync?: boolean;
|
||||
enable_onedrive_integration: boolean;
|
||||
enable_image_generation: boolean;
|
||||
enable_admin_export: boolean;
|
||||
|
|
|
|||
291
src/lib/types/google-drive.ts
Normal file
291
src/lib/types/google-drive.ts
Normal file
|
|
@ -0,0 +1,291 @@
|
|||
// Google Drive TypeScript Type Definitions
|
||||
// Mirrors backend Python types for consistency
|
||||
|
||||
/**
|
||||
* Google Drive file information from Google Drive API
|
||||
*/
|
||||
export interface GoogleDriveFile {
|
||||
id: string;
|
||||
name: string;
|
||||
mimeType: string;
|
||||
modifiedTime: string;
|
||||
size?: string; // Optional field, not present for some files
|
||||
parents?: string[]; // Optional, may not be present
|
||||
path: string; // Custom field added by our service
|
||||
webViewLink?: string; // Optional web view URL
|
||||
}
|
||||
|
||||
/**
|
||||
* Google Drive folder information
|
||||
*/
|
||||
export interface GoogleDriveFolder {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Google Drive API list response structure
|
||||
*/
|
||||
export interface GoogleDriveAPIResponse {
|
||||
files: GoogleDriveFile[];
|
||||
nextPageToken?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* File download result structure
|
||||
*/
|
||||
export interface FileDownloadResult {
|
||||
content: ArrayBuffer | string;
|
||||
filename: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Google Drive sync configuration stored in knowledge base data
|
||||
*/
|
||||
export interface GoogleDriveSyncConfig {
|
||||
google_drive_folder_id: string;
|
||||
google_drive_include_nested: boolean;
|
||||
google_drive_sync_interval_days: number;
|
||||
google_drive_last_sync: number; // Unix timestamp
|
||||
file_ids: string[]; // List of file IDs in the knowledge base
|
||||
}
|
||||
|
||||
/**
|
||||
* Google Drive file metadata stored in file records
|
||||
*/
|
||||
export interface GoogleDriveFileMetadata {
|
||||
name: string;
|
||||
content_type: string;
|
||||
size: number;
|
||||
google_drive_id: string;
|
||||
google_drive_modified: string; // ISO timestamp from Google Drive API
|
||||
}
|
||||
|
||||
/**
|
||||
* Google Drive service account email response
|
||||
*/
|
||||
export interface GoogleDriveServiceAccount {
|
||||
email: string;
|
||||
status: 'active' | 'inactive';
|
||||
}
|
||||
|
||||
/**
|
||||
* Google Drive sync operation response
|
||||
*/
|
||||
export interface GoogleDriveSyncResponse {
|
||||
files_added: number;
|
||||
files_updated: number;
|
||||
files_removed: number;
|
||||
errors: string[];
|
||||
last_sync_time: string;
|
||||
folder_id: string;
|
||||
total_files: number;
|
||||
}
|
||||
|
||||
// Google Picker API Types
|
||||
|
||||
/**
|
||||
* Google Picker configuration options
|
||||
*/
|
||||
export interface GooglePickerConfig {
|
||||
clientId: string;
|
||||
discoveryDocs: string[];
|
||||
scope: string;
|
||||
immediate?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Google Picker result data structure
|
||||
*/
|
||||
export interface GooglePickerResult {
|
||||
action: string;
|
||||
docs?: GooglePickerDocument[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Google Picker API response constants (based on google.picker constants)
|
||||
*/
|
||||
export interface GooglePickerResponse {
|
||||
ACTION: string;
|
||||
DOCUMENTS: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Google Picker callback data structure (matches Google Picker API response format)
|
||||
*/
|
||||
export interface GooglePickerCallbackData {
|
||||
[key: string]: string | number | GooglePickerDocument[] | GooglePickerDocument;
|
||||
}
|
||||
|
||||
/**
|
||||
* Google Picker document structure
|
||||
*/
|
||||
export interface GooglePickerDocument {
|
||||
id: string;
|
||||
name: string;
|
||||
mimeType: string;
|
||||
sizeBytes?: number;
|
||||
url?: string;
|
||||
iconUrl?: string;
|
||||
parents?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Google Auth token response
|
||||
*/
|
||||
export interface GoogleAuthToken {
|
||||
access_token: string;
|
||||
expires_in: number;
|
||||
scope: string;
|
||||
token_type: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Google Picker callback function types
|
||||
*/
|
||||
export type GooglePickerCallback = (result: GooglePickerResult) => void;
|
||||
export type GooglePickerErrorCallback = (error: GoogleAPIError) => void;
|
||||
|
||||
/**
|
||||
* Google API error structure
|
||||
*/
|
||||
export interface GoogleAPIError {
|
||||
code: number;
|
||||
message: string;
|
||||
status: string;
|
||||
details?: Array<{ message: string; domain: string; reason: string }>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Google OAuth error structure
|
||||
*/
|
||||
export interface GoogleOAuthError {
|
||||
error: string;
|
||||
error_description?: string;
|
||||
error_uri?: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Knowledge base data with Google Drive fields
|
||||
*/
|
||||
export interface KnowledgeDataWithGoogleDrive {
|
||||
id?: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
data?: Record<string, unknown>;
|
||||
// Google Drive specific fields
|
||||
google_drive_folder_id?: string;
|
||||
google_drive_include_nested?: boolean;
|
||||
google_drive_sync_interval_days?: number;
|
||||
google_drive_last_sync?: number;
|
||||
google_drive_file_ids?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* API error response structure
|
||||
*/
|
||||
export interface APIError {
|
||||
detail: string;
|
||||
status?: number;
|
||||
code?: string;
|
||||
}
|
||||
|
||||
// Global Google API declarations for TypeScript
|
||||
declare global {
|
||||
interface Window {
|
||||
gapi: {
|
||||
load: (api: string, callback: () => void) => void;
|
||||
auth2: {
|
||||
getAuthInstance: () => {
|
||||
signIn: () => Promise<GoogleAuthToken>;
|
||||
isSignedIn: {
|
||||
get: () => boolean;
|
||||
};
|
||||
currentUser: {
|
||||
get: () => {
|
||||
getAuthResponse: () => GoogleAuthToken;
|
||||
};
|
||||
};
|
||||
};
|
||||
init: (config: GooglePickerConfig) => Promise<void>;
|
||||
};
|
||||
client: {
|
||||
init: (config: GooglePickerConfig) => Promise<void>;
|
||||
request: (config: Record<string, unknown>) => Promise<Record<string, unknown>>;
|
||||
};
|
||||
};
|
||||
google: {
|
||||
accounts: {
|
||||
oauth2: {
|
||||
initTokenClient: (config: {
|
||||
client_id: string;
|
||||
scope: string;
|
||||
callback: (response: GoogleAuthToken) => void;
|
||||
error_callback: (error: GoogleOAuthError) => void;
|
||||
}) => {
|
||||
requestAccessToken: () => void;
|
||||
};
|
||||
};
|
||||
};
|
||||
picker: {
|
||||
api: {
|
||||
loaded: () => boolean;
|
||||
};
|
||||
PickerBuilder: new () => {
|
||||
addView: (view: GooglePickerView) => GooglePickerBuilder;
|
||||
setCallback: (callback: (data: GooglePickerCallbackData) => void) => GooglePickerBuilder;
|
||||
setOAuthToken: (token: string) => GooglePickerBuilder;
|
||||
setDeveloperKey: (key: string) => GooglePickerBuilder;
|
||||
enableFeature: (feature: GooglePickerFeature) => GooglePickerBuilder;
|
||||
build: () => GooglePickerWidget;
|
||||
};
|
||||
DocsView: new () => GooglePickerView;
|
||||
ViewId: {
|
||||
DOCS: string;
|
||||
FOLDERS: string;
|
||||
};
|
||||
Feature: {
|
||||
NAV_HIDDEN: GooglePickerFeature;
|
||||
MULTISELECT_ENABLED: GooglePickerFeature;
|
||||
};
|
||||
Response: {
|
||||
ACTION: string;
|
||||
DOCUMENTS: string;
|
||||
};
|
||||
Action: {
|
||||
PICKED: string;
|
||||
CANCEL: string;
|
||||
};
|
||||
Document: {
|
||||
ID: string;
|
||||
NAME: string;
|
||||
URL: string;
|
||||
MIME_TYPE: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
// Additional Google Picker types
|
||||
interface GooglePickerBuilder {
|
||||
addView: (view: GooglePickerView) => GooglePickerBuilder;
|
||||
setCallback: (callback: (data: GooglePickerCallbackData) => void) => GooglePickerBuilder;
|
||||
setOAuthToken: (token: string) => GooglePickerBuilder;
|
||||
setDeveloperKey: (key: string) => GooglePickerBuilder;
|
||||
enableFeature: (feature: GooglePickerFeature) => GooglePickerBuilder;
|
||||
build: () => GooglePickerWidget;
|
||||
}
|
||||
|
||||
interface GooglePickerWidget {
|
||||
setVisible: (visible: boolean) => void;
|
||||
}
|
||||
|
||||
interface GooglePickerView {
|
||||
setIncludeFolders: (include: boolean) => GooglePickerView;
|
||||
setSelectFolderEnabled: (enabled: boolean) => GooglePickerView;
|
||||
setMimeTypes: (types: string) => GooglePickerView;
|
||||
}
|
||||
|
||||
interface GooglePickerFeature {}
|
||||
}
|
||||
|
|
@ -13,3 +13,6 @@ export enum TTS_RESPONSE_SPLIT {
|
|||
PARAGRAPHS = 'paragraphs',
|
||||
NONE = 'none'
|
||||
}
|
||||
|
||||
// Re-export Google Drive types for convenience
|
||||
export * from './google-drive';
|
||||
|
|
|
|||
|
|
@ -1,16 +1,29 @@
|
|||
import type {
|
||||
GoogleAuthToken,
|
||||
GooglePickerCallbackData,
|
||||
GoogleOAuthError
|
||||
} from '$lib/types/google-drive.ts';
|
||||
|
||||
// Google Drive Picker API configuration
|
||||
let API_KEY = '';
|
||||
let CLIENT_ID = '';
|
||||
|
||||
interface ConfigResponse {
|
||||
google_drive?: {
|
||||
api_key?: string;
|
||||
client_id?: string;
|
||||
};
|
||||
}
|
||||
|
||||
// Function to fetch credentials from backend config
|
||||
async function getCredentials() {
|
||||
async function getCredentials(): Promise<void> {
|
||||
const response = await fetch('/api/config');
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch Google Drive credentials');
|
||||
}
|
||||
const config = await response.json();
|
||||
API_KEY = config.google_drive?.api_key;
|
||||
CLIENT_ID = config.google_drive?.client_id;
|
||||
const config: ConfigResponse = await response.json();
|
||||
API_KEY = config.google_drive?.api_key || '';
|
||||
CLIENT_ID = config.google_drive?.client_id || '';
|
||||
|
||||
if (!API_KEY || !CLIENT_ID) {
|
||||
throw new Error('Google Drive API credentials not configured');
|
||||
|
|
@ -22,7 +35,7 @@ const SCOPE = [
|
|||
];
|
||||
|
||||
// Validate required credentials
|
||||
const validateCredentials = () => {
|
||||
const validateCredentials = (): void => {
|
||||
if (!API_KEY || !CLIENT_ID) {
|
||||
throw new Error('Google Drive API credentials not configured');
|
||||
}
|
||||
|
|
@ -31,18 +44,16 @@ const validateCredentials = () => {
|
|||
}
|
||||
};
|
||||
|
||||
let pickerApiLoaded = false;
|
||||
let oauthToken: string | null = null;
|
||||
let initialized = false;
|
||||
|
||||
export const loadGoogleDriveApi = () => {
|
||||
export const loadGoogleDriveApi = (): Promise<boolean> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (typeof gapi === 'undefined') {
|
||||
const script = document.createElement('script');
|
||||
script.src = 'https://apis.google.com/js/api.js';
|
||||
script.onload = () => {
|
||||
gapi.load('picker', () => {
|
||||
pickerApiLoaded = true;
|
||||
resolve(true);
|
||||
});
|
||||
};
|
||||
|
|
@ -50,14 +61,13 @@ export const loadGoogleDriveApi = () => {
|
|||
document.body.appendChild(script);
|
||||
} else {
|
||||
gapi.load('picker', () => {
|
||||
pickerApiLoaded = true;
|
||||
resolve(true);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const loadGoogleAuthApi = () => {
|
||||
export const loadGoogleAuthApi = (): Promise<unknown> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (typeof google === 'undefined') {
|
||||
const script = document.createElement('script');
|
||||
|
|
@ -71,13 +81,13 @@ export const loadGoogleAuthApi = () => {
|
|||
});
|
||||
};
|
||||
|
||||
export const getAuthToken = async () => {
|
||||
export const getAuthToken = async (): Promise<string | null> => {
|
||||
if (!oauthToken) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const tokenClient = google.accounts.oauth2.initTokenClient({
|
||||
client_id: CLIENT_ID,
|
||||
scope: SCOPE.join(' '),
|
||||
callback: (response: any) => {
|
||||
callback: (response: GoogleAuthToken) => {
|
||||
if (response.access_token) {
|
||||
oauthToken = response.access_token;
|
||||
resolve(oauthToken);
|
||||
|
|
@ -85,8 +95,8 @@ export const getAuthToken = async () => {
|
|||
reject(new Error('Failed to get access token'));
|
||||
}
|
||||
},
|
||||
error_callback: (error: any) => {
|
||||
reject(new Error(error.message || 'OAuth error occurred'));
|
||||
error_callback: (error: GoogleOAuthError) => {
|
||||
reject(new Error(error.message || error.error_description || 'OAuth error occurred'));
|
||||
}
|
||||
});
|
||||
tokenClient.requestAccessToken();
|
||||
|
|
@ -95,7 +105,7 @@ export const getAuthToken = async () => {
|
|||
return oauthToken;
|
||||
};
|
||||
|
||||
const initialize = async () => {
|
||||
const initialize = async (): Promise<void> => {
|
||||
if (!initialized) {
|
||||
await getCredentials();
|
||||
validateCredentials();
|
||||
|
|
@ -104,19 +114,30 @@ const initialize = async () => {
|
|||
}
|
||||
};
|
||||
|
||||
export const createPicker = () => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
console.log('Initializing Google Drive Picker...');
|
||||
await initialize();
|
||||
console.log('Getting auth token...');
|
||||
const token = await getAuthToken();
|
||||
if (!token) {
|
||||
console.error('Failed to get OAuth token');
|
||||
throw new Error('Unable to get OAuth token');
|
||||
}
|
||||
console.log('Auth token obtained successfully');
|
||||
interface PickerFileResult {
|
||||
id: string;
|
||||
name: string;
|
||||
url: string;
|
||||
blob: Blob;
|
||||
headers: {
|
||||
Authorization: string;
|
||||
Accept: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const createPicker = async (): Promise<PickerFileResult | null> => {
|
||||
try {
|
||||
console.log('Initializing Google Drive Picker...');
|
||||
await initialize();
|
||||
console.log('Getting auth token...');
|
||||
const token = await getAuthToken();
|
||||
if (!token) {
|
||||
console.error('Failed to get OAuth token');
|
||||
throw new Error('Unable to get OAuth token');
|
||||
}
|
||||
console.log('Auth token obtained successfully');
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const picker = new google.picker.PickerBuilder()
|
||||
.enableFeature(google.picker.Feature.NAV_HIDDEN)
|
||||
.enableFeature(google.picker.Feature.MULTISELECT_ENABLED)
|
||||
|
|
@ -131,13 +152,12 @@ export const createPicker = () => {
|
|||
.setOAuthToken(token)
|
||||
.setDeveloperKey(API_KEY)
|
||||
// Remove app ID setting as it's not needed and can cause 404 errors
|
||||
.setCallback(async (data: any) => {
|
||||
.setCallback(async (data: GooglePickerCallbackData) => {
|
||||
if (data[google.picker.Response.ACTION] === google.picker.Action.PICKED) {
|
||||
try {
|
||||
const doc = data[google.picker.Response.DOCUMENTS][0];
|
||||
const fileId = doc[google.picker.Document.ID];
|
||||
const fileName = doc[google.picker.Document.NAME];
|
||||
const fileUrl = doc[google.picker.Document.URL];
|
||||
|
||||
if (!fileId || !fileName) {
|
||||
throw new Error('Required file details missing');
|
||||
|
|
@ -204,9 +224,9 @@ export const createPicker = () => {
|
|||
})
|
||||
.build();
|
||||
picker.setVisible(true);
|
||||
} catch (error) {
|
||||
console.error('Google Drive Picker error:', error);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Google Drive Picker error:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
466
uv.lock
generated
466
uv.lock
generated
|
|
@ -16,7 +16,7 @@ resolution-markers = [
|
|||
|
||||
[[package]]
|
||||
name = "accelerate"
|
||||
version = "1.8.1"
|
||||
version = "1.7.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "huggingface-hub" },
|
||||
|
|
@ -27,9 +27,9 @@ dependencies = [
|
|||
{ name = "safetensors" },
|
||||
{ name = "torch" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/bd/c2/b9e33ad13232606dded4c546e654fb06a15f1dbcbd95d81c9f9dd3ccc771/accelerate-1.8.1.tar.gz", hash = "sha256:f60df931671bc4e75077b852990469d4991ce8bd3a58e72375c3c95132034db9", size = 380872, upload-time = "2025-06-20T15:36:14.618Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/97/33/47bbd507e3a851d33d19ce7b2141c5ea3689bfae91ba168044d7db24b0e9/accelerate-1.7.0.tar.gz", hash = "sha256:e8a2a5503d6237b9eee73cc8d36cf543f9c2d8dd2c6713450b322f5e6d53a610", size = 376026, upload-time = "2025-05-15T10:00:52.117Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/91/d9/e044c9d42d8ad9afa96533b46ecc9b7aea893d362b3c52bd78fb9fe4d7b3/accelerate-1.8.1-py3-none-any.whl", hash = "sha256:c47b8994498875a2b1286e945bd4d20e476956056c7941d512334f4eb44ff991", size = 365338, upload-time = "2025-06-20T15:36:12.71Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/bb/be8146c196ad6e4dec78385d91e92591f8a433576c4e04c342a636fcd811/accelerate-1.7.0-py3-none-any.whl", hash = "sha256:cf57165cca28769c6cf2650812371c81b18e05743dfa3c748524b1bb4f2b272f", size = 362095, upload-time = "2025-05-15T10:00:49.914Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -290,30 +290,30 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "azure-ai-documentintelligence"
|
||||
version = "1.0.2"
|
||||
version = "1.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "azure-core" },
|
||||
{ name = "isodate" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/44/7b/8115cd713e2caa5e44def85f2b7ebd02a74ae74d7113ba20bdd41fd6dd80/azure_ai_documentintelligence-1.0.2.tar.gz", hash = "sha256:4d75a2513f2839365ebabc0e0e1772f5601b3a8c9a71e75da12440da13b63484", size = 170940, upload-time = "2025-03-27T02:46:20.606Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ca/fd/cd0d493e9dc93a5ce097db7508f1b2467a73dcc7022c235b409ce48b9679/azure_ai_documentintelligence-1.0.0.tar.gz", hash = "sha256:c8b6efc0fc7e65d7892c9585cfd256f7d8b3f2b46cecf92c75ab82e629eac253", size = 169420, upload-time = "2024-12-18T01:54:11.342Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/75/c9ec040f23082f54ffb1977ff8f364c2d21c79a640a13d1c1809e7fd6b1a/azure_ai_documentintelligence-1.0.2-py3-none-any.whl", hash = "sha256:e1fb446abbdeccc9759d897898a0fe13141ed29f9ad11fc705f951925822ed59", size = 106005, upload-time = "2025-03-27T02:46:22.356Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/a8/c9c66d4d04b8aee06ebdc9a6077736b222b9b2fe92364fed6f9a1c08ece0/azure_ai_documentintelligence-1.0.0-py3-none-any.whl", hash = "sha256:cdedb1a67c075f58f47a413ec5846bf8d532a83a71f0c51ec49ce9b5bfe2a519", size = 105454, upload-time = "2024-12-18T01:54:14.639Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "azure-core"
|
||||
version = "1.35.0"
|
||||
version = "1.34.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "requests" },
|
||||
{ name = "six" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ce/89/f53968635b1b2e53e4aad2dd641488929fef4ca9dfb0b97927fa7697ddf3/azure_core-1.35.0.tar.gz", hash = "sha256:c0be528489485e9ede59b6971eb63c1eaacf83ef53001bfe3904e475e972be5c", size = 339689, upload-time = "2025-07-03T00:55:23.496Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c9/29/ff7a519a315e41c85bab92a7478c6acd1cf0b14353139a08caee4c691f77/azure_core-1.34.0.tar.gz", hash = "sha256:bdb544989f246a0ad1c85d72eeb45f2f835afdcbc5b45e43f0dbde7461c81ece", size = 297999, upload-time = "2025-05-01T23:17:27.59Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/78/bf94897361fdd650850f0f2e405b2293e2f12808239046232bdedf554301/azure_core-1.35.0-py3-none-any.whl", hash = "sha256:8db78c72868a58f3de8991eb4d22c4d368fae226dac1002998d6c50437e7dad1", size = 210708, upload-time = "2025-07-03T00:55:25.238Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/9e/5c87b49f65bb16571599bc789857d0ded2f53014d3392bc88a5d1f3ad779/azure_core-1.34.0-py3-none-any.whl", hash = "sha256:0615d3b756beccdb6624d1c0ae97284f38b78fb59a2a9839bf927c66fbbdddd6", size = 207409, upload-time = "2025-05-01T23:17:29.818Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -786,6 +786,15 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorclass"
|
||||
version = "2.2.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d7/1a/31ff00a33569a3b59d65bbdc445c73e12f92ad28195b7ace299f68b9af70/colorclass-2.2.2.tar.gz", hash = "sha256:6d4fe287766166a98ca7bc6f6312daf04a0481b1eda43e7173484051c0ab4366", size = 16709, upload-time = "2021-12-09T00:41:35.661Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/30/b6/daf3e2976932da4ed3579cff7a30a53d22ea9323ee4f0d8e43be60454897/colorclass-2.2.2-py2.py3-none-any.whl", hash = "sha256:6f10c273a0ef7a1150b1120b6095cbdd68e5cf36dfd5d0fc957a2500bbf99a55", size = 18995, upload-time = "2021-12-09T00:41:34.653Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "coloredlogs"
|
||||
version = "15.0.1"
|
||||
|
|
@ -798,6 +807,12 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/a7/06/3d6badcf13db419e25b07041d9c7b4a2c331d3f4e7134445ec5df57714cd/coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934", size = 46018, upload-time = "2021-06-11T10:22:42.561Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "compressed-rtf"
|
||||
version = "1.0.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8e/ac/abb196bb0b42a239d605fe97c314c3312374749013a07da4e6e0408f223c/compressed_rtf-1.0.6.tar.gz", hash = "sha256:c1c827f1d124d24608981a56e8b8691eb1f2a69a78ccad6440e7d92fde1781dd", size = 5800, upload-time = "2020-02-16T20:01:11.917Z" }
|
||||
|
||||
[[package]]
|
||||
name = "cryptography"
|
||||
version = "44.0.0"
|
||||
|
|
@ -887,20 +902,6 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/d7/84/0df6c5981f5fc722381662ff8cfbdf8aad64bec875f75d80b55bfef394ce/datasets-3.2.0-py3-none-any.whl", hash = "sha256:f3d2ba2698b7284a4518019658596a6a8bc79f31e51516524249d6c59cf0fe2a", size = 480647, upload-time = "2024-12-10T16:56:34.742Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ddgs"
|
||||
version = "9.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
{ name = "lxml" },
|
||||
{ name = "primp" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1f/08/0e84549a1d7d5950573f73d7bc5d36f2a00f92ad8e644b59066afd430a6f/ddgs-9.0.0.tar.gz", hash = "sha256:53b47c74a8060457cb02cbb64acdf59655d799ce8e0934e945bcd878fcab3a7f", size = 21795, upload-time = "2025-07-06T15:43:50.862Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/05/bd3ed9a28212b313f5678533152c4d79fbc386e44245ca5eed426d75f019/ddgs-9.0.0-py3-none-any.whl", hash = "sha256:5dd11d666d6caf1cfdbd341579637bb670c4b2f41df5413b76705519d8e7a22c", size = 17944, upload-time = "2025-07-06T15:43:49.564Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "defusedxml"
|
||||
version = "0.7.1"
|
||||
|
|
@ -992,6 +993,23 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/4c/a3/ac312faeceffd2d8f86bc6dcb5c401188ba5a01bc88e69bed97578a0dfcd/durationpy-0.9-py3-none-any.whl", hash = "sha256:e65359a7af5cedad07fb77a2dd3f390f8eb0b74cb845589fa6c057086834dd38", size = 3461, upload-time = "2024-10-02T17:58:59.349Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "easygui"
|
||||
version = "0.98.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/cc/ad/e35f7a30272d322be09dc98592d2f55d27cc933a7fde8baccbbeb2bd9409/easygui-0.98.3.tar.gz", hash = "sha256:d653ff79ee1f42f63b5a090f2f98ce02335d86ad8963b3ce2661805cafe99a04", size = 85583, upload-time = "2022-04-01T13:15:50.752Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/a7/b276ff776533b423710a285c8168b52551cb2ab0855443131fdc7fd8c16f/easygui-0.98.3-py2.py3-none-any.whl", hash = "sha256:33498710c68b5376b459cd3fc48d1d1f33822139eb3ed01defbc0528326da3ba", size = 92655, upload-time = "2022-04-01T13:15:49.568Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ebcdic"
|
||||
version = "1.1.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/2f/633031205333bee5f9f93761af8268746aa75f38754823aabb8570eb245b/ebcdic-1.1.1-py2.py3-none-any.whl", hash = "sha256:33b4cb729bc2d0bf46cc1847b0e5946897cb8d3f53520c5b9aa5fa98d7e735f1", size = 128537, upload-time = "2019-08-09T00:54:35.544Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ecdsa"
|
||||
version = "0.19.0"
|
||||
|
|
@ -1075,6 +1093,24 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/25/ed/e47dec0626edd468c84c04d97769e7ab4ea6457b7f54dcb3f72b17fcd876/Events-0.5-py3-none-any.whl", hash = "sha256:a7286af378ba3e46640ac9825156c93bdba7502174dd696090fdfcd4d80a1abd", size = 6758, upload-time = "2023-07-31T08:23:13.645Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "extract-msg"
|
||||
version = "0.52.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "beautifulsoup4" },
|
||||
{ name = "compressed-rtf" },
|
||||
{ name = "ebcdic" },
|
||||
{ name = "olefile" },
|
||||
{ name = "red-black-tree-mod" },
|
||||
{ name = "rtfde" },
|
||||
{ name = "tzlocal" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1f/eb/e2ac47dcc951818f7185fafc5eff2dea76adcce4d468064ffc7680d7ac4b/extract_msg-0.52.0.tar.gz", hash = "sha256:c21c548c43e1f0cdce5616102d33e590e2b46fbdc9d04f21af4eb62dcbf296dd", size = 328420, upload-time = "2024-10-22T20:34:53.813Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/44/c2/ceb6eb951fa62140c80624517f1ee57cdf4495fc1cc91080ea33f192b757/extract_msg-0.52.0-py3-none-any.whl", hash = "sha256:93c919846bac2a6034cf7d0dcf8e825d640b6ddb8539e42f7f1817869cd1eeaf", size = 334568, upload-time = "2024-10-22T20:34:52.342Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fake-useragent"
|
||||
version = "2.1.0"
|
||||
|
|
@ -1744,24 +1780,9 @@ wheels = [
|
|||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
brotli = [
|
||||
{ name = "brotli", marker = "platform_python_implementation == 'CPython'" },
|
||||
{ name = "brotlicffi", marker = "platform_python_implementation != 'CPython'" },
|
||||
]
|
||||
cli = [
|
||||
{ name = "click" },
|
||||
{ name = "pygments" },
|
||||
{ name = "rich" },
|
||||
]
|
||||
http2 = [
|
||||
{ name = "h2" },
|
||||
]
|
||||
socks = [
|
||||
{ name = "socksio" },
|
||||
]
|
||||
zstd = [
|
||||
{ name = "zstandard" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpx-sse"
|
||||
|
|
@ -1975,7 +1996,7 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "langchain"
|
||||
version = "0.3.26"
|
||||
version = "0.3.24"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "langchain-core" },
|
||||
|
|
@ -1986,14 +2007,14 @@ dependencies = [
|
|||
{ name = "requests" },
|
||||
{ name = "sqlalchemy" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7f/13/a9931800ee42bbe0f8850dd540de14e80dda4945e7ee36e20b5d5964286e/langchain-0.3.26.tar.gz", hash = "sha256:8ff034ee0556d3e45eff1f1e96d0d745ced57858414dba7171c8ebdbeb5580c9", size = 10226808, upload-time = "2025-06-20T22:23:01.174Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a3/8f/db961066a65e678036886c73234827c56547fed2e06fd1b425767e4dc059/langchain-0.3.24.tar.gz", hash = "sha256:caf1bacdabbea429bc79b58b118c06c3386107d92812e15922072b91745f070f", size = 10224882, upload-time = "2025-04-22T15:27:30.834Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/f2/c09a2e383283e3af1db669ab037ac05a45814f4b9c472c48dc24c0cef039/langchain-0.3.26-py3-none-any.whl", hash = "sha256:361bb2e61371024a8c473da9f9c55f4ee50f269c5ab43afdb2b1309cb7ac36cf", size = 1012336, upload-time = "2025-06-20T22:22:58.874Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/83/77392f0a6a560e471075b125656b392d3b889be65ee8e93a5c31aa7a62bb/langchain-0.3.24-py3-none-any.whl", hash = "sha256:596c5444716644ddd0cd819fb2bc9d0fd4221503b219fdfb5016edcfaa7da8ef", size = 1010778, upload-time = "2025-04-22T15:27:28.631Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "langchain-community"
|
||||
version = "0.3.26"
|
||||
version = "0.3.23"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
|
|
@ -2009,14 +2030,14 @@ dependencies = [
|
|||
{ name = "sqlalchemy" },
|
||||
{ name = "tenacity" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/aa/15/69940212569e7d7ac7b486fba244701448e8685f79069b73206c44e96fde/langchain_community-0.3.26.tar.gz", hash = "sha256:49f9d71dc20bc42ccecd6875d02fafef1be0e211a0b22cecbd678f5fd3719487", size = 33235791, upload-time = "2025-06-20T22:32:41.727Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c2/01/fdd97e392ab888ee195cbb3ed9d1140b66dd0090375151c768288eb63e61/langchain_community-0.3.23.tar.gz", hash = "sha256:afb4b34d8b75fc00f78b2270e988bb48fff96b333d23fae05ab32d012940973f", size = 33229515, upload-time = "2025-04-28T18:59:04.551Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/44/8e/d3d201f648e8d09dc1072a734c4dc1f59455b91d7d162427256533bf5a87/langchain_community-0.3.26-py3-none-any.whl", hash = "sha256:b25a553ee9d44a6c02092a440da6c561a9312c7013ffc25365ac3f8694edb53a", size = 2529186, upload-time = "2025-06-20T22:32:39.738Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/a7/b779146b33e1f2b5ef6d44525a8cb476f8d156e2e98a251588f467d74ce3/langchain_community-0.3.23-py3-none-any.whl", hash = "sha256:7b5328e749df6bbaf8e60c53d810a95ab22f2d2262911b206b0fb582d58350b7", size = 2525391, upload-time = "2025-04-28T18:59:02.076Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "langchain-core"
|
||||
version = "0.3.68"
|
||||
version = "0.3.64"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "jsonpatch" },
|
||||
|
|
@ -2027,9 +2048,9 @@ dependencies = [
|
|||
{ name = "tenacity" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/23/20/f5b18a17bfbe3416177e702ab2fd230b7d168abb17be31fb48f43f0bb772/langchain_core-0.3.68.tar.gz", hash = "sha256:312e1932ac9aa2eaf111b70fdc171776fa571d1a86c1f873dcac88a094b19c6f", size = 563041, upload-time = "2025-07-03T17:02:28.704Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/58/40/89a80157f495d4adc9e5e770171806e3231600647f4ca0e89bdf743702ff/langchain_core-0.3.64.tar.gz", hash = "sha256:71b51bf77003eb57e74b8fa2a84ac380e24aa7357f173b51645c5834b9fc0d62", size = 558483, upload-time = "2025-06-05T21:27:10.817Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/da/c89be0a272993bfcb762b2a356b9f55de507784c2755ad63caec25d183bf/langchain_core-0.3.68-py3-none-any.whl", hash = "sha256:5e5c1fbef419590537c91b8c2d86af896fbcbaf0d5ed7fdcdd77f7d8f3467ba0", size = 441405, upload-time = "2025-07-03T17:02:27.115Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/43/94b486eeb778443887e4eb76326e704ee0c6244f5fab6a46686e09968e9a/langchain_core-0.3.64-py3-none-any.whl", hash = "sha256:e844c425329d450cb3010001d86b61fd59a6a17691641109bae39322c85e27dd", size = 438113, upload-time = "2025-06-05T21:27:07.981Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -2073,7 +2094,7 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "langsmith"
|
||||
version = "0.4.5"
|
||||
version = "0.3.45"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "httpx" },
|
||||
|
|
@ -2084,9 +2105,18 @@ dependencies = [
|
|||
{ name = "requests-toolbelt" },
|
||||
{ name = "zstandard" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5c/92/7885823f3d13222f57773921f0da19b37d628c64607491233dc853a0f6ea/langsmith-0.4.5.tar.gz", hash = "sha256:49444bd8ccd4e46402f1b9ff1d686fa8e3a31b175e7085e72175ab8ec6164a34", size = 352235, upload-time = "2025-07-10T22:08:04.505Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/be/86/b941012013260f95af2e90a3d9415af4a76a003a28412033fc4b09f35731/langsmith-0.3.45.tar.gz", hash = "sha256:1df3c6820c73ed210b2c7bc5cdb7bfa19ddc9126cd03fdf0da54e2e171e6094d", size = 348201, upload-time = "2025-06-05T05:10:28.948Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/10/ad3107b666c3203b7938d10ea6b8746b9735c399cf737a51386d58e41d34/langsmith-0.4.5-py3-none-any.whl", hash = "sha256:4167717a2cccc4dff5809dbddc439628e836f6fd13d4fdb31ea013bc8d5cfaf5", size = 367795, upload-time = "2025-07-10T22:08:02.548Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/f4/c206c0888f8a506404cb4f16ad89593bdc2f70cf00de26a1a0a7a76ad7a3/langsmith-0.3.45-py3-none-any.whl", hash = "sha256:5b55f0518601fa65f3bb6b1a3100379a96aa7b3ed5e9380581615ba9c65ed8ed", size = 363002, upload-time = "2025-06-05T05:10:27.228Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lark"
|
||||
version = "1.1.9"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2c/e1/804b6196b3fbdd0f8ba785fc62837b034782a891d6f663eea2f30ca23cfa/lark-1.1.9.tar.gz", hash = "sha256:15fa5236490824c2c4aba0e22d2d6d823575dcaf4cdd1848e34b6ad836240fba", size = 255451, upload-time = "2024-01-10T08:33:51.411Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/9c/eef7c591e6dc952f3636cfe0df712c0f9916cedf317810a3bb53ccb65cdd/lark-1.1.9-py3-none-any.whl", hash = "sha256:a0dd3a87289f8ccbb325901e4222e723e7d745dbfc1803eaf5f3d2ace19cf2db", size = 111693, upload-time = "2024-01-10T08:33:48.873Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -2362,6 +2392,19 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/5e/75/bd9b7bb966668920f06b200e84454c8f3566b102183bc55c5473d96cb2b9/msal_extensions-1.3.1-py3-none-any.whl", hash = "sha256:96d3de4d034504e969ac5e85bae8106c8373b5c6568e4c8fa7af2eca9dbe6bca", size = 20583, upload-time = "2025-03-14T23:51:03.016Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "msoffcrypto-tool"
|
||||
version = "5.4.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cryptography" },
|
||||
{ name = "olefile" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d2/b7/0fd6573157e0ec60c0c470e732ab3322fba4d2834fd24e1088d670522a01/msoffcrypto_tool-5.4.2.tar.gz", hash = "sha256:44b545adba0407564a0cc3d6dde6ca36b7c0fdf352b85bca51618fa1d4817370", size = 41183, upload-time = "2024-08-08T15:50:28.462Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/03/54/7f6d3d9acad083dae8c22d9ab483b657359a1bf56fee1d7af88794677707/msoffcrypto_tool-5.4.2-py3-none-any.whl", hash = "sha256:274fe2181702d1e5a107ec1b68a4c9fea997a44972ae1cc9ae0cb4f6a50fef0e", size = 48713, upload-time = "2024-08-08T15:50:27.093Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "multidict"
|
||||
version = "6.1.0"
|
||||
|
|
@ -2635,6 +2678,23 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/17/d3/b64c356a907242d719fc668b71befd73324e47ab46c8ebbbede252c154b2/olefile-0.47-py2.py3-none-any.whl", hash = "sha256:543c7da2a7adadf21214938bb79c83ea12b473a4b6ee4ad4bf854e7715e13d1f", size = 114565, upload-time = "2023-12-01T16:22:51.518Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oletools"
|
||||
version = "0.60.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorclass" },
|
||||
{ name = "easygui" },
|
||||
{ name = "msoffcrypto-tool", marker = "(platform_python_implementation != 'PyPy' and sys_platform == 'darwin') or (platform_python_implementation != 'PyPy' and sys_platform == 'win32') or (sys_platform != 'darwin' and sys_platform != 'win32')" },
|
||||
{ name = "olefile" },
|
||||
{ name = "pcodedmp" },
|
||||
{ name = "pyparsing" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5c/2f/037f40e44706d542b94a2312ccc33ee2701ebfc9a83b46b55263d49ce55a/oletools-0.60.2.zip", hash = "sha256:ad452099f4695ffd8855113f453348200d195ee9fa341a09e197d66ee7e0b2c3", size = 3433750, upload-time = "2024-07-02T14:50:38.242Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/ff/05257b7183279b80ecec6333744de23f48f0faeeba46c93e6d13ce835515/oletools-0.60.2-py2.py3-none-any.whl", hash = "sha256:72ad8bd748fd0c4e7b5b4733af770d11543ebb2bf2697455f99f975fcd50cc96", size = 989449, upload-time = "2024-07-02T14:50:29.122Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "onnxruntime"
|
||||
version = "1.20.1"
|
||||
|
|
@ -2683,12 +2743,12 @@ dependencies = [
|
|||
{ name = "boto3" },
|
||||
{ name = "chromadb" },
|
||||
{ name = "colbert-ai" },
|
||||
{ name = "cryptography" },
|
||||
{ name = "ddgs" },
|
||||
{ name = "docker" },
|
||||
{ name = "docx2txt" },
|
||||
{ name = "duckduckgo-search" },
|
||||
{ name = "einops" },
|
||||
{ name = "elasticsearch" },
|
||||
{ name = "extract-msg" },
|
||||
{ name = "fake-useragent" },
|
||||
{ name = "fastapi" },
|
||||
{ name = "faster-whisper" },
|
||||
|
|
@ -2703,7 +2763,6 @@ dependencies = [
|
|||
{ name = "google-genai" },
|
||||
{ name = "google-generativeai" },
|
||||
{ name = "googleapis-common-protos" },
|
||||
{ name = "httpx", extra = ["brotli", "cli", "http2", "socks", "zstd"] },
|
||||
{ name = "langchain" },
|
||||
{ name = "langchain-community" },
|
||||
{ name = "langfuse" },
|
||||
|
|
@ -2728,7 +2787,6 @@ dependencies = [
|
|||
{ name = "playwright" },
|
||||
{ name = "psutil" },
|
||||
{ name = "psycopg2-binary" },
|
||||
{ name = "pycrdt" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "pydub" },
|
||||
{ name = "pyjwt", extra = ["crypto"] },
|
||||
|
|
@ -2767,11 +2825,6 @@ dependencies = [
|
|||
{ name = "youtube-transcript-api" },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
dev = [
|
||||
{ name = "pytest-asyncio" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "accelerate" },
|
||||
|
|
@ -2785,7 +2838,7 @@ requires-dist = [
|
|||
{ name = "asgiref", specifier = "==3.8.1" },
|
||||
{ name = "async-timeout" },
|
||||
{ name = "authlib", specifier = "==1.4.1" },
|
||||
{ name = "azure-ai-documentintelligence", specifier = "==1.0.2" },
|
||||
{ name = "azure-ai-documentintelligence", specifier = "==1.0.0" },
|
||||
{ name = "azure-identity", specifier = "==1.20.0" },
|
||||
{ name = "azure-storage-blob", specifier = "==12.24.1" },
|
||||
{ name = "bcrypt", specifier = "==4.3.0" },
|
||||
|
|
@ -2793,13 +2846,12 @@ requires-dist = [
|
|||
{ name = "boto3", specifier = "==1.35.53" },
|
||||
{ name = "chromadb", specifier = "==0.6.3" },
|
||||
{ name = "colbert-ai", specifier = "==0.2.21" },
|
||||
{ name = "cryptography" },
|
||||
{ name = "ddgs", specifier = "==9.0.0" },
|
||||
{ name = "docker", specifier = "~=7.1.0" },
|
||||
{ name = "docx2txt", specifier = "==0.8" },
|
||||
{ name = "duckduckgo-search", specifier = "==8.0.2" },
|
||||
{ name = "einops", specifier = "==0.8.1" },
|
||||
{ name = "elasticsearch", specifier = "==9.0.1" },
|
||||
{ name = "extract-msg" },
|
||||
{ name = "fake-useragent", specifier = "==2.1.0" },
|
||||
{ name = "fastapi", specifier = "==0.115.7" },
|
||||
{ name = "faster-whisper", specifier = "==1.1.1" },
|
||||
|
|
@ -2814,9 +2866,8 @@ requires-dist = [
|
|||
{ name = "google-genai", specifier = "==1.15.0" },
|
||||
{ name = "google-generativeai", specifier = "==0.8.5" },
|
||||
{ name = "googleapis-common-protos", specifier = "==1.63.2" },
|
||||
{ name = "httpx", extras = ["brotli", "cli", "http2", "socks", "zstd"], specifier = "==0.28.1" },
|
||||
{ name = "langchain", specifier = "==0.3.26" },
|
||||
{ name = "langchain-community", specifier = "==0.3.26" },
|
||||
{ name = "langchain", specifier = "==0.3.24" },
|
||||
{ name = "langchain-community", specifier = "==0.3.23" },
|
||||
{ name = "langfuse", specifier = "==2.44.0" },
|
||||
{ name = "ldap3", specifier = "==2.9.1" },
|
||||
{ name = "loguru", specifier = "==0.7.3" },
|
||||
|
|
@ -2834,13 +2885,12 @@ requires-dist = [
|
|||
{ name = "peewee", specifier = "==3.18.1" },
|
||||
{ name = "peewee-migrate", specifier = "==1.12.2" },
|
||||
{ name = "pgvector", specifier = "==0.4.0" },
|
||||
{ name = "pillow", specifier = "==11.2.1" },
|
||||
{ name = "pillow", specifier = "==11.1.0" },
|
||||
{ name = "pinecone", specifier = "==6.0.2" },
|
||||
{ name = "playwright", specifier = "==1.49.1" },
|
||||
{ name = "psutil" },
|
||||
{ name = "psycopg2-binary", specifier = "==2.9.9" },
|
||||
{ name = "pycrdt", specifier = "==0.12.25" },
|
||||
{ name = "pydantic", specifier = "==2.11.7" },
|
||||
{ name = "pydantic", specifier = "==2.10.6" },
|
||||
{ name = "pydub" },
|
||||
{ name = "pyjwt", extras = ["crypto"], specifier = "==2.10.1" },
|
||||
{ name = "pymdown-extensions", specifier = "==10.14.2" },
|
||||
|
|
@ -2853,15 +2903,15 @@ requires-dist = [
|
|||
{ name = "pytest-docker", specifier = "~=3.1.1" },
|
||||
{ name = "python-jose", specifier = "==3.4.0" },
|
||||
{ name = "python-multipart", specifier = "==0.0.20" },
|
||||
{ name = "python-pptx", specifier = "==1.0.2" },
|
||||
{ name = "python-pptx", specifier = "==1.0.0" },
|
||||
{ name = "python-socketio", specifier = "==5.13.0" },
|
||||
{ name = "pytube", specifier = "==15.0.0" },
|
||||
{ name = "pyxlsb", specifier = "==1.0.10" },
|
||||
{ name = "qdrant-client", specifier = "==1.14.3" },
|
||||
{ name = "qdrant-client", specifier = "~=1.12.0" },
|
||||
{ name = "rank-bm25", specifier = "==0.2.2" },
|
||||
{ name = "rapidocr-onnxruntime", specifier = "==1.4.4" },
|
||||
{ name = "redis" },
|
||||
{ name = "requests", specifier = "==2.32.4" },
|
||||
{ name = "requests", specifier = "==2.32.3" },
|
||||
{ name = "restrictedpython", specifier = "==8.0" },
|
||||
{ name = "sentence-transformers", specifier = "==4.1.0" },
|
||||
{ name = "sentencepiece" },
|
||||
|
|
@ -2872,15 +2922,12 @@ requires-dist = [
|
|||
{ name = "tiktoken" },
|
||||
{ name = "transformers" },
|
||||
{ name = "unstructured", specifier = "==0.16.17" },
|
||||
{ name = "uvicorn", extras = ["standard"], specifier = "==0.34.2" },
|
||||
{ name = "validators", specifier = "==0.35.0" },
|
||||
{ name = "uvicorn", extras = ["standard"], specifier = "==0.34.0" },
|
||||
{ name = "validators", specifier = "==0.34.0" },
|
||||
{ name = "xlrd", specifier = "==2.0.1" },
|
||||
{ name = "youtube-transcript-api", specifier = "==1.1.0" },
|
||||
{ name = "youtube-transcript-api", specifier = "==1.0.3" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [{ name = "pytest-asyncio", specifier = ">=1.0.0" }]
|
||||
|
||||
[[package]]
|
||||
name = "openai"
|
||||
version = "1.59.7"
|
||||
|
|
@ -3211,6 +3258,19 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pcodedmp"
|
||||
version = "1.2.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "oletools" },
|
||||
{ name = "win-unicode-console", marker = "platform_python_implementation != 'PyPy' and sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3d/20/6d461e29135f474408d0d7f95b2456a9ba245560768ee51b788af10f7429/pcodedmp-1.2.6.tar.gz", hash = "sha256:025f8c809a126f45a082ffa820893e6a8d990d9d7ddb68694b5a9f0a6dbcd955", size = 35549, upload-time = "2019-07-30T18:05:42.516Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/72/b380fb5c89d89c3afafac8cf02a71a45f4f4a4f35531ca949a34683962d1/pcodedmp-1.2.6-py2.py3-none-any.whl", hash = "sha256:4441f7c0ab4cbda27bd4668db3b14f36261d86e5059ce06c0828602cbe1c4278", size = 30939, upload-time = "2019-07-30T18:05:40.483Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "peewee"
|
||||
version = "3.18.1"
|
||||
|
|
@ -3244,39 +3304,32 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "pillow"
|
||||
version = "11.2.1"
|
||||
version = "11.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/af/cb/bb5c01fcd2a69335b86c22142b2bccfc3464087efb7fd382eee5ffc7fdf7/pillow-11.2.1.tar.gz", hash = "sha256:a64dd61998416367b7ef979b73d3a85853ba9bec4c2925f74e588879a58716b6", size = 47026707, upload-time = "2025-04-12T17:50:03.289Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f3/af/c097e544e7bd278333db77933e535098c259609c4eb3b85381109602fb5b/pillow-11.1.0.tar.gz", hash = "sha256:368da70808b36d73b4b390a8ffac11069f8a5c85f29eff1f1b01bcf3ef5b2a20", size = 46742715, upload-time = "2025-01-02T08:13:58.407Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/68/08/3fbf4b98924c73037a8e8b4c2c774784805e0fb4ebca6c5bb60795c40125/pillow-11.2.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:35ca289f712ccfc699508c4658a1d14652e8033e9b69839edf83cbdd0ba39e70", size = 3198450, upload-time = "2025-04-12T17:47:37.135Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/92/6505b1af3d2849d5e714fc75ba9e69b7255c05ee42383a35a4d58f576b16/pillow-11.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0409af9f829f87a2dfb7e259f78f317a5351f2045158be321fd135973fff7bf", size = 3030550, upload-time = "2025-04-12T17:47:39.345Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/8c/ac2f99d2a70ff966bc7eb13dacacfaab57c0549b2ffb351b6537c7840b12/pillow-11.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4e5c5edee874dce4f653dbe59db7c73a600119fbea8d31f53423586ee2aafd7", size = 4415018, upload-time = "2025-04-12T17:47:41.128Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/e3/0a58b5d838687f40891fff9cbaf8669f90c96b64dc8f91f87894413856c6/pillow-11.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b93a07e76d13bff9444f1a029e0af2964e654bfc2e2c2d46bfd080df5ad5f3d8", size = 4498006, upload-time = "2025-04-12T17:47:42.912Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/f5/6ba14718135f08fbfa33308efe027dd02b781d3f1d5c471444a395933aac/pillow-11.2.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:e6def7eed9e7fa90fde255afaf08060dc4b343bbe524a8f69bdd2a2f0018f600", size = 4517773, upload-time = "2025-04-12T17:47:44.611Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/f2/805ad600fc59ebe4f1ba6129cd3a75fb0da126975c8579b8f57abeb61e80/pillow-11.2.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:8f4f3724c068be008c08257207210c138d5f3731af6c155a81c2b09a9eb3a788", size = 4607069, upload-time = "2025-04-12T17:47:46.46Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/6b/4ef8a288b4bb2e0180cba13ca0a519fa27aa982875882392b65131401099/pillow-11.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a0a6709b47019dff32e678bc12c63008311b82b9327613f534e496dacaefb71e", size = 4583460, upload-time = "2025-04-12T17:47:49.255Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/ae/f29c705a09cbc9e2a456590816e5c234382ae5d32584f451c3eb41a62062/pillow-11.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f6b0c664ccb879109ee3ca702a9272d877f4fcd21e5eb63c26422fd6e415365e", size = 4661304, upload-time = "2025-04-12T17:47:51.067Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/1a/c8217b6f2f73794a5e219fbad087701f412337ae6dbb956db37d69a9bc43/pillow-11.2.1-cp311-cp311-win32.whl", hash = "sha256:cc5d875d56e49f112b6def6813c4e3d3036d269c008bf8aef72cd08d20ca6df6", size = 2331809, upload-time = "2025-04-12T17:47:54.425Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/72/25a8f40170dc262e86e90f37cb72cb3de5e307f75bf4b02535a61afcd519/pillow-11.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:0f5c7eda47bf8e3c8a283762cab94e496ba977a420868cb819159980b6709193", size = 2676338, upload-time = "2025-04-12T17:47:56.535Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/9e/76825e39efee61efea258b479391ca77d64dbd9e5804e4ad0fa453b4ba55/pillow-11.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:4d375eb838755f2528ac8cbc926c3e31cc49ca4ad0cf79cff48b20e30634a4a7", size = 2414918, upload-time = "2025-04-12T17:47:58.217Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/40/052610b15a1b8961f52537cc8326ca6a881408bc2bdad0d852edeb6ed33b/pillow-11.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:78afba22027b4accef10dbd5eed84425930ba41b3ea0a86fa8d20baaf19d807f", size = 3190185, upload-time = "2025-04-12T17:48:00.417Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/7e/b86dbd35a5f938632093dc40d1682874c33dcfe832558fc80ca56bfcb774/pillow-11.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78092232a4ab376a35d68c4e6d5e00dfd73454bd12b230420025fbe178ee3b0b", size = 3030306, upload-time = "2025-04-12T17:48:02.391Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/5c/467a161f9ed53e5eab51a42923c33051bf8d1a2af4626ac04f5166e58e0c/pillow-11.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25a5f306095c6780c52e6bbb6109624b95c5b18e40aab1c3041da3e9e0cd3e2d", size = 4416121, upload-time = "2025-04-12T17:48:04.554Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/73/972b7742e38ae0e2ac76ab137ca6005dcf877480da0d9d61d93b613065b4/pillow-11.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c7b29dbd4281923a2bfe562acb734cee96bbb129e96e6972d315ed9f232bef4", size = 4501707, upload-time = "2025-04-12T17:48:06.831Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/3a/427e4cb0b9e177efbc1a84798ed20498c4f233abde003c06d2650a6d60cb/pillow-11.2.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3e645b020f3209a0181a418bffe7b4a93171eef6c4ef6cc20980b30bebf17b7d", size = 4522921, upload-time = "2025-04-12T17:48:09.229Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/7c/d8b1330458e4d2f3f45d9508796d7caf0c0d3764c00c823d10f6f1a3b76d/pillow-11.2.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b2dbea1012ccb784a65349f57bbc93730b96e85b42e9bf7b01ef40443db720b4", size = 4612523, upload-time = "2025-04-12T17:48:11.631Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/2f/65738384e0b1acf451de5a573d8153fe84103772d139e1e0bdf1596be2ea/pillow-11.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:da3104c57bbd72948d75f6a9389e6727d2ab6333c3617f0a89d72d4940aa0443", size = 4587836, upload-time = "2025-04-12T17:48:13.592Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/c5/e795c9f2ddf3debb2dedd0df889f2fe4b053308bb59a3cc02a0cd144d641/pillow-11.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:598174aef4589af795f66f9caab87ba4ff860ce08cd5bb447c6fc553ffee603c", size = 4669390, upload-time = "2025-04-12T17:48:15.938Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/ae/ca0099a3995976a9fce2f423166f7bff9b12244afdc7520f6ed38911539a/pillow-11.2.1-cp312-cp312-win32.whl", hash = "sha256:1d535df14716e7f8776b9e7fee118576d65572b4aad3ed639be9e4fa88a1cad3", size = 2332309, upload-time = "2025-04-12T17:48:17.885Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/18/24bff2ad716257fc03da964c5e8f05d9790a779a8895d6566e493ccf0189/pillow-11.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:14e33b28bf17c7a38eede290f77db7c664e4eb01f7869e37fa98a5aa95978941", size = 2676768, upload-time = "2025-04-12T17:48:19.655Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/bb/e8d656c9543276517ee40184aaa39dcb41e683bca121022f9323ae11b39d/pillow-11.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:21e1470ac9e5739ff880c211fc3af01e3ae505859392bf65458c224d0bf283eb", size = 2415087, upload-time = "2025-04-12T17:48:21.991Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/ad/2613c04633c7257d9481ab21d6b5364b59fc5d75faafd7cb8693523945a3/pillow-11.2.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:80f1df8dbe9572b4b7abdfa17eb5d78dd620b1d55d9e25f834efdbee872d3aed", size = 3181734, upload-time = "2025-04-12T17:49:46.789Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/fd/dcdda4471ed667de57bb5405bb42d751e6cfdd4011a12c248b455c778e03/pillow-11.2.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ea926cfbc3957090becbcbbb65ad177161a2ff2ad578b5a6ec9bb1e1cd78753c", size = 2999841, upload-time = "2025-04-12T17:49:48.812Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/89/8a2536e95e77432833f0db6fd72a8d310c8e4272a04461fb833eb021bf94/pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:738db0e0941ca0376804d4de6a782c005245264edaa253ffce24e5a15cbdc7bd", size = 3437470, upload-time = "2025-04-12T17:49:50.831Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/8f/abd47b73c60712f88e9eda32baced7bfc3e9bd6a7619bb64b93acff28c3e/pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db98ab6565c69082ec9b0d4e40dd9f6181dab0dd236d26f7a50b8b9bfbd5076", size = 3460013, upload-time = "2025-04-12T17:49:53.278Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/20/5c0a0aa83b213b7a07ec01e71a3d6ea2cf4ad1d2c686cc0168173b6089e7/pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:036e53f4170e270ddb8797d4c590e6dd14d28e15c7da375c18978045f7e6c37b", size = 3527165, upload-time = "2025-04-12T17:49:55.164Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/0e/2abab98a72202d91146abc839e10c14f7cf36166f12838ea0c4db3ca6ecb/pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:14f73f7c291279bd65fda51ee87affd7c1e097709f7fdd0188957a16c264601f", size = 3571586, upload-time = "2025-04-12T17:49:57.171Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/2c/5e05f58658cf49b6667762cca03d6e7d85cededde2caf2ab37b81f80e574/pillow-11.2.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:208653868d5c9ecc2b327f9b9ef34e0e42a4cdd172c2988fd81d62d2bc9bc044", size = 2674751, upload-time = "2025-04-12T17:49:59.628Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/d6/2000bfd8d5414fb70cbbe52c8332f2283ff30ed66a9cde42716c8ecbe22c/pillow-11.1.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:e06695e0326d05b06833b40b7ef477e475d0b1ba3a6d27da1bb48c23209bf457", size = 3229968, upload-time = "2025-01-02T08:10:48.172Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/45/3fe487010dd9ce0a06adf9b8ff4f273cc0a44536e234b0fad3532a42c15b/pillow-11.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96f82000e12f23e4f29346e42702b6ed9a2f2fea34a740dd5ffffcc8c539eb35", size = 3101806, upload-time = "2025-01-02T08:10:50.981Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/72/776b3629c47d9d5f1c160113158a7a7ad177688d3a1159cd3b62ded5a33a/pillow-11.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3cd561ded2cf2bbae44d4605837221b987c216cff94f49dfeed63488bb228d2", size = 4322283, upload-time = "2025-01-02T08:10:54.724Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/c2/e25199e7e4e71d64eeb869f5b72c7ddec70e0a87926398785ab944d92375/pillow-11.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f189805c8be5ca5add39e6f899e6ce2ed824e65fb45f3c28cb2841911da19070", size = 4402945, upload-time = "2025-01-02T08:10:57.376Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/ed/51d6136c9d5911f78632b1b86c45241c712c5a80ed7fa7f9120a5dff1eba/pillow-11.1.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:dd0052e9db3474df30433f83a71b9b23bd9e4ef1de13d92df21a52c0303b8ab6", size = 4361228, upload-time = "2025-01-02T08:11:02.374Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/a4/fbfe9d5581d7b111b28f1d8c2762dee92e9821bb209af9fa83c940e507a0/pillow-11.1.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:837060a8599b8f5d402e97197d4924f05a2e0d68756998345c829c33186217b1", size = 4484021, upload-time = "2025-01-02T08:11:04.431Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/db/0b3c1a5018117f3c1d4df671fb8e47d08937f27519e8614bbe86153b65a5/pillow-11.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:aa8dd43daa836b9a8128dbe7d923423e5ad86f50a7a14dc688194b7be5c0dea2", size = 4287449, upload-time = "2025-01-02T08:11:07.412Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/58/bc128da7fea8c89fc85e09f773c4901e95b5936000e6f303222490c052f3/pillow-11.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0a2f91f8a8b367e7a57c6e91cd25af510168091fb89ec5146003e424e1558a96", size = 4419972, upload-time = "2025-01-02T08:11:09.508Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/bb/58f34379bde9fe197f51841c5bbe8830c28bbb6d3801f16a83b8f2ad37df/pillow-11.1.0-cp311-cp311-win32.whl", hash = "sha256:c12fc111ef090845de2bb15009372175d76ac99969bdf31e2ce9b42e4b8cd88f", size = 2291201, upload-time = "2025-01-02T08:11:13.056Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/c6/fce9255272bcf0c39e15abd2f8fd8429a954cf344469eaceb9d0d1366913/pillow-11.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fbd43429d0d7ed6533b25fc993861b8fd512c42d04514a0dd6337fb3ccf22761", size = 2625686, upload-time = "2025-01-02T08:11:16.547Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/52/8ba066d569d932365509054859f74f2a9abee273edcef5cd75e4bc3e831e/pillow-11.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:f7955ecf5609dee9442cbface754f2c6e541d9e6eda87fad7f7a989b0bdb9d71", size = 2375194, upload-time = "2025-01-02T08:11:19.897Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/20/9ce6ed62c91c073fcaa23d216e68289e19d95fb8188b9fb7a63d36771db8/pillow-11.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2062ffb1d36544d42fcaa277b069c88b01bb7298f4efa06731a7fd6cc290b81a", size = 3226818, upload-time = "2025-01-02T08:11:22.518Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/d8/f6004d98579a2596c098d1e30d10b248798cceff82d2b77aa914875bfea1/pillow-11.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a85b653980faad27e88b141348707ceeef8a1186f75ecc600c395dcac19f385b", size = 3101662, upload-time = "2025-01-02T08:11:25.19Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/d9/892e705f90051c7a2574d9f24579c9e100c828700d78a63239676f960b74/pillow-11.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9409c080586d1f683df3f184f20e36fb647f2e0bc3988094d4fd8c9f4eb1b3b3", size = 4329317, upload-time = "2025-01-02T08:11:30.371Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/aa/7f29711f26680eab0bcd3ecdd6d23ed6bce180d82e3f6380fb7ae35fcf3b/pillow-11.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fdadc077553621911f27ce206ffcbec7d3f8d7b50e0da39f10997e8e2bb7f6a", size = 4412999, upload-time = "2025-01-02T08:11:33.499Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/c4/8f0fe3b9e0f7196f6d0bbb151f9fba323d72a41da068610c4c960b16632a/pillow-11.1.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:93a18841d09bcdd774dcdc308e4537e1f867b3dec059c131fde0327899734aa1", size = 4368819, upload-time = "2025-01-02T08:11:37.304Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/0d/84200ed6a871ce386ddc82904bfadc0c6b28b0c0ec78176871a4679e40b3/pillow-11.1.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:9aa9aeddeed452b2f616ff5507459e7bab436916ccb10961c4a382cd3e03f47f", size = 4496081, upload-time = "2025-01-02T08:11:39.598Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/9c/9bcd66f714d7e25b64118e3952d52841a4babc6d97b6d28e2261c52045d4/pillow-11.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3cdcdb0b896e981678eee140d882b70092dac83ac1cdf6b3a60e2216a73f2b91", size = 4296513, upload-time = "2025-01-02T08:11:43.083Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/61/ada2a226e22da011b45f7104c95ebda1b63dcbb0c378ad0f7c2a710f8fd2/pillow-11.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:36ba10b9cb413e7c7dfa3e189aba252deee0602c86c309799da5a74009ac7a1c", size = 4431298, upload-time = "2025-01-02T08:11:46.626Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/c4/fc6e86750523f367923522014b821c11ebc5ad402e659d8c9d09b3c9d70c/pillow-11.1.0-cp312-cp312-win32.whl", hash = "sha256:cfd5cd998c2e36a862d0e27b2df63237e67273f2fc78f47445b14e73a810e7e6", size = 2291630, upload-time = "2025-01-02T08:11:49.401Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/5c/2104299949b9d504baf3f4d35f73dbd14ef31bbd1ddc2c1b66a5b7dfda44/pillow-11.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:a697cd8ba0383bba3d2d3ada02b34ed268cb548b369943cd349007730c92bddf", size = 2626369, upload-time = "2025-01-02T08:11:52.02Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/f3/9b18362206b244167c958984b57c7f70a0289bfb59a530dd8af5f699b910/pillow-11.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:4dd43a78897793f60766563969442020e90eb7847463eca901e41ba186a7d4a5", size = 2375240, upload-time = "2025-01-02T08:11:56.193Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -3579,96 +3632,57 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pycrdt"
|
||||
version = "0.12.25"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f4/1e/f7f507471fb7eab34c48198f2ca665d4983b7257acff4ca0f76db619d3e3/pycrdt-0.12.25.tar.gz", hash = "sha256:b671565064b67e94b80b294467f70cc584781a6246c72206e451eeb05429e847", size = 76689, upload-time = "2025-07-09T13:13:27.622Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/2f/3f58aa472ee2e91f175fd85f90a58ba3207261911ce0f0be1f5d4e66f5c6/pycrdt-0.12.25-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:685651949205bcb4cf91f00dfafc2b932048a54d894e812efd1e2dcec1f80ed4", size = 1700346, upload-time = "2025-07-09T13:11:55.828Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/0a/0bf8ba1997662543703f35a761db7c55e2c1557aa88ceef7464a983c2bea/pycrdt-0.12.25-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:86583128b531f8adc81b48e7cf4e363a4ecbc906dfdf497b498c9c3adf748f17", size = 915262, upload-time = "2025-07-09T13:11:57.442Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/3c/f9b7e376bc71801f404ab3430d361e8eec402d427a5361e2f43558968ca9/pycrdt-0.12.25-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:171656c8ec399a404fd63193851ebaa115035dbaaca14e53bb2edaea01f36b2b", size = 953327, upload-time = "2025-07-09T13:11:58.911Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/0f/754560ea300feefd00dca2c5ce4545d8bf143f12f83c53b61f31a2c0869d/pycrdt-0.12.25-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5be501544b37d48017dc07ac8aba631e07abf92657f43cb160ba558879b81635", size = 1126390, upload-time = "2025-07-09T13:12:00.953Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/3b/f1911138c383598dd2ae3533288ed91e407f3546420e61fca0a9054df1de/pycrdt-0.12.25-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81d51c9c949e85548ed8ba7f3dae19cc2c6ed71b5126ff7545e2e7248967757e", size = 991052, upload-time = "2025-07-09T13:12:02.835Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/f0/b26c35a2e20137c94dbe4213955cd0eace7460d245535737cf1a4491edf1/pycrdt-0.12.25-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:537994ef5a43de71815cbce1e5dac7490af67ae429311dc9d3d307bbd9574bd3", size = 938340, upload-time = "2025-07-09T13:12:04.484Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/f8/9ac043f550c6899b7138449558da92ce36a29a1b100c6c7de177eeefac01/pycrdt-0.12.25-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64f0e566b447c5239a1be8bfe79d8981594c34d9db4544a11c1bdf7f228aa877", size = 1029702, upload-time = "2025-07-09T13:12:06.099Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/fc/dd05f7df18a83de6cf8ba2205ecaaad8461e419b0aa224ac4b316f3f5629/pycrdt-0.12.25-cp311-cp311-win32.whl", hash = "sha256:586e18559ea2f9384685714bb3b5dd3de3392e11d96db5ef9e90d91416284700", size = 684134, upload-time = "2025-07-09T13:12:07.939Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/87/c6d7e70b3c2e6256f2b125051949a573c9ae7a526f4a028494ad680f364f/pycrdt-0.12.25-cp311-cp311-win_amd64.whl", hash = "sha256:2492ffac459e0f305978496059a6020efe4860fd65988f4e9ac75692d37ac9b5", size = 729392, upload-time = "2025-07-09T13:12:09.569Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/b7/7657a7a88dacb3bd4ef7fb1e2c6493c7b18e404bc961f54bc61cd6aa229c/pycrdt-0.12.25-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:f4fa769124e8bee08e8e3b0dd40bce4b576e44f8d4472fc65c30c9188f3b986e", size = 1690725, upload-time = "2025-07-09T13:12:11.59Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/50/775e09cc9961ddbb2bfac2737ad067b8a63e990315ea5ed99311f29e470c/pycrdt-0.12.25-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d39604c225a1e88ec82a7d9376a58642747bcafc32412313a86fda48a1cf1b8d", size = 919353, upload-time = "2025-07-09T13:12:13.305Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/03/16cf1d529e033002c292e9c43d3b4993dbc374e73591459dc348f0e7a42e/pycrdt-0.12.25-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a719a452901964cf4d98362d34ee32e650e481dedaf300c63f64381ef6f9ce5f", size = 953210, upload-time = "2025-07-09T13:12:14.912Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/e5/0574108735eee7c33647c323a9f5f9bda3090c18a7567083007355fb3478/pycrdt-0.12.25-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c2ba33d86ec754cd6fd46fd10bf29404b0db8e2e805178f91955f5cdd8c5299e", size = 1134521, upload-time = "2025-07-09T13:12:16.523Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/2d/a2b224a4324be9213ed69f8069c9e4744c1e24fe1c9ec122c29b1b831c20/pycrdt-0.12.25-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a2e9f5b7b04c9a78edccbc8ff577de07d71b7beb108aacc7dfa9fa8b3ca46878", size = 990679, upload-time = "2025-07-09T13:12:18.086Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/f9/54ed5284aabe582191927ad63ec82dbaa6198b2f4721ca65e386d37dec44/pycrdt-0.12.25-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9cb2a5c12eaab4e56b8c781b4f9ca6a450109d2a2295d9331fef9a2361b7211a", size = 943790, upload-time = "2025-07-09T13:12:19.871Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/f0/ff6107190d2b4122bd1c046ee71a054f0f45ea2c9d312b032c010909f2bc/pycrdt-0.12.25-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d4f5630d362c1f6b3c8df605bc0841ed7a6d090217a17e117a82be14efa9a114", size = 1028099, upload-time = "2025-07-09T13:12:21.95Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/65/a21ca1348cf1a09e9ba01ff64d042c51b0d1d1e419ae4a3ed24220da9a35/pycrdt-0.12.25-cp312-cp312-win32.whl", hash = "sha256:08dd03bec5274d4d27895f10eb9b83fff9c30313b25103d9fc4ca76d14eb4e30", size = 682156, upload-time = "2025-07-09T13:12:23.987Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/ae/6c1f3a1df2ad70d2f1fcf4f0dbc9bb24ab27562f85b966e7d66e4f45ac21/pycrdt-0.12.25-cp312-cp312-win_amd64.whl", hash = "sha256:86000c3356ef7833e0c8694da1f71912d8fa0bba9ac071060ef227d950cf012f", size = 732399, upload-time = "2025-07-09T13:12:25.581Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "2.11.7"
|
||||
version = "2.10.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "annotated-types" },
|
||||
{ name = "pydantic-core" },
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "typing-inspection" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350, upload-time = "2025-06-14T08:33:17.137Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b7/ae/d5220c5c52b158b1de7ca89fc5edb72f304a70a4c540c84c8844bf4008de/pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236", size = 761681, upload-time = "2025-01-24T01:42:12.693Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782, upload-time = "2025-06-14T08:33:14.905Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/3c/8cc1cc84deffa6e25d2d0c688ebb80635dfdbf1dbea3e30c541c8cf4d860/pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584", size = 431696, upload-time = "2025-01-24T01:42:10.371Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-core"
|
||||
version = "2.33.2"
|
||||
version = "2.27.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443, upload-time = "2024-12-18T11:31:54.917Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/8d/71db63483d518cbbf290261a1fc2839d17ff89fce7089e08cad07ccfce67/pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7", size = 2028584, upload-time = "2025-04-23T18:31:03.106Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/2f/3cfa7244ae292dd850989f328722d2aef313f74ffc471184dc509e1e4e5a/pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246", size = 1855071, upload-time = "2025-04-23T18:31:04.621Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/d3/4ae42d33f5e3f50dd467761304be2fa0a9417fbf09735bc2cce003480f2a/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f", size = 1897823, upload-time = "2025-04-23T18:31:06.377Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/f3/aa5976e8352b7695ff808599794b1fba2a9ae2ee954a3426855935799488/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc", size = 1983792, upload-time = "2025-04-23T18:31:07.93Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/7a/cda9b5a23c552037717f2b2a5257e9b2bfe45e687386df9591eff7b46d28/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de", size = 2136338, upload-time = "2025-04-23T18:31:09.283Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/9f/b8f9ec8dd1417eb9da784e91e1667d58a2a4a7b7b34cf4af765ef663a7e5/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a", size = 2730998, upload-time = "2025-04-23T18:31:11.7Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/bc/cd720e078576bdb8255d5032c5d63ee5c0bf4b7173dd955185a1d658c456/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef", size = 2003200, upload-time = "2025-04-23T18:31:13.536Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/22/3602b895ee2cd29d11a2b349372446ae9727c32e78a94b3d588a40fdf187/pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e", size = 2113890, upload-time = "2025-04-23T18:31:15.011Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/e6/e3c5908c03cf00d629eb38393a98fccc38ee0ce8ecce32f69fc7d7b558a7/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d", size = 2073359, upload-time = "2025-04-23T18:31:16.393Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/e7/6a36a07c59ebefc8777d1ffdaf5ae71b06b21952582e4b07eba88a421c79/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30", size = 2245883, upload-time = "2025-04-23T18:31:17.892Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/3f/59b3187aaa6cc0c1e6616e8045b284de2b6a87b027cce2ffcea073adf1d2/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf", size = 2241074, upload-time = "2025-04-23T18:31:19.205Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/ed/55532bb88f674d5d8f67ab121a2a13c385df382de2a1677f30ad385f7438/pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51", size = 1910538, upload-time = "2025-04-23T18:31:20.541Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/1b/25b7cccd4519c0b23c2dd636ad39d381abf113085ce4f7bec2b0dc755eb1/pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab", size = 1952909, upload-time = "2025-04-23T18:31:22.371Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/a9/d809358e49126438055884c4366a1f6227f0f84f635a9014e2deb9b9de54/pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65", size = 1897786, upload-time = "2025-04-23T18:31:24.161Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000, upload-time = "2025-04-23T18:31:25.863Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996, upload-time = "2025-04-23T18:31:27.341Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957, upload-time = "2025-04-23T18:31:28.956Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199, upload-time = "2025-04-23T18:31:31.025Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296, upload-time = "2025-04-23T18:31:32.514Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109, upload-time = "2025-04-23T18:31:33.958Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028, upload-time = "2025-04-23T18:31:39.095Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044, upload-time = "2025-04-23T18:31:41.034Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881, upload-time = "2025-04-23T18:31:42.757Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034, upload-time = "2025-04-23T18:31:44.304Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187, upload-time = "2025-04-23T18:31:45.891Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628, upload-time = "2025-04-23T18:31:47.819Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866, upload-time = "2025-04-23T18:31:49.635Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894, upload-time = "2025-04-23T18:31:51.609Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/27/d4ae6487d73948d6f20dddcd94be4ea43e74349b56eba82e9bdee2d7494c/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8", size = 2025200, upload-time = "2025-04-23T18:33:14.199Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/b8/b3cb95375f05d33801024079b9392a5ab45267a63400bf1866e7ce0f0de4/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593", size = 1859123, upload-time = "2025-04-23T18:33:16.555Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/bc/0d0b5adeda59a261cd30a1235a445bf55c7e46ae44aea28f7bd6ed46e091/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612", size = 1892852, upload-time = "2025-04-23T18:33:18.513Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/11/d37bdebbda2e449cb3f519f6ce950927b56d62f0b84fd9cb9e372a26a3d5/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7", size = 2067484, upload-time = "2025-04-23T18:33:20.475Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/55/1f95f0a05ce72ecb02a8a8a1c3be0579bbc29b1d5ab68f1378b7bebc5057/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e", size = 2108896, upload-time = "2025-04-23T18:33:22.501Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/89/2b2de6c81fa131f423246a9109d7b2a375e83968ad0800d6e57d0574629b/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8", size = 2069475, upload-time = "2025-04-23T18:33:24.528Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/e9/1f7efbe20d0b2b10f6718944b5d8ece9152390904f29a78e68d4e7961159/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf", size = 2239013, upload-time = "2025-04-23T18:33:26.621Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/b2/5309c905a93811524a49b4e031e9851a6b00ff0fb668794472ea7746b448/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb", size = 2238715, upload-time = "2025-04-23T18:33:28.656Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/56/8a7ca5d2cd2cda1d245d34b1c9a942920a718082ae8e54e5f3e5a58b7add/pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1", size = 2066757, upload-time = "2025-04-23T18:33:30.645Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/89/f3450af9d09d44eea1f2c369f49e8f181d742f28220f88cc4dfaae91ea6e/pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc", size = 1893421, upload-time = "2024-12-18T11:27:55.409Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/e3/71fe85af2021f3f386da42d291412e5baf6ce7716bd7101ea49c810eda90/pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7", size = 1814998, upload-time = "2024-12-18T11:27:57.252Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/3c/724039e0d848fd69dbf5806894e26479577316c6f0f112bacaf67aa889ac/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15", size = 1826167, upload-time = "2024-12-18T11:27:59.146Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/5b/1b29e8c1fb5f3199a9a57c1452004ff39f494bbe9bdbe9a81e18172e40d3/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306", size = 1865071, upload-time = "2024-12-18T11:28:02.625Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/6c/3985203863d76bb7d7266e36970d7e3b6385148c18a68cc8915fd8c84d57/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99", size = 2036244, upload-time = "2024-12-18T11:28:04.442Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/41/f15316858a246b5d723f7d7f599f79e37493b2e84bfc789e58d88c209f8a/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459", size = 2737470, upload-time = "2024-12-18T11:28:07.679Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/7c/b860618c25678bbd6d1d99dbdfdf0510ccb50790099b963ff78a124b754f/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048", size = 1992291, upload-time = "2024-12-18T11:28:10.297Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/73/42c3742a391eccbeab39f15213ecda3104ae8682ba3c0c28069fbcb8c10d/pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d", size = 1994613, upload-time = "2024-12-18T11:28:13.362Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/7a/941e89096d1175d56f59340f3a8ebaf20762fef222c298ea96d36a6328c5/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b", size = 2002355, upload-time = "2024-12-18T11:28:16.587Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/95/2359937a73d49e336a5a19848713555605d4d8d6940c3ec6c6c0ca4dcf25/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474", size = 2126661, upload-time = "2024-12-18T11:28:18.407Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/4c/ca02b7bdb6012a1adef21a50625b14f43ed4d11f1fc237f9d7490aa5078c/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6", size = 2153261, upload-time = "2024-12-18T11:28:21.471Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/9d/a241db83f973049a1092a079272ffe2e3e82e98561ef6214ab53fe53b1c7/pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c", size = 1812361, upload-time = "2024-12-18T11:28:23.53Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/ef/013f07248041b74abd48a385e2110aa3a9bbfef0fbd97d4e6d07d2f5b89a/pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc", size = 1982484, upload-time = "2024-12-18T11:28:25.391Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/1c/16b3a3e3398fd29dca77cea0a1d998d6bde3902fa2706985191e2313cc76/pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4", size = 1867102, upload-time = "2024-12-18T11:28:28.593Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/74/51c8a5482ca447871c93e142d9d4a92ead74de6c8dc5e66733e22c9bba89/pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0", size = 1893127, upload-time = "2024-12-18T11:28:30.346Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/f3/c97e80721735868313c58b89d2de85fa80fe8dfeeed84dc51598b92a135e/pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef", size = 1811340, upload-time = "2024-12-18T11:28:32.521Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/91/840ec1375e686dbae1bd80a9e46c26a1e0083e1186abc610efa3d9a36180/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7", size = 1822900, upload-time = "2024-12-18T11:28:34.507Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/31/4240bc96025035500c18adc149aa6ffdf1a0062a4b525c932065ceb4d868/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934", size = 1869177, upload-time = "2024-12-18T11:28:36.488Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/20/02fbaadb7808be578317015c462655c317a77a7c8f0ef274bc016a784c54/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6", size = 2038046, upload-time = "2024-12-18T11:28:39.409Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/86/7f306b904e6c9eccf0668248b3f272090e49c275bc488a7b88b0823444a4/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c", size = 2685386, upload-time = "2024-12-18T11:28:41.221Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/f0/49129b27c43396581a635d8710dae54a791b17dfc50c70164866bbf865e3/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2", size = 1997060, upload-time = "2024-12-18T11:28:44.709Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/0f/943b4af7cd416c477fd40b187036c4f89b416a33d3cc0ab7b82708a667aa/pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4", size = 2004870, upload-time = "2024-12-18T11:28:46.839Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/40/aea70b5b1a63911c53a4c8117c0a828d6790483f858041f47bab0b779f44/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3", size = 1999822, upload-time = "2024-12-18T11:28:48.896Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/b3/807b94fd337d58effc5498fd1a7a4d9d59af4133e83e32ae39a96fddec9d/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4", size = 2130364, upload-time = "2024-12-18T11:28:50.755Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/df/791c827cd4ee6efd59248dca9369fb35e80a9484462c33c6649a8d02b565/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57", size = 2158303, upload-time = "2024-12-18T11:28:54.122Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/67/4e197c300976af185b7cef4c02203e175fb127e414125916bf1128b639a9/pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc", size = 1834064, upload-time = "2024-12-18T11:28:56.074Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/ea/cd7209a889163b8dcca139fe32b9687dd05249161a3edda62860430457a5/pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9", size = 1989046, upload-time = "2024-12-18T11:28:58.107Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/49/c54baab2f4658c26ac633d798dab66b4c3a9bbf47cff5284e9c182f4137a/pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b", size = 1885092, upload-time = "2024-12-18T11:29:01.335Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -3863,18 +3877,6 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/11/92/76a1c94d3afee238333bc0a42b82935dd8f9cf8ce9e336ff87ee14d9e1cf/pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6", size = 343083, upload-time = "2024-12-01T12:54:19.735Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest-asyncio"
|
||||
version = "1.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pytest" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d0/d4/14f53324cb1a6381bef29d698987625d80052bb33932d8e7cbf9b337b17c/pytest_asyncio-1.0.0.tar.gz", hash = "sha256:d15463d13f4456e1ead2594520216b225a16f781e144f8fdf6c5bb4667c48b3f", size = 46960, upload-time = "2025-05-26T04:54:40.484Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/30/05/ce271016e351fddc8399e546f6e23761967ee09c8c568bbfbecb0c150171/pytest_asyncio-1.0.0-py3-none-any.whl", hash = "sha256:4f024da9f1ef945e680dc68610b52550e36590a67fd31bb3b4943979a1f90ef3", size = 15976, upload-time = "2025-05-26T04:54:39.035Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest-docker"
|
||||
version = "3.1.1"
|
||||
|
|
@ -3978,7 +3980,7 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "python-pptx"
|
||||
version = "1.0.2"
|
||||
version = "1.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "lxml" },
|
||||
|
|
@ -3986,9 +3988,9 @@ dependencies = [
|
|||
{ name = "typing-extensions" },
|
||||
{ name = "xlsxwriter" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/52/a9/0c0db8d37b2b8a645666f7fd8accea4c6224e013c42b1d5c17c93590cd06/python_pptx-1.0.2.tar.gz", hash = "sha256:479a8af0eaf0f0d76b6f00b0887732874ad2e3188230315290cd1f9dd9cc7095", size = 10109297, upload-time = "2024-08-07T17:33:37.772Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5d/b5/b5f64158c9230429bbe9b87a372ccda6ce9b0a64fa48c43a377be284e144/python_pptx-1.0.0.tar.gz", hash = "sha256:5c0f9fbf564fccf825c03c8bb75af9245fbdfad75e2857d115f47fd94b65eae8", size = 10109490, upload-time = "2024-08-03T21:26:51.272Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/4f/00be2196329ebbff56ce564aa94efb0fbc828d00de250b1980de1a34ab49/python_pptx-1.0.2-py3-none-any.whl", hash = "sha256:160838e0b8565a8b1f67947675886e9fea18aa5e795db7ae531606d68e785cba", size = 472788, upload-time = "2024-08-07T17:33:28.192Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/06/62d0069a8b6ece6dded497593538a97339121e3096eff77ba6f3734a84a7/python_pptx-1.0.0-py3-none-any.whl", hash = "sha256:e099cbcb370e97ae1cca2186ac757774a2c7bf1b5cb7e6ac3cb77061466713c5", size = 472287, upload-time = "2024-08-03T21:26:39.331Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -4072,20 +4074,20 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "qdrant-client"
|
||||
version = "1.14.3"
|
||||
version = "1.12.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "grpcio" },
|
||||
{ name = "grpcio-tools" },
|
||||
{ name = "httpx", extra = ["http2"] },
|
||||
{ name = "numpy" },
|
||||
{ name = "portalocker" },
|
||||
{ name = "protobuf" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1d/56/3f355f931c239c260b4fe3bd6433ec6c9e6185cd5ae0970fe89d0ca6daee/qdrant_client-1.14.3.tar.gz", hash = "sha256:bb899e3e065b79c04f5e47053d59176150c0a5dabc09d7f476c8ce8e52f4d281", size = 286766, upload-time = "2025-06-16T11:13:47.838Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/15/5e/ec560881e086f893947c8798949c72de5cfae9453fd05c2250f8dfeaa571/qdrant_client-1.12.1.tar.gz", hash = "sha256:35e8e646f75b7b883b3d2d0ee4c69c5301000bba41c82aa546e985db0f1aeb72", size = 237441, upload-time = "2024-10-29T17:31:09.698Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/35/5e/8174c845707e60b60b65c58f01e40bbc1d8181b5ff6463f25df470509917/qdrant_client-1.14.3-py3-none-any.whl", hash = "sha256:66faaeae00f9b5326946851fe4ca4ddb1ad226490712e2f05142266f68dfc04d", size = 328969, upload-time = "2025-06-16T11:13:46.636Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/c0/eef4fe9dad6d41333f7dc6567fa8144ffc1837c8a0edfc2317d50715335f/qdrant_client-1.12.1-py3-none-any.whl", hash = "sha256:b2d17ce18e9e767471368380dd3bbc4a0e3a0e2061fedc9af3542084b48451e0", size = 267171, upload-time = "2024-10-29T17:31:07.758Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -4157,6 +4159,12 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/ba/12/1e5497183bdbe782dbb91bad1d0d2297dba4d2831b2652657f7517bfc6df/rapidocr_onnxruntime-1.4.4-py3-none-any.whl", hash = "sha256:971d7d5f223a7a808662229df1ef69893809d8457d834e6373d3854bc1782cbf", size = 14915192, upload-time = "2025-01-17T01:48:25.104Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "red-black-tree-mod"
|
||||
version = "1.20"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/34/12/944f61bc67a1e918953741c0b3b75a28f96d8060d08fd3614233309ced3b/red-black-tree-mod-1.20.tar.gz", hash = "sha256:2448e6fc9cbf1be204c753f352c6ee49aa8156dbf1faa57dfc26bd7705077e0a", size = 28589, upload-time = "2013-11-04T16:58:20.788Z" }
|
||||
|
||||
[[package]]
|
||||
name = "redis"
|
||||
version = "5.2.1"
|
||||
|
|
@ -4209,7 +4217,7 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.32.4"
|
||||
version = "2.32.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
|
|
@ -4217,9 +4225,9 @@ dependencies = [
|
|||
{ name = "idna" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218, upload-time = "2024-05-29T15:37:49.536Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928, upload-time = "2024-05-29T15:37:47.027Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -4295,6 +4303,18 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/49/97/fa78e3d2f65c02c8e1268b9aba606569fe97f6c8f7c2d74394553347c145/rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7", size = 34315, upload-time = "2022-07-20T10:28:34.978Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rtfde"
|
||||
version = "0.1.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "lark" },
|
||||
{ name = "oletools" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/32/1ad82739351117c0711767b828e8f2567a5ffb783741a87120d955564a19/RTFDE-0.1.2-py3-none-any.whl", hash = "sha256:f6d1450c99b04e930da130e8b419aa33b1f953623e1b94ad5c0f67f0362eb737", size = 36142, upload-time = "2024-06-22T15:11:56.792Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "s3transfer"
|
||||
version = "0.10.4"
|
||||
|
|
@ -4503,15 +4523,6 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "socksio"
|
||||
version = "1.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f8/5c/48a7d9495be3d1c651198fd99dbb6ce190e2274d0f28b9051307bdec6b85/socksio-1.0.0.tar.gz", hash = "sha256:f88beb3da5b5c38b9890469de67d0cb0f9d494b78b106ca1845f96c10b91c4ac", size = 19055, upload-time = "2020-04-17T15:50:34.664Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/37/c3/6eeb6034408dac0fa653d126c9204ade96b819c936e136c5e8a6897eee9c/socksio-1.0.0-py3-none-any.whl", hash = "sha256:95dc1f15f9b34e8d7b16f06d74b8ccf48f609af32ab33c608d08761c5dcbb1f3", size = 12763, upload-time = "2020-04-17T15:50:31.878Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "soundfile"
|
||||
version = "0.13.1"
|
||||
|
|
@ -4901,20 +4912,23 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "unstructured-client"
|
||||
version = "0.38.1"
|
||||
version = "0.32.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "aiofiles" },
|
||||
{ name = "cryptography" },
|
||||
{ name = "eval-type-backport" },
|
||||
{ name = "httpx" },
|
||||
{ name = "nest-asyncio" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "pypdf" },
|
||||
{ name = "python-dateutil" },
|
||||
{ name = "requests-toolbelt" },
|
||||
{ name = "typing-inspection" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/85/60/412092671bfc4952640739f2c0c9b2f4c8af26a3c921738fd12621b4ddd8/unstructured_client-0.38.1.tar.gz", hash = "sha256:43ab0670dd8ff53d71e74f9b6dfe490a84a5303dab80a4873e118a840c6d46ca", size = 91781, upload-time = "2025-07-03T15:46:35.054Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/51/36/a40ab6f30c2e567b3bf17e61056943076622e2d09fcf625947e6ba1be9e3/unstructured_client-0.32.3.tar.gz", hash = "sha256:1426d03325f7b93daad524ad2b954f1e7cceb0c15e67a4f4e88b49220dd2472c", size = 79281, upload-time = "2025-04-07T23:35:31.058Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/26/e0/8c249f00ba85fb4aba5c541463312befbfbf491105ff5c06e508089467be/unstructured_client-0.38.1-py3-none-any.whl", hash = "sha256:71e5467870d0a0119c788c29ec8baf5c0f7123f424affc9d6682eeeb7b8d45fa", size = 212626, upload-time = "2025-07-03T15:46:33.929Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/3b/cc5f193ff32294e709b57b4f5c00cc71a5ecce4bef4e6cf224bf9da6c818/unstructured_client-0.32.3-py3-none-any.whl", hash = "sha256:50b8198a3c3f984bdb53d848be7665d352093a99841858976f596cc2105903ec", size = 180617, upload-time = "2025-04-07T23:35:29.594Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -4937,15 +4951,15 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "uvicorn"
|
||||
version = "0.34.2"
|
||||
version = "0.34.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
{ name = "h11" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a6/ae/9bbb19b9e1c450cf9ecaef06463e40234d98d95bf572fab11b4f19ae5ded/uvicorn-0.34.2.tar.gz", hash = "sha256:0e929828f6186353a80b58ea719861d2629d766293b6d19baf086ba31d4f3328", size = 76815, upload-time = "2025-04-19T06:02:50.101Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4b/4d/938bd85e5bf2edeec766267a5015ad969730bb91e31b44021dfe8b22df6c/uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9", size = 76568, upload-time = "2024-12-15T13:33:30.42Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/4b/4cef6ce21a2aaca9d852a6e84ef4f135d99fcd74fa75105e2fc0c8308acd/uvicorn-0.34.2-py3-none-any.whl", hash = "sha256:deb49af569084536d269fe0a6d67e3754f104cf03aba7c11c40f01aadf33c403", size = 62483, upload-time = "2025-04-19T06:02:48.42Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4", size = 62315, upload-time = "2024-12-15T13:33:27.467Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
|
|
@ -4981,11 +4995,11 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "validators"
|
||||
version = "0.35.0"
|
||||
version = "0.34.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/53/66/a435d9ae49850b2f071f7ebd8119dd4e84872b01630d6736761e6e7fd847/validators-0.35.0.tar.gz", hash = "sha256:992d6c48a4e77c81f1b4daba10d16c3a9bb0dbb79b3a19ea847ff0928e70497a", size = 73399, upload-time = "2025-05-01T05:42:06.7Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/64/07/91582d69320f6f6daaf2d8072608a4ad8884683d4840e7e4f3a9dbdcc639/validators-0.34.0.tar.gz", hash = "sha256:647fe407b45af9a74d245b943b18e6a816acf4926974278f6dd617778e1e781f", size = 70955, upload-time = "2024-09-03T17:45:04.386Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/6e/3e955517e22cbdd565f2f8b2e73d52528b14b8bcfdb04f62466b071de847/validators-0.35.0-py3-none-any.whl", hash = "sha256:e8c947097eae7892cb3d26868d637f79f47b4a0554bc6b80065dfe5aac3705dd", size = 44712, upload-time = "2025-05-01T05:42:04.203Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/78/36828a4d857b25896f9774c875714ba4e9b3bc8a92d2debe3f4df3a83d4f/validators-0.34.0-py3-none-any.whl", hash = "sha256:c804b476e3e6d3786fa07a30073a4ef694e617805eb1946ceee3fe5a9b8b1321", size = 43536, upload-time = "2024-09-03T17:45:01.127Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -5095,6 +5109,12 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/52/24/ab44c871b0f07f491e5d2ad12c9bd7358e527510618cb1b803a88e986db1/werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e", size = 224498, upload-time = "2024-11-08T15:52:16.132Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "win-unicode-console"
|
||||
version = "0.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/89/8d/7aad74930380c8972ab282304a2ff45f3d4927108bb6693cabcc9fc6a099/win_unicode_console-0.5.zip", hash = "sha256:d4142d4d56d46f449d6f00536a73625a871cba040f0bc1a2e305a04578f07d1e", size = 31420, upload-time = "2016-06-25T19:48:54.05Z" }
|
||||
|
||||
[[package]]
|
||||
name = "win32-setctime"
|
||||
version = "1.2.0"
|
||||
|
|
@ -5260,15 +5280,15 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "youtube-transcript-api"
|
||||
version = "1.1.0"
|
||||
version = "1.0.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "defusedxml" },
|
||||
{ name = "requests" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/36/dd/10d413b20a2d14fa483853d0f6d920a0a0a6887d7c60167e4641733f99fb/youtube_transcript_api-1.1.0.tar.gz", hash = "sha256:786d9e64bd7fffee0dbc1471a61a798cebdc379b9cf8f7661d3664e831fcc1a5", size = 470144, upload-time = "2025-06-11T22:30:44.048Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b0/32/f60d87a99c05a53604c58f20f670c7ea6262b55e0bbeb836ffe4550b248b/youtube_transcript_api-1.0.3.tar.gz", hash = "sha256:902baf90e7840a42e1e148335e09fe5575dbff64c81414957aea7038e8a4db46", size = 2153252, upload-time = "2025-03-25T18:14:21.119Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/69/63f1b9f96a9d3b6bd35288fe27f987c41bd157e47b3d07ca025549e3f8e6/youtube_transcript_api-1.1.0-py3-none-any.whl", hash = "sha256:876ac42b1e3f8cc99b81d8fd810bd74ed07511e51dff5db50e714e3156ad3595", size = 485739, upload-time = "2025-06-11T22:30:40.515Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/44/40c03bb0f8bddfb9d2beff2ed31641f52d96c287ba881d20e0c074784ac2/youtube_transcript_api-1.0.3-py3-none-any.whl", hash = "sha256:d1874e57de65cf14c9d7d09b2b37c814d6287fa0e770d4922c4cd32a5b3f6c47", size = 2169911, upload-time = "2025-03-25T18:14:19.416Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue