diff --git a/DSL/CronManager/DSL/data_resync.yml b/DSL/CronManager/DSL/data_resync.yml new file mode 100644 index 0000000..c5fb58d --- /dev/null +++ b/DSL/CronManager/DSL/data_resync.yml @@ -0,0 +1,5 @@ +agency_data_resync: + # trigger: "0 0/1 * * * ?" + trigger: off + type: exec + command: "../app/scripts/agency_data_resync.sh -s 10" \ No newline at end of file diff --git a/DSL/CronManager/DSL/initiate_vector_indexer.yml b/DSL/CronManager/DSL/initiate_vector_indexer.yml new file mode 100644 index 0000000..561f787 --- /dev/null +++ b/DSL/CronManager/DSL/initiate_vector_indexer.yml @@ -0,0 +1,5 @@ +vector_indexer: + trigger: off + type: exec + command: "../app/scripts/vector_indexer_pipeline.sh" + allowedEnvs: ['signedUrl', 'clientDataHash'] \ No newline at end of file diff --git a/DSL/CronManager/script/agency_data_resync.sh b/DSL/CronManager/script/agency_data_resync.sh new file mode 100644 index 0000000..33ae952 --- /dev/null +++ b/DSL/CronManager/script/agency_data_resync.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +# DEFINING ENDPOINTS + +CHECK_RESYNC_DATA_AVAILABILITY_ENDPOINT=http://ruuter-public:8086/rag-search/data/update + +# Construct payload to update training status using cat +payload=$(cat <=1.21.0,<2.0" || exit 1 +"$UV_BIN" pip install --python "$VENV_PATH/bin/python3" "requests>=2.32.5" || exit 1 +"$UV_BIN" pip install --python "$VENV_PATH/bin/python3" "pydantic>=2.11.7" || exit 1 +"$UV_BIN" pip install --python "$VENV_PATH/bin/python3" "qdrant-client>=1.15.1" || exit 1 +"$UV_BIN" pip install --python "$VENV_PATH/bin/python3" "rank-bm25>=0.2.2" || exit 1 +"$UV_BIN" pip install --python "$VENV_PATH/bin/python3" "tiktoken>=0.11.0" || exit 1 +"$UV_BIN" pip install --python "$VENV_PATH/bin/python3" "dvc[s3]>=3.55.2" || exit 1 +"$UV_BIN" pip install --python "$VENV_PATH/bin/python3" "loguru>=0.7.3" || exit 1 + +echo "[PACKAGES] All packages installed successfully" + +export PYTHONPATH="/app:/app/src:/app/src/vector_indexer:$PYTHONPATH" + +[ ! -f "$PYTHON_SCRIPT" ] && { echo "[ERROR] Python script not found"; exit 1; } + +echo "[FOUND] Python script at: $PYTHON_SCRIPT" + +# Run vector indexer with signed URL parameter +echo "[STARTING] Vector indexer processing..." + +echo "[DEBUG] About to execute main_indexer.py..." +if [ -n "$signedUrl" ]; then + echo "[SIGNED_URL] Using signed URL for dataset processing" + echo "[COMMAND] python3 -u $PYTHON_SCRIPT --signed-url $signedUrl" + python3 -u "$PYTHON_SCRIPT" --signed-url "$signedUrl" 2>&1 + PYTHON_EXIT_CODE=$? +else + echo "[NO_URL] Running without signed URL" + echo "[COMMAND] python3 -u $PYTHON_SCRIPT" + python3 -u "$PYTHON_SCRIPT" 2>&1 + PYTHON_EXIT_CODE=$? +fi + +echo "[DEBUG] Python execution completed with exit code: $PYTHON_EXIT_CODE" + +# Handle exit codes +if [ $PYTHON_EXIT_CODE -eq 0 ]; then + echo "[SUCCESS] Vector indexer completed successfully" + exit 0 +elif [ $PYTHON_EXIT_CODE -eq 2 ]; then + echo "[WARNING] Vector indexer completed with some failures" + exit 2 +elif [ $PYTHON_EXIT_CODE -eq 130 ]; then + echo "[INTERRUPTED] Vector indexer was interrupted by user" + exit 130 +else + echo "[ERROR] Vector indexer failed with exit code: $PYTHON_EXIT_CODE" + exit $PYTHON_EXIT_CODE +fi \ No newline at end of file diff --git a/DSL/Liquibase/changelog/rag-search-script-v1-llm-connections.sql b/DSL/Liquibase/changelog/rag-search-script-v1-llm-connections.sql index cfead3d..55cccd2 100644 --- a/DSL/Liquibase/changelog/rag-search-script-v1-llm-connections.sql +++ b/DSL/Liquibase/changelog/rag-search-script-v1-llm-connections.sql @@ -121,3 +121,20 @@ INSERT INTO embedding_models (platform_id, model_key, model_name) VALUES CREATE INDEX idx_llm_models_platform_id ON llm_models(platform_id); CREATE INDEX idx_embedding_models_platform_id ON embedding_models(platform_id); +CREATE TABLE public.agency_sync ( + agency_id VARCHAR(50) PRIMARY KEY, + agency_data_hash VARCHAR(255), + data_url TEXT, + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP NOT NULL DEFAULT NOW() +); + +INSERT INTO public.agency_sync (agency_id, created_at) VALUES +('AGENCY001', NOW()); + +CREATE TABLE public.mock_ckb ( + client_id VARCHAR(50) PRIMARY KEY, + client_data_hash VARCHAR(255) NOT NULL, + signed_s3_url TEXT NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT NOW() +); \ No newline at end of file diff --git a/DSL/Resql/rag-search/POST/get-agency-id.sql b/DSL/Resql/rag-search/POST/get-agency-id.sql new file mode 100644 index 0000000..a2bf5b0 --- /dev/null +++ b/DSL/Resql/rag-search/POST/get-agency-id.sql @@ -0,0 +1,4 @@ +SELECT + agency_id, + agency_data_hash +FROM public.agency_sync; \ No newline at end of file diff --git a/DSL/Resql/rag-search/POST/mock-get-data-from-kb.sql b/DSL/Resql/rag-search/POST/mock-get-data-from-kb.sql new file mode 100644 index 0000000..9c9dc1b --- /dev/null +++ b/DSL/Resql/rag-search/POST/mock-get-data-from-kb.sql @@ -0,0 +1,5 @@ +SELECT + client_id, + client_data_hash, + signed_s3_url +FROM public.mock_ckb; diff --git a/DSL/Ruuter.private/rag-search/POST/ckb/agency_data_import.yml b/DSL/Ruuter.private/rag-search/POST/ckb/agency_data_import.yml new file mode 100644 index 0000000..ba892e5 --- /dev/null +++ b/DSL/Ruuter.private/rag-search/POST/ckb/agency_data_import.yml @@ -0,0 +1,33 @@ +declaration: + call: declare + version: 0.1 + description: "Get agency data information by agency IDs" + method: post + accepts: json + returns: json + namespace: rag-search + allowlist: + body: + - field: agencyIds + type: array + description: "Array of unique institution IDs" + +extractRequestData: + assign: + agencyIds: ${incoming.body.agencyIds || []} + log: "Received request for agency data: ${agencyIds}" + +get_agency_data: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/mock-get-data-from-kb" + headers: + type: json + body: + agencyIds: ${agencyIds} + result: agency_data_info + next: return_result + +return_result: + return: ${agency_data_info.response.body} + next: end \ No newline at end of file diff --git a/DSL/Ruuter.private/rag-search/POST/inference/production.yml b/DSL/Ruuter.private/rag-search/POST/inference/production.yml index 5b13570..39fbf7e 100644 --- a/DSL/Ruuter.private/rag-search/POST/inference/production.yml +++ b/DSL/Ruuter.private/rag-search/POST/inference/production.yml @@ -101,7 +101,7 @@ assign_disconnected_response: disconnected_response: { chatId: "${chatId}", - content: "The LLM connection is currently unavailable. Your request couldnโ€™t be processed. Please retry shortly.", + content: "The LLM connection is currently unavailable. Your request couldn't be processed. Please retry shortly.", status: 400 } next: return_connection_disconnected @@ -118,4 +118,4 @@ return_budget_check_error: return_no_production_connection: status: 404 return: "No production connection found" - next: end + next: end \ No newline at end of file diff --git a/DSL/Ruuter.public/rag-search/POST/ckb/agency-data-import.yml b/DSL/Ruuter.public/rag-search/POST/ckb/agency-data-import.yml new file mode 100644 index 0000000..9905b27 --- /dev/null +++ b/DSL/Ruuter.public/rag-search/POST/ckb/agency-data-import.yml @@ -0,0 +1,33 @@ +declaration: + call: declare + version: 0.1 + description: "Get agency data information by agency IDs" + method: post + accepts: json + returns: json + namespace: rag-search + allowlist: + body: + - field: agencyIds + type: array + description: "Array of unique institution IDs" + +extractRequestData: + assign: + agencyIds: ${incoming.body.agencyIds || []} + log: "Received request for agency data: ${agencyIds}" + +get_agency_data: + call: http.post + args: + url: "[#RAG_SEARCH_RESQL]/mock-get-data-from-kb" + headers: + type: json + body: + agencyIds: ${agencyIds} + result: agency_data_info + next: return_result + +return_result: + return: ${agency_data_info.response.body} + next: end \ No newline at end of file diff --git a/DSL/Ruuter.public/rag-search/POST/data/update.yml b/DSL/Ruuter.public/rag-search/POST/data/update.yml new file mode 100644 index 0000000..7ba211f --- /dev/null +++ b/DSL/Ruuter.public/rag-search/POST/data/update.yml @@ -0,0 +1,88 @@ +declaration: + call: declare + version: 0.1 + description: "Resync new data from KB" + method: post + accepts: json + returns: json + namespace: rag-search + +get_agency_id: + call: http.post + args: + url: "[#RAG_SEARCH_RESQL]/get-agency-id" + result: get_agency_id_result + next: log_result + +log_result: + log: ${get_agency_id_result.response.body[0].agencyId} + next: extract_params + +extract_params: + assign: + single_agency_id: ${get_agency_id_result.response.body[0].agencyId} + agency_ids: + - ${single_agency_id} + agency_data_hash: ${get_agency_id_result.response.body[0].agencyDataHash} + next: logs_params + +logs_params: + log: "Agency ID: ${agency_ids}, Agency Data Hash: ${agency_data_hash}" + next: import_agency_data + +import_agency_data: + call: http.post + args: + url: "[#RAG_SEARCH_RUUTER_PUBLIC]/ckb/agency-data-import" + body: + agencyIds: ${agency_ids} + result: importResult + next: log_import_agency_data_response + +log_import_agency_data_response: + log: ${JSON.stringify(importResult.response)} + next: assign_import_agency_data + +assign_import_agency_data: + assign: + client_data_hash: ${importResult.response.body.response[0].clientDataHash} + signed_s3_url: ${importResult.response.body.response[0].signedS3Url} + next: check_has_match + +check_has_match: + switch: + - condition: ${agency_data_hash === importResult.response.body.response[0].clientDataHash} + next: noAgencyData + - condition: true + next: execute_cron_manager + +execute_cron_manager: + call: http.post + args: + url: "[#RAG_SEARCH_CRON_MANAGER]/execute/initiate_vector_indexer/vector_indexer" + query: + signedUrl: ${signed_s3_url} + clientDataHash: ${client_data_hash} + result: res + next: log_new_data_present + +log_new_data_present: + log: "New data present - synchronization required" + next: end + +assignNoAgencyResponse: + assign: + no_agency_response: + success: false + message: "No agency data available for sync" + next: noAgencyData + +noAgencyData: + assign: + response_data: + success: true + message: "No sync required - data is up to date" + status: 200 + return: ${response_data} + next: end + \ No newline at end of file diff --git a/src/vector_indexer/diff_identifier/_init__py b/GUI/vite.config.ts.timestamp-1760669278597-b5a7f003c52fe.mjs similarity index 100% rename from src/vector_indexer/diff_identifier/_init__py rename to GUI/vite.config.ts.timestamp-1760669278597-b5a7f003c52fe.mjs diff --git a/docker-compose.yml b/docker-compose.yml index 8fc77ef..5884a96 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -54,7 +54,7 @@ services: - ./DSL/DMapper/rag-search/hbs:/workspace/app/views/rag-search - ./DSL/DMapper/rag-search/lib:/workspace/app/lib ports: - - 3000:3000 + - 3001:3000 networks: - bykstack @@ -120,21 +120,6 @@ services: - ./DSL/DatasetGenerator/output_datasets:/app/output_datasets networks: - bykstack - - cron-manager: - container_name: cron-manager - image: cron-manager-python:latest - volumes: - - ./DSL/CronManager/DSL:/DSL - - ./DSL/CronManager/script:/app/scripts - - cron_data:/app/data - - ./models:/app/models - environment: - - server.port=9010 - ports: - - 9010:8080 - networks: - - bykstack gui: container_name: gui @@ -169,6 +154,44 @@ services: mem_limit: "1G" restart: unless-stopped + rag-s3-ferry: + image: s3-ferry:latest + container_name: rag-s3-ferry + volumes: + - shared-volume:/app/shared + - cron_data:/app/data + - ./datasets:/app/datasets # Access to datasets folder for diff identifier operations + env_file: + - .env + ports: + - "3006:3000" + networks: + - bykstack + depends_on: + minio: + condition: service_started + + cron-manager: + container_name: cron-manager + image: cron-manager-python:latest + user: "root" + volumes: + - ./DSL/CronManager/DSL:/DSL + - ./DSL/CronManager/script:/app/scripts + - ./src/vector_indexer:/app/src/vector_indexer + - cron_data:/app/data + - shared-volume:/app/shared # Access to shared resources for cross-container coordination + - ./datasets:/app/datasets # Direct access to datasets folder for diff identifier operations + - ./grafana-configs/loki_logger.py:/app/src/vector_indexer/loki_logger.py + - ./.env:/app/.env:ro + environment: + - server.port=9010 + - PYTHONPATH=/app:/app/src/vector_indexer + ports: + - 9010:8080 + networks: + - bykstack + qdrant: image: qdrant/qdrant:v1.15.1 restart: always @@ -290,7 +313,7 @@ services: - langfuse-worker - rag_search_db ports: - - 3000:3000 + - 3005:3000 env_file: - .env environment: @@ -499,6 +522,8 @@ volumes: name: vault-data vault-agent-out: name: vault-agent-out + shared-volume: + name: shared-volume cron_data: name: cron_data diff --git a/generate_presigned_url.py b/generate_presigned_url.py new file mode 100644 index 0000000..4f4d417 --- /dev/null +++ b/generate_presigned_url.py @@ -0,0 +1,63 @@ +import boto3 +from botocore.client import Config +from typing import List, Dict + +# Create S3 client for MinIO +s3_client = boto3.client( + "s3", + endpoint_url="http://minio:9000", # Replace with your MinIO URL + aws_access_key_id="", # Replace with your access key + aws_secret_access_key="", # Replace with your secret key + config=Config(signature_version="s3v4"), # Hardcoded signature version + region_name="us-east-1", # MinIO usually works with any region +) + +# List of files to process +files_to_process: List[Dict[str, str]] = [ + {"bucket": "ckb", "key": "sm_someuuid/sm_someuuid.zip"}, +] + +# Generate presigned URLs +presigned_urls: List[str] = [] + +print("Generating presigned URLs...") +for file_info in files_to_process: + try: + url = s3_client.generate_presigned_url( + ClientMethod="get_object", + Params={"Bucket": file_info["bucket"], "Key": file_info["key"]}, + ExpiresIn=24 * 3600, # 4 hours in seconds + ) + presigned_urls.append(url) + print(f":white_check_mark: Generated URL for: {file_info['key']}") + print(f" URL: {url}") + except Exception as e: + print(f":x: Failed to generate URL for: {file_info['key']}") + print(f" Error: {str(e)}") + +output_file: str = "minio_presigned_urls.txt" + +try: + with open(output_file, "w") as f: + # Write URLs separated by ||| delimiter (for your script) + url_string: str = "|||".join(presigned_urls) + f.write(url_string) + f.write("\n\n") + + # Also write each URL on separate lines for readability + f.write("Individual URLs:\n") + f.write("=" * 50 + "\n") + for i, url in enumerate(presigned_urls, 1): + f.write(f"URL {i}:\n{url}\n\n") + + print(f"\n:white_check_mark: Presigned URLs saved to: {output_file}") + print(f"Total URLs generated: {len(presigned_urls)}") + + # Display the combined URL string for easy copying + if presigned_urls: + print("\nCombined URL string (for signedUrls environment variable):") + print("=" * 60) + print("|||".join(presigned_urls)) + +except Exception as e: + print(f":x: Failed to save URLs to file: {str(e)}") diff --git a/grafana-configs/loki_logger.py b/grafana-configs/loki_logger.py index 444db47..e25b340 100644 --- a/grafana-configs/loki_logger.py +++ b/grafana-configs/loki_logger.py @@ -32,7 +32,7 @@ def __init__( # Set default timeout for all requests self.timeout = 5 - def _send_to_loki(self, level: str, message: str, **extra_fields): + def _send_to_loki(self, level: str, message: str): """Send log entry directly to Loki API""" try: # Create timestamp in nanoseconds (Loki requirement) @@ -45,14 +45,6 @@ def _send_to_loki(self, level: str, message: str, **extra_fields): "hostname": self.hostname, } - # Add extra fields as labels, filtering out None values except for model_id - for key, value in extra_fields.items(): - if key == "model_id": - # Always include model_id, default to "None" if not provided - labels[key] = str(value) if value is not None else "None" - elif value is not None: - labels[key] = str(value) - # Create log entry log_entry = { "timestamp": datetime.now().isoformat(), @@ -60,7 +52,6 @@ def _send_to_loki(self, level: str, message: str, **extra_fields): "message": message, "hostname": self.hostname, "service": self.service_name, - **extra_fields, } # Prepare Loki payload @@ -87,29 +78,16 @@ def _send_to_loki(self, level: str, message: str, **extra_fields): # Also print to console for immediate feedback timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - model_info = ( - f" [Model: {extra_fields.get('model_id', 'N/A')}]" - if extra_fields.get("model_id") - else "" - ) - print(f"[{timestamp}] {level: <8}{model_info} | {message}") - - def info(self, message: str, model_id: str | None = None, **extra_fields): - if model_id: - extra_fields["model_id"] = model_id - self._send_to_loki("INFO", message, **extra_fields) - - def error(self, message: str, model_id: str | None = None, **extra_fields): - if model_id: - extra_fields["model_id"] = model_id - self._send_to_loki("ERROR", message, **extra_fields) - - def warning(self, message: str, model_id: str | None = None, **extra_fields): - if model_id: - extra_fields["model_id"] = model_id - self._send_to_loki("WARNING", message, **extra_fields) - - def debug(self, message: str, model_id: str | None = None, **extra_fields): - if model_id: - extra_fields["model_id"] = model_id - self._send_to_loki("DEBUG", message, **extra_fields) + print(f"[{timestamp}] {level: <8} | {message}") + + def info(self, message: str): + self._send_to_loki("INFO", message) + + def error(self, message: str): + self._send_to_loki("ERROR", message) + + def warning(self, message: str): + self._send_to_loki("WARNING", message) + + def debug(self, message: str): + self._send_to_loki("DEBUG", message) diff --git a/migrate.sh b/migrate.sh index 3a03cdc..c156698 100644 --- a/migrate.sh +++ b/migrate.sh @@ -12,4 +12,4 @@ INI_FILE="constants.ini" DB_PASSWORD=$(get_ini_value "$INI_FILE" "DB_PASSWORD") -docker run --rm --network bykstack -v `pwd`/DSL/Liquibase/changelog:/liquibase/changelog -v `pwd`/DSL/Liquibase/master.yml:/liquibase/master.yml -v `pwd`/DSL/Liquibase/data:/liquibase/data liquibase/liquibase --defaultsFile=/liquibase/changelog/liquibase.properties --changelog-file=master.yml --url=jdbc:postgresql://rag_search_db:5432/rag-search?user=postgres --password=$DB_PASSWORD update +docker run --rm --network bykstack -v `pwd`/DSL/Liquibase/changelog:/liquibase/changelog -v `pwd`/DSL/Liquibase/master.yml:/liquibase/master.yml -v `pwd`/DSL/Liquibase/data:/liquibase/data liquibase/liquibase:4.33 --defaultsFile=/liquibase/changelog/liquibase.properties --changelog-file=master.yml --url=jdbc:postgresql://rag_search_db:5432/rag-search?user=postgres --password=$DB_PASSWORD update diff --git a/pyproject.toml b/pyproject.toml index 8fcefde..4db4d3d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,6 +32,11 @@ dependencies = [ "anthropic>=0.69.0", "nemoguardrails>=0.16.0", "rerankers[transformers]>=0.10.0", + "deepeval>=3.6.0", + "pytest-json-report>=1.5.0", + "deepteam>=0.2.5", + "anthropic>=0.69.0", + "nemoguardrails>=0.16.0", "tiktoken>=0.11.0", ] diff --git a/run_vector_indexer.py b/run_vector_indexer.py deleted file mode 100644 index b01150f..0000000 --- a/run_vector_indexer.py +++ /dev/null @@ -1,179 +0,0 @@ -#!/usr/bin/env python3 -""" -Entry point script for Vector Indexer - Contextual Retrieval Pipeline - -This script can be run directly or called by cron jobs for automated processing. - -Usage: - python run_vector_indexer.py [--config CONFIG_PATH] [--health-check] [--dry-run] - -Examples: - # Run with default config - python run_vector_indexer.py - - # Run with custom config - python run_vector_indexer.py --config /path/to/config.yaml - - # Health check only - python run_vector_indexer.py --health-check - - # Dry run (validate without processing) - python run_vector_indexer.py --dry-run -""" - -import argparse -import asyncio -import sys -from pathlib import Path - -# Add src to Python path -sys.path.insert(0, str(Path(__file__).parent / "src")) - -from src.vector_indexer.main_indexer import VectorIndexer - - -async def main(): - """Main entry point with command line argument parsing.""" - - parser = argparse.ArgumentParser( - description="Vector Indexer - Contextual Retrieval Pipeline", - formatter_class=argparse.RawDescriptionHelpFormatter, - epilog=__doc__, - ) - - parser.add_argument( - "--config", - type=str, - default="src/vector_indexer/config/vector_indexer_config.yaml", - help="Path to configuration file (default: src/vector_indexer/config/vector_indexer_config.yaml)", - ) - - parser.add_argument( - "--health-check", action="store_true", help="Run health check only and exit" - ) - - parser.add_argument( - "--dry-run", - action="store_true", - help="Validate configuration and connectivity without processing documents", - ) - - parser.add_argument( - "--verbose", "-v", action="store_true", help="Enable verbose logging" - ) - - parser.add_argument( - "--quiet", "-q", action="store_true", help="Suppress non-error output" - ) - - args = parser.parse_args() - - # Configure logging level based on arguments - log_level = "INFO" - if args.verbose: - log_level = "DEBUG" - elif args.quiet: - log_level = "ERROR" - - try: - # Initialize vector indexer with specified config - indexer = VectorIndexer(config_path=args.config) - - if args.health_check: - # Health check only - print("๐Ÿ” Running health check...") - health_ok = await indexer.run_health_check() - - if health_ok: - print("โœ… Health check passed!") - return 0 - else: - print("โŒ Health check failed!") - return 1 - - elif args.dry_run: - # Dry run - validate without processing - print("๐Ÿงช Running dry run validation...") - - health_ok = await indexer.run_health_check() - if not health_ok: - print("โŒ Validation failed!") - return 1 - - # Discover documents but don't process - documents = indexer.document_loader.discover_all_documents() - print(f"๐Ÿ“„ Found {len(documents)} documents ready for processing") - print("โœ… Dry run validation passed!") - return 0 - - else: - # Full processing run - print("๐Ÿš€ Starting Vector Indexer processing...") - - # Health check first - health_ok = await indexer.run_health_check() - if not health_ok: - print("โŒ Pre-processing health check failed!") - return 1 - - # Process all documents - stats = await indexer.process_all_documents() - - # Return appropriate exit code - if stats.documents_failed > 0: - print(f"โš ๏ธ Processing completed with {stats.documents_failed} failures") - return 2 # Partial success - else: - print("โœ… Processing completed successfully!") - return 0 - - except KeyboardInterrupt: - print("\nโน๏ธ Processing interrupted by user") - return 130 - except FileNotFoundError as e: - print(f"โŒ Configuration file not found: {e}") - return 1 - except Exception as e: - print(f"๐Ÿ’ฅ Fatal error: {e}") - return 1 - - -def cron_entry_point(): - """ - Entry point specifically designed for cron jobs. - - This function: - - Uses minimal output suitable for cron logs - - Returns appropriate exit codes for monitoring - - Handles errors gracefully for automated systems - """ - import logging - - # Configure minimal logging for cron - logging.basicConfig( - level=logging.INFO, - format="%(asctime)s - Vector Indexer - %(levelname)s - %(message)s", - ) - - try: - # Run with default configuration - result = asyncio.run(main()) - - if result == 0: - logging.info("Vector indexer completed successfully") - elif result == 2: - logging.warning("Vector indexer completed with some failures") - else: - logging.error("Vector indexer failed") - - return result - - except Exception as e: - logging.error(f"Vector indexer fatal error: {e}") - return 1 - - -if __name__ == "__main__": - # Run the async main function - exit_code = asyncio.run(main()) - sys.exit(exit_code) diff --git a/src/llm_orchestrator_config/config/schema.py b/src/llm_orchestrator_config/config/schema.py index e7c656a..8a2b525 100644 --- a/src/llm_orchestrator_config/config/schema.py +++ b/src/llm_orchestrator_config/config/schema.py @@ -8,7 +8,7 @@ class VaultConfig(BaseModel): """Configuration for HashiCorp Vault integration.""" - url: str = "http://localhost:8200" + url: str = "http://vault:8200" token: str = "" enabled: bool = True diff --git a/src/vector_indexer/config/config_loader.py b/src/vector_indexer/config/config_loader.py index 34a21d7..2d644c7 100644 --- a/src/vector_indexer/config/config_loader.py +++ b/src/vector_indexer/config/config_loader.py @@ -77,7 +77,8 @@ class VectorIndexerConfig(BaseModel): """Configuration model for vector indexer.""" # API Configuration - api_base_url: str = "http://localhost:8100" + api_base_url: str = "http://llm-orchestration-service:8100" + qdrant_url: str = "http://qdrant:6333" api_timeout: int = 300 # Processing Configuration @@ -209,9 +210,7 @@ def load_config( # API config api_config = indexer_config.get("api", {}) - flattened_config["api_base_url"] = api_config.get( - "base_url", "http://localhost:8100" - ) + flattened_config["api_base_url"] = api_config.get("base_url") flattened_config["api_timeout"] = api_config.get("timeout", 300) # Processing config diff --git a/src/vector_indexer/config/vector_indexer_config.yaml b/src/vector_indexer/config/vector_indexer_config.yaml index 5d09cf9..6a7d583 100644 --- a/src/vector_indexer/config/vector_indexer_config.yaml +++ b/src/vector_indexer/config/vector_indexer_config.yaml @@ -2,7 +2,7 @@ vector_indexer: # API Configuration api: - base_url: "http://localhost:8100" + base_url: "http://llm-orchestration-service:8100" qdrant_url: "http://qdrant:6333" timeout: 300 # seconds @@ -92,4 +92,18 @@ vector_indexer: # File validation min_file_size_bytes: 1 - max_file_size_bytes: 50000000 # 50MB \ No newline at end of file + max_file_size_bytes: 50000000 # 50MB + + # Diff Identifier Configuration + diff_identifier: + # Dataset tracking + datasets_path: "datasets" + metadata_filename: "processed-metadata.json" + + # Retry configuration + max_retries: 3 + max_delay_seconds: 8 + + # S3Ferry configuration (uses environment variables) + # S3_DATA_BUCKET_NAME, S3_DATA_BUCKET_PATH, S3_ENDPOINT_URL + # S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY \ No newline at end of file diff --git a/src/vector_indexer/constants.py b/src/vector_indexer/constants.py index 2b9e796..b13ed43 100644 --- a/src/vector_indexer/constants.py +++ b/src/vector_indexer/constants.py @@ -110,3 +110,29 @@ class LoggingConstants: # Progress reporting PROGRESS_REPORT_INTERVAL = 10 # Report every N documents + + +def GET_S3_FERRY_PAYLOAD( + destinationFilePath: str, + destinationStorageType: str, + sourceFilePath: str, + sourceStorageType: str, +) -> dict[str, str]: # noqa: N802 + """ + Generate S3Ferry payload for file transfer operations. + + Args: + destinationFilePath: Path where file should be stored + destinationStorageType: "S3" or "FS" (filesystem) + sourceFilePath: Path of source file + sourceStorageType: "S3" or "FS" (filesystem) + + Returns: + dict: Payload for S3Ferry API + """ + return { + "destinationFilePath": destinationFilePath, + "destinationStorageType": destinationStorageType, + "sourceFilePath": sourceFilePath, + "sourceStorageType": sourceStorageType, + } diff --git a/src/vector_indexer/dataset_download.py b/src/vector_indexer/dataset_download.py new file mode 100644 index 0000000..ebd9590 --- /dev/null +++ b/src/vector_indexer/dataset_download.py @@ -0,0 +1,91 @@ +"""Simple dataset download utility using requests.""" + +import zipfile +import tempfile +from pathlib import Path +import requests +from loguru import logger + + +def download_and_extract_dataset(signed_url: str) -> tuple[str, int]: + """ + Download ZIP from signed URL and extract it to datasets folder. + + Args: + signed_url: URL to download ZIP from + + Returns: + tuple: (extraction_path, files_extracted_count) + + Raises: + requests.RequestException: If download fails + zipfile.BadZipFile: If ZIP file is corrupted + IOError: If extraction fails + """ + if not signed_url: + raise ValueError("signed_url cannot be empty") + + logger.info("Starting dataset download...") + logger.debug(f"Download URL (first 100 chars): {signed_url[:100]}...") + + # Create datasets folder + datasets_path = Path("/app/datasets") + datasets_path.mkdir(parents=True, exist_ok=True) + logger.debug(f"Dataset directory ready: {datasets_path}") + + # Download ZIP to temp file + with tempfile.NamedTemporaryFile(delete=False, suffix=".zip") as temp_file: + temp_zip_path = Path(temp_file.name) + + try: + # Download file with progress logging + logger.info("Downloading ZIP file...") + response = requests.get( + signed_url, stream=True, timeout=300, allow_redirects=True + ) + response.raise_for_status() + + # Write to temp file + with open(temp_zip_path, "wb") as f: + for chunk in response.iter_content(chunk_size=8192): + if chunk: + f.write(chunk) + + file_size_mb = temp_zip_path.stat().st_size / (1024 * 1024) + logger.info(f"โœ“ Downloaded {file_size_mb:.1f} MB") + + # Extract ZIP + logger.info("Extracting files...") + files_count = 0 + with zipfile.ZipFile(temp_zip_path, "r") as zip_ref: + files_count = len(zip_ref.namelist()) + zip_ref.extractall(datasets_path) + + logger.info(f"Extracted {files_count} files to {datasets_path}") + logger.info("Cleaning up temporary files...") + + return str(datasets_path), files_count + + except requests.exceptions.HTTPError as e: + logger.error(f"Download failed with HTTP error {e.response.status_code}") + raise + except requests.exceptions.Timeout: + logger.error("Download timed out after 300 seconds") + raise + except requests.RequestException as e: + logger.error(f"Download request failed: {e}") + raise + except zipfile.BadZipFile as e: + logger.error(f"Invalid or corrupted ZIP file: {e}") + raise + except Exception as e: + logger.error(f"Extraction failed: {e}") + raise + finally: + # Always clean up temp file + if temp_zip_path.exists(): + try: + temp_zip_path.unlink() + logger.debug("Temporary ZIP file cleaned up") + except Exception as e: + logger.warning(f"Failed to clean up temp file: {e}") diff --git a/src/vector_indexer/diff_identifier/DIFF_IDENTIFIER_FLOW.md b/src/vector_indexer/diff_identifier/DIFF_IDENTIFIER_FLOW.md new file mode 100644 index 0000000..57a48d2 --- /dev/null +++ b/src/vector_indexer/diff_identifier/DIFF_IDENTIFIER_FLOW.md @@ -0,0 +1,1602 @@ +# Vector Indexer Diff Identifier with Automatic Cleanup + +## Overview + +The **Diff Identifier** is a sophisticated change detection and cleanup system that forms the first critical step in the Vector Indexer pipeline. It intelligently identifies which files have changed between dataset downloads using **Data Version Control (DVC)** and **content hashing**, and automatically manages **vector chunk cleanup** for deleted and modified files. This ensures that only new or modified content is processed for vector generation while maintaining vector store consistency by removing orphaned chunks. + +## Key Features + +โœ… **Incremental Processing**: Only process new or changed files +โœ… **Automatic Cleanup**: Delete chunks for removed/modified files +โœ… **Comprehensive Change Detection**: New, modified, deleted, unchanged files +โœ… **Vector Store Consistency**: Prevent orphaned chunks in Qdrant +โœ… **Performance Optimization**: Reduce processing time by up to 90% +โœ… **Robust Fallback**: Graceful degradation when diff detection fails + +## System Architecture + +### Enhanced Component Structure + +``` +src/vector_indexer/diff_identifier/ +โ”œโ”€โ”€ __init__.py # Module exports and public API +โ”œโ”€โ”€ diff_detector.py # Main orchestrator with cleanup coordination +โ”œโ”€โ”€ version_manager.py # DVC operations & comprehensive diff analysis +โ”œโ”€โ”€ s3_ferry_client.py # S3Ferry service integration for metadata transfer +โ””โ”€โ”€ diff_models.py # Enhanced data models with cleanup metadata +``` + +### Core Components Deep Dive + +#### 1. **DiffDetector** (`diff_detector.py`) +**Primary Role:** Main orchestrator that coordinates diff identification and cleanup workflow. + +**Enhanced Responsibilities:** +- Orchestrate comprehensive change detection (new, modified, deleted, unchanged) +- Coordinate automatic vector chunk cleanup operations +- Provide detailed logging for cleanup operations +- Handle both incremental and first-run scenarios + +**Enhanced Public Interface:** +```python +class DiffDetector: + async def get_changed_files() -> DiffResult # Now includes cleanup metadata + async def mark_files_processed(file_paths: List[str], chunks_info: Optional[Dict] = None) -> bool +``` + +**Cleanup Integration:** +```python +# New comprehensive flow +1. Detect all file changes (new, modified, deleted, unchanged) +2. Generate cleanup metadata (chunks to delete) +3. Return enhanced DiffResult with cleanup information +4. Main indexer executes cleanup before processing +``` + +#### 2. **Enhanced VersionManager** (`version_manager.py`) +**Primary Role:** Advanced change detection with cleanup metadata generation. + +**Enhanced Capabilities:** +```python +class VersionManager: + # Core DVC and detection methods + def initialize_dvc() -> bool + def scan_current_files() -> Dict[str, str] + + # NEW: Comprehensive change analysis + def identify_comprehensive_changes() -> Dict[str, Any] # Returns all change types + cleanup info + + # Enhanced metadata management + async def update_processed_files_metadata( + processed_files: Dict[str, str], + chunks_info: Optional[Dict[str, Dict[str, Any]]] = None + ) -> None +``` + +**Advanced Change Detection Algorithm:** +1. **File Discovery:** Recursively scan `datasets/` folder for all files +2. **Content Hashing:** Calculate SHA-256 hash for each file's content +3. **Comprehensive Comparison:** Identify 4 file types: + - **New files**: Not in previous metadata + - **Modified files**: Same path, different content hash + - **Deleted files**: In metadata but not in current scan + - **Unchanged files**: Same content hash (skip processing) +4. **Cleanup Metadata Generation:** Map deleted/modified files to their chunk IDs +5. **Statistics Calculation:** Provide detailed change statistics + +**Enhanced Metadata Structure:** +```python +{ + "last_updated": "2025-10-17T00:00:46Z", + "total_processed": 3, + "processing_stats": { + "last_run_new_files": 2, + "last_run_modified_files": 1, + "last_run_deleted_files": 1, + "last_cleanup_deleted_chunks": 15, + "last_run_timestamp": "2025-10-17T00:00:46Z" + }, + "processed_files": { + "sha256_hash": { + "content_hash": "sha256_hash", + "original_path": "datasets/doc1/cleaned.txt", + "file_size": 15234, + "processed_at": "2025-10-17T00:00:46Z", + "chunk_count": 5, # Track chunk count for validation + "chunk_ids": ["uuid1", "uuid2", "uuid3", "uuid4", "uuid5"] # Track exact chunks + } + } +} +``` + +#### 3. **Enhanced QdrantManager Integration** +**New Cleanup Capabilities:** + +```python +# NEW: Vector chunk deletion methods +async def delete_chunks_by_document_hash(collection_name: str, document_hash: str) -> int +async def delete_chunks_by_file_path(collection_name: str, file_path: str) -> int # Fallback +async def get_chunks_for_document(collection_name: str, document_hash: str) -> List[Dict] + +# Efficient deletion using Qdrant filters +delete_payload = { + "filter": { + "must": [{"key": "document_hash", "match": {"value": document_hash}}] + } +} +``` + +#### 4. **Enhanced Data Models** (`diff_models.py`) +**Enhanced with Cleanup Support:** + +```python +class ProcessedFileInfo(BaseModel): + content_hash: str + original_path: str + file_size: int + processed_at: str + chunk_count: int = 0 # NEW: Track number of chunks + chunk_ids: List[str] = Field(default_factory=list) # NEW: Track chunk IDs + +class DiffResult(BaseModel): + # File change detection + new_files: List[str] = Field(..., description="Files to process for first time") + modified_files: List[str] = Field(default_factory=list, description="Files with changed content") + deleted_files: List[str] = Field(default_factory=list, description="Files removed from dataset") + unchanged_files: List[str] = Field(default_factory=list, description="Files with same content") + + # Statistics + total_files_scanned: int + previously_processed_count: int + is_first_run: bool + + # NEW: Cleanup metadata + chunks_to_delete: Dict[str, List[str]] = Field(default_factory=dict) # document_hash -> chunk_ids + estimated_cleanup_count: int = Field(default=0) # Total chunks to be removed + +class VersionState(BaseModel): + last_updated: str + processed_files: Dict[str, ProcessedFileInfo] + total_processed: int + processing_stats: Dict[str, Any] = Field(default_factory=dict) # NEW: Enhanced stats +``` + +## Enhanced Processing Flow + +### Comprehensive Workflow + +```mermaid +graph TD + A[Start Vector Indexer] --> B[Check Existing Metadata] + B --> C{Metadata Exists?} + C -->|No| D[First Run: All Files New] + C -->|Yes| E[Comprehensive Diff Analysis] + + E --> F[Identify File Changes] + F --> G[New Files] + F --> H[Modified Files] + F --> I[Deleted Files] + F --> J[Unchanged Files] + + G --> K[Mark for Processing] + H --> L[Mark for Processing + Cleanup] + I --> M[Mark for Cleanup Only] + J --> N[Skip Processing] + + K --> O[Execute Cleanup Operations] + L --> O + M --> O + N --> P[Document Discovery] + + O --> Q{Chunks to Delete?} + Q -->|Yes| R[Delete Chunks from Qdrant] + Q -->|No| P + R --> S[Log Cleanup Results] + S --> P + + P --> T[Filter Documents] + T --> U[Process Documents] + U --> V[Store New Chunks] + V --> W[Update Metadata] + W --> X[Commit to DVC] + X --> Y[Complete] +``` + +### Detailed Processing Steps + +#### Step 1: Enhanced Diff Detection +```python +# NEW: Comprehensive change detection +diff_result = await diff_detector.get_changed_files() + +# Enhanced logging output: +๐Ÿ” COMPREHENSIVE DIFF ANALYSIS COMPLETE: + ๐Ÿ“„ New files: 2 + ๐Ÿ”„ Modified files: 1 + ๐Ÿ—‘๏ธ Deleted files: 1 + โญ๏ธ Unchanged files: 5 + ๐Ÿงน Total chunks to cleanup: 8 +``` + +#### Step 2: Automatic Cleanup Execution +```python +# NEW: Execute cleanup before processing +if diff_result.chunks_to_delete: + await main_indexer._execute_cleanup_operations(qdrant_manager, diff_result) + +# Cleanup logging output: +๐Ÿงน STARTING CLEANUP: 2 documents with chunks to delete +๐Ÿ—‘๏ธ DELETING 5 chunks for document abc123... + โœ… Deleted 5 chunks from contextual_chunks_azure + โœ… Deleted 0 chunks from contextual_chunks_aws + ๐Ÿ“Š Total deleted for document abc123...: 5 chunks +๐Ÿงน CLEANUP COMPLETED: 8 total chunks removed from 2 documents +``` + +#### Step 3: Selective Processing +```python +# Only process new and modified files +files_to_process = diff_result.new_files + diff_result.modified_files + +if not files_to_process: + logger.info("No new or changed files detected. Processing complete.") + return self.stats # Early exit - no processing needed +``` + +#### Step 4: Enhanced Metadata Tracking +```python +# NEW: Track chunk information in metadata +await diff_detector.mark_files_processed( + processed_paths, + chunks_info=collected_chunk_information # Future enhancement +) +``` + +## Change Detection Logic + +### File Change Classification + +| File State | Detection Logic | Action Required | +|------------|----------------|-----------------| +| **New** | Hash not in metadata | โœ… Process + Store chunks | +| **Modified** | Same path, different hash | โœ… Delete old chunks + Process + Store new chunks | +| **Deleted** | In metadata, not in current scan | โœ… Delete chunks only | +| **Unchanged** | Same hash as metadata | โญ๏ธ Skip processing | + +### Cleanup Target Identification + +```python +# Efficient chunk identification for cleanup +chunks_to_delete = { + "document_hash_123": ["chunk_uuid_1", "chunk_uuid_2", "chunk_uuid_3"], + "document_hash_456": ["chunk_uuid_4", "chunk_uuid_5"] +} + +# Cleanup execution per collection +for document_hash, chunk_ids in chunks_to_delete.items(): + for collection_name in ["contextual_chunks_azure", "contextual_chunks_aws"]: + deleted_count = await qdrant_manager.delete_chunks_by_document_hash( + collection_name, document_hash + ) +``` + +## Performance Optimizations + +### Efficient Vector Deletion +- **Filter-based deletion**: Single API call per document using Qdrant filters +- **Batch operations**: Process multiple documents in parallel +- **Collection targeting**: Only clean collections that contain chunks +- **Validation counting**: Pre-count chunks before deletion for accurate logging + +### Metadata Optimizations +- **Incremental updates**: Only update changed file records +- **Batch metadata operations**: Single S3Ferry call per operation type +- **Minimal Qdrant queries**: Use metadata as source of truth, not live queries + +### Example Performance Gains + +| Dataset Size | Traditional Approach | With Diff + Cleanup | Performance Gain | +|--------------|---------------------|----------------------|------------------| +| 100 files, 10 changed | Process all 100 | Process 10 + cleanup 5 | **85% reduction** | +| 1000 files, 50 modified | Process all 1000 | Process 50 + cleanup 25 | **92% reduction** | +| 10 files, 2 deleted | Process all 10 | Process 0 + cleanup 2 | **100% processing skip** | + +## Configuration + +### Environment Variables +```bash +# Core S3Ferry Configuration +S3_FERRY_URL=http://rag-s3-ferry:3000/v1/files/copy +DATASETS_PATH=/app/datasets +METADATA_FILENAME=processed-metadata.json + +# DVC S3 Configuration +S3_ENDPOINT_URL=http://minio:9000 +S3_ACCESS_KEY_ID=minioadmin +S3_SECRET_ACCESS_KEY=minioadmin +``` + +### Enhanced Logging Levels +```yaml +# Enable detailed cleanup logging +logging: + level: "INFO" # Standard level shows cleanup summaries + level: "DEBUG" # Detailed level shows individual chunk operations +``` + +## Error Handling and Recovery + +### Cleanup Failure Scenarios + +1. **Partial Cleanup Failure** + ```python + # Continue processing even if some chunks fail to delete + try: + deleted_count = await delete_chunks_by_document_hash(collection, doc_hash) + except Exception as e: + logger.error(f"Failed to delete chunks from {collection}: {e}") + continue # Continue with other collections/documents + ``` + +2. **Qdrant Connection Issues** + ```python + # Fallback: Process files but skip cleanup + if cleanup_failed: + logger.warning("Cleanup failed - proceeding with processing only") + # Processing continues, cleanup will be attempted in next run + ``` + +3. **Metadata Consistency** + ```python + # Validate metadata against actual vector store state + if chunk_count_mismatch: + logger.warning("Metadata chunk count doesn't match actual chunks") + # Cleanup based on document_hash filter (more reliable than chunk IDs) + ``` + +### Recovery Mechanisms + +- **Graceful Degradation**: If cleanup fails, processing continues +- **Next-Run Recovery**: Failed cleanups are retried in subsequent runs +- **Metadata Validation**: Cross-check metadata against vector store state +- **Manual Cleanup**: Provide tools for manual cleanup if needed + +## Troubleshooting + +### Common Issues + +1. **Cleanup Operations Failing** + ```bash + # Check Qdrant connectivity + curl http://qdrant:6333/collections + + # Check for orphaned chunks + # Look for document_hash values that no longer exist in datasets + ``` + +2. **Inconsistent Chunk Counts** + ```python + # Symptoms: Metadata shows N chunks but Qdrant has different count + # Cause: Processing interruption or partial failures + # Solution: Run manual cleanup or reset metadata + ``` + +3. **Performance Degradation** + ```python + # Too many small cleanup operations + # Solution: Batch cleanup operations, optimize Qdrant filters + ``` + +### Debug Commands + +```python +# Enable comprehensive diff logging +diff_result = await diff_detector.get_changed_files() +logger.info(f"Cleanup metadata: {diff_result.chunks_to_delete}") + +# Test cleanup operations +cleanup_count = await main_indexer._execute_cleanup_operations(qdrant_manager, diff_result) +logger.info(f"Total cleanup: {cleanup_count} chunks") +``` + +## Integration Points + +### Enhanced Main Indexer Integration + +```python +# NEW: Comprehensive processing flow +async def process_all_documents(self) -> ProcessingStats: + # 1. Enhanced diff detection + diff_result = await diff_detector.get_changed_files() + + # 2. NEW: Automatic cleanup execution + if diff_result.chunks_to_delete: + cleanup_count = await self._execute_cleanup_operations(qdrant_manager, diff_result) + + # 3. Selective document processing + files_to_process = diff_result.new_files + diff_result.modified_files + if not files_to_process: + return self.stats # Early exit + + # 4. Standard processing pipeline + documents = self._filter_documents_by_paths(files_to_process) + results = await self._process_documents(documents) + + # 5. Enhanced metadata update + await diff_detector.mark_files_processed(processed_paths, chunks_info) +``` + +### Vector Store Consistency + +- **Before Processing**: Clean up orphaned chunks from deleted/modified files +- **During Processing**: Generate new chunks with consistent IDs +- **After Processing**: Update metadata with chunk tracking information +- **Validation**: Periodic consistency checks between metadata and vector store + +## Future Enhancements + +### Planned Improvements + +1. **Chunk ID Collection During Processing** + ```python + # Collect actual chunk IDs during document processing + chunk_info = await process_document_with_tracking(document) + # Update metadata with actual chunk IDs for precise cleanup + ``` + +2. **Advanced Cleanup Strategies** + ```python + # Age-based cleanup: Remove chunks older than X days + # Size-based cleanup: Remove largest chunks first if storage limit reached + # Performance-based cleanup: Batch multiple cleanup operations + ``` + +3. **Cleanup Verification** + ```python + # Post-cleanup validation + remaining_chunks = await qdrant_manager.count_chunks_by_document_hash(doc_hash) + assert remaining_chunks == 0, "Cleanup incomplete" + ``` + +4. **Rollback Capability** + ```python + # Optional: Backup chunks before deletion for potential rollback + # Useful for testing or when unsure about cleanup operations + ``` + +## Conclusion + +The enhanced Diff Identifier with automatic cleanup transforms the Vector Indexer into a fully consistent, efficient incremental processing system, providing: + +- **Performance**: Only process what changed (up to 92% reduction) +- **Consistency**: Automatic vector store cleanup prevents orphaned chunks +- **Reliability**: Graceful fallback and error recovery mechanisms +- **Scalability**: Efficient handling of large, frequently updated datasets +- **Transparency**: Comprehensive logging and statistics for all operations +- **Maintainability**: Clean separation of concerns and robust error handling + +The system now ensures that the vector store always accurately reflects the current dataset state, with no orphaned chunks and optimal processing efficiency. + content_hash: str # SHA-256 of file content + original_path: str # Relative path from datasets folder + file_size: int # File size in bytes + processed_at: str # ISO timestamp of processing + +class DiffResult(BaseModel): + new_files: List[str] # Files requiring processing + total_files_scanned: int # Total files discovered + previously_processed_count: int # Files already processed + is_first_run: bool # First-time execution flag + +class DiffConfig(BaseModel): + # S3 Configuration (from environment - no defaults for error detection) + s3_bucket_name: str + s3_bucket_path: str + s3_endpoint_url: str + s3_access_key_id: str + s3_secret_access_key: str + + # Service URLs + s3_ferry_url: str # S3Ferry service endpoint + + # Paths + datasets_path: str # Path to datasets folder + metadata_filename: str = "processed-metadata.json" + + # Retry Configuration + max_retries: int = 3 + max_delay_seconds: int = 8 +``` + +## Comprehensive Flow Analysis + +### High-Level Processing Pipeline + +``` +Dataset Download โ†’ Diff Identification โ†’ Selective Processing โ†’ Vector Generation โ†’ Metadata Update โ†’ Cleanup + โ†“ โ†“ โ†“ โ†“ โ†“ โ†“ + [Future Step] [Current Focus] [Filtered Docs] [Unchanged] [S3 Upload] [Volume Cleanup] +``` + +### Detailed Component Interaction Flow + +#### Phase 1: Initialization & Setup +```python +# 1. Configuration Bootstrap (main_indexer.py) +diff_config = create_diff_config() # Load from environment variables +diff_detector = DiffDetector(diff_config) + +# 2. Component Initialization (diff_detector.py) +version_manager = VersionManager(config) # DVC operations handler +s3_ferry_client = S3FerryClient(config) # S3 metadata operations +``` + +**What Happens Internally:** +1. **Environment Validation:** Checks for all required S3 and service configuration +2. **Service Discovery:** Validates S3Ferry service availability +3. **Directory Validation:** Ensures datasets folder exists and is accessible +4. **Component Wiring:** Creates fully configured component instances + +#### Phase 2: Version State Analysis +```python +# 3. DVC State Detection (version_manager.py) +is_first_run = not version_manager._is_dvc_initialized() + +if is_first_run: + version_manager.initialize_dvc() # Set up DVC with S3 remote + return DiffResult(new_files=all_files, is_first_run=True) +``` + +**First Run Scenario:** +1. **DVC Detection:** Checks for `.dvc/` folder existence in datasets directory +2. **Repository Setup:** Initializes DVC repository with `dvc init` +3. **Remote Configuration:** Configures MinIO S3 as DVC remote storage +4. **Baseline Creation:** Marks this as initial state for future comparisons +5. **Full Processing:** Returns all discovered files for complete indexing + +**Subsequent Run Detection:** +1. **DVC Validation:** Verifies existing DVC configuration integrity +2. **Remote Connectivity:** Tests connection to MinIO S3 remote +3. **Metadata Availability:** Checks for previous processing metadata +4. **Change Detection Mode:** Proceeds to differential analysis + +#### Phase 3: Current State Scanning +```python +# 4. Comprehensive File Discovery (version_manager.py) +current_files = version_manager.scan_current_files() +# Returns: Dict[content_hash, file_path] for all discovered files + +def scan_current_files(self) -> Dict[str, str]: + file_hash_map = {} + for root, _, files in os.walk(self.config.datasets_path): + for file in files: + file_path = os.path.join(root, file) + relative_path = os.path.relpath(file_path, self.config.datasets_path) + + # Calculate content hash for change detection + content_hash = self._calculate_file_hash(file_path) + file_hash_map[content_hash] = relative_path + + return file_hash_map +``` + +**File Discovery Process:** +1. **Recursive Traversal:** Walks entire datasets directory tree +2. **Content Hashing:** Calculates SHA-256 hash for each file's content +3. **Path Normalization:** Converts to relative paths for portability +4. **Hash Mapping:** Creates hash-to-path mapping for efficient lookup +5. **Metadata Collection:** Gathers file size and modification timestamps + +#### Phase 4: Historical State Retrieval +```python +# 5. Previous State Download (s3_ferry_client.py) +processed_metadata = await s3_ferry_client.download_metadata() +# Downloads from: s3://rag-search/resources/datasets/processed-metadata.json + +def download_metadata(self) -> Optional[Dict[str, Any]]: + # Create temporary file for S3Ferry transfer + with tempfile.NamedTemporaryFile(suffix='.json', delete=False) as temp_file: + temp_file_path = temp_file.name + + # Transfer S3 โ†’ FS via S3Ferry API + response = self._retry_with_backoff( + lambda: self.s3_ferry.transfer_file( + destinationFilePath=temp_file_path, + destinationStorageType="FS", + sourceFilePath=self.config.metadata_s3_path, + sourceStorageType="S3" + ) + ) + + if response.status_code == 200: + with open(temp_file_path, 'r') as f: + return json.load(f) + elif response.status_code == 404: + return None # First run - no metadata exists yet +``` + +**Metadata Retrieval Process:** +1. **Temporary File Creation:** Creates secure temp file for S3Ferry operations +2. **S3Ferry Transfer:** Uses standardized payload format for S3 โ†’ FS transfer +3. **Response Handling:** Distinguishes between success, not found, and errors +4. **JSON Parsing:** Loads structured metadata into memory +5. **Cleanup Operations:** Ensures temporary files are properly removed + +#### Phase 5: Differential Analysis +```python +# 6. Change Detection Algorithm (version_manager.py) +changed_files = version_manager.identify_changed_files(current_files, processed_metadata) + +def identify_changed_files(self, current_files: Dict[str, str], + processed_state: Optional[Dict]) -> Set[str]: + if not processed_state: + return set(current_files.values()) # All files are "new" + + processed_hashes = set(processed_state.get('processed_files', {}).keys()) + current_hashes = set(current_files.keys()) + + # Identify new and modified files + new_or_changed_hashes = current_hashes - processed_hashes + + # Convert hashes back to file paths + return {current_files[hash_val] for hash_val in new_or_changed_hashes} +``` + +**Change Detection Logic:** +1. **Hash Set Operations:** Uses set mathematics for efficient comparison +2. **New File Detection:** Identifies hashes present in current but not in processed +3. **Modification Detection:** Content hash changes indicate file modifications +4. **Deletion Handling:** Processed files no longer present are ignored (graceful) +5. **Path Resolution:** Converts hash differences back to actionable file paths + +#### Phase 6: Result Compilation & Statistics +```python +# 7. Comprehensive Result Generation (diff_detector.py) +return DiffResult( + new_files=list(changed_files), + total_files_scanned=len(current_files), + previously_processed_count=len(processed_state.get('processed_files', {})), + is_first_run=is_first_run +) +``` + +**Statistical Analysis:** +- **Processing Efficiency:** Calculate percentage of files requiring processing +- **Change Rate Metrics:** Track how much content changes between runs +- **Performance Insights:** Measure time savings from selective processing +- **Trend Analysis:** Historical view of dataset evolution patterns + +### Container Integration & Deployment Architecture + +#### Docker Volume Configuration +```yaml +# docker-compose.yml - Updated for diff identifier support + +rag-s3-ferry: + volumes: + - shared-volume:/app/shared # Cross-container communication + - cron_data:/app/data # Persistent operation data + - ./datasets:/app/datasets # Direct datasets access for S3Ferry operations + +cron-manager: + volumes: + - ./src/vector_indexer:/app/src/vector_indexer # Source code mounting + - cron_data:/app/data # Shared operational data + - shared-volume:/app/shared # Cross-container coordination + - ./datasets:/app/datasets # Direct datasets access +``` + +**Volume Strategy Rationale:** +1. **`shared-volume`:** Enables cross-container file coordination and temporary data exchange +2. **`./datasets`:** Direct mount ensures both containers see the same dataset state +3. **`cron_data`:** Persistent storage for operational metadata and logs +4. **Separation of Concerns:** S3Ferry handles transfers, cron-manager handles processing + +#### Cross-Container Communication Flow +``` +Dataset Download โ†’ [shared-volume] โ†’ diff_identifier โ†’ [datasets mount] โ†’ S3Ferry โ†’ MinIO S3 + โ†“ โ†“ โ†“ +[Future Step] [Change Detection] [Metadata Storage] + โ†“ โ†“ โ†“ + Processing โ† [datasets mount] โ† Filtered Files โ† [Version Manager] โ† [S3 Metadata] +``` + +### Phase 7: Selective Document Processing +```python +# 8. Document Filtering Integration (main_indexer.py) +if diff_result.new_files: + # Process only changed files + documents = self._filter_documents_by_paths(diff_result.new_files) + logger.info(f"Processing {len(documents)} documents from {len(diff_result.new_files)} changed files") +else: + # No changes detected - skip processing entirely + logger.info("No changes detected. Skipping processing phase.") + return ProcessingResult(processed_count=0, skipped_count=diff_result.total_files_scanned) + +# Continue with existing vector generation pipeline... +``` + +**Document Filtering Process:** +1. **Path-Based Selection:** Filter discovered documents by changed file paths +2. **Content Preservation:** Maintain document structure and metadata +3. **Processing Optimization:** Skip unchanged content while preserving relationships +4. **Quality Assurance:** Ensure filtered subset maintains processing integrity + +### Phase 8: Post-Processing State Update +```python +# 9. Metadata Update & Persistence (diff_detector.py) +async def mark_files_processed(self, file_paths: List[str]) -> bool: + # Update processed files metadata + new_metadata = self._create_updated_metadata(file_paths) + + # Upload to S3 via S3Ferry + success = await self.s3_ferry_client.upload_metadata(new_metadata) + + # Commit DVC state (optional - for advanced versioning) + if success: + self.version_manager.commit_dvc_state(f"Processed {len(file_paths)} files") + + return success + +def _create_updated_metadata(self, file_paths: List[str]) -> Dict[str, Any]: + current_files = self.version_manager.scan_current_files() + + metadata = { + "last_updated": datetime.utcnow().isoformat(), + "total_processed": len(file_paths), + "processed_files": {} + } + + # Add file metadata for each processed file + for file_path in file_paths: + file_hash = self._get_file_hash(file_path) + metadata["processed_files"][file_hash] = ProcessedFileInfo( + content_hash=file_hash, + original_path=file_path, + file_size=os.path.getsize(file_path), + processed_at=datetime.utcnow().isoformat() + ).dict() + + return metadata +``` + +**State Persistence Strategy:** +1. **Incremental Updates:** Merge new processed files with existing metadata +2. **Atomic Operations:** Ensure metadata consistency during concurrent access +3. **Timestamp Tracking:** Maintain processing history for audit and debugging +4. **Hash-Based Keys:** Use content hashes as stable identifiers across runs +5. **Rollback Safety:** Preserve previous state until new state is confirmed + +## Multi-Tier Storage Architecture + +### Layer 1: DVC Version Control Storage (Content-Addressed) +- **Location**: `s3://rag-search/resources/datasets/dvc-cache/` +- **Purpose**: Immutable file content storage with deduplication +- **Format**: Content-addressed storage (SHA-256 hashes as keys) +- **Benefits**: Automatic deduplication, integrity verification, version history + +**DVC Storage Structure:** +``` +s3://rag-search/resources/datasets/dvc-cache/ +โ”œโ”€โ”€ ab/ # First 2 chars of content hash +โ”‚ โ””โ”€โ”€ cdef123...890 # Remaining hash - actual file content +โ”œโ”€โ”€ cd/ +โ”‚ โ””โ”€โ”€ ef456...123 +โ””โ”€โ”€ .dvcignore # DVC configuration files +``` + +### Layer 2: Processing Metadata Storage (State Tracking) +- **Location**: `s3://rag-search/resources/datasets/processed-metadata.json` +- **Purpose**: Track processing state and enable incremental operations +- **Format**: Structured JSON with comprehensive file metadata +- **Access Pattern**: Download โ†’ Process โ†’ Upload (atomic updates) + +**Enhanced Metadata Structure:** +```json +{ + "schema_version": "1.0", + "last_updated": "2024-10-15T10:30:00Z", + "processing_session_id": "session_20241015_103000", + "total_processed": 150, + "total_files_scanned": 152, + "processing_statistics": { + "new_files_count": 5, + "modified_files_count": 2, + "unchanged_files_count": 145, + "processing_time_seconds": 45.7, + "efficiency_ratio": 0.95 + }, + "processed_files": { + "sha256:abc123def456...": { + "content_hash": "sha256:abc123def456...", + "original_path": "datasets/collection1/abc123/cleaned.txt", + "file_size": 1024, + "processed_at": "2024-10-15T10:30:00Z", + "processing_duration_ms": 150, + "document_count": 1, + "vector_count": 25 + }, + "sha256:def789ghi012...": { + "content_hash": "sha256:def789ghi012...", + "original_path": "datasets/collection2/def789/cleaned.txt", + "file_size": 2048, + "processed_at": "2024-10-15T10:30:15Z", + "processing_duration_ms": 280, + "document_count": 3, + "vector_count": 67 + } + }, + "system_metadata": { + "diff_identifier_version": "1.0.0", + "dvc_version": "3.55.2", + "container_id": "cron-manager-abc123", + "environment": "production" + } +} +``` + +### Layer 3: Temporary Cross-Container Storage +- **Location**: `shared-volume:/app/shared/` +- **Purpose**: Facilitate communication between rag-s3-ferry and cron-manager containers +- **Lifecycle**: Ephemeral files created during operations, cleaned up after completion +- **Use Cases**: Temporary S3Ferry payloads, processing locks, status files + +## Configuration Management + +### Environment Variables (Required - No Defaults Policy) + +The diff identifier follows a **"fail-fast"** configuration philosophy where missing environment variables cause immediate startup failure rather than silent defaults. This prevents production issues from misconfiguration. + +#### Core S3 Configuration +```bash +# MinIO S3 Backend Configuration +S3_DATA_BUCKET_NAME=rag-search # Target bucket for all data operations +S3_DATA_BUCKET_PATH=resources # Prefix path within bucket +S3_ENDPOINT_URL=http://minio:9000 # MinIO service endpoint (container network) +S3_ACCESS_KEY_ID=minioadmin # S3 access credentials +S3_SECRET_ACCESS_KEY=minioadmin # S3 secret credentials + +# S3Ferry Service Integration +S3_FERRY_URL=http://rag-s3-ferry:3000 # S3Ferry service endpoint +``` + +#### Service Discovery & Networking +```bash +# Container Network Configuration +PYTHONPATH=/app:/app/src/vector_indexer # Python module path for imports +DATASETS_PATH=/app/datasets # Mounted datasets directory path + +# Optional Performance Tuning +MAX_RETRIES=3 # S3Ferry operation retry attempts +MAX_DELAY_SECONDS=8 # Maximum backoff delay for retries +``` + +### Advanced Configuration Schema + +#### DVC Configuration (Auto-Generated) +```yaml +# .dvc/config (Created automatically during initialization) +[core] + remote = minio-s3 + +['remote "minio-s3"'] + url = s3://rag-search/resources/datasets/dvc-cache + endpointurl = http://minio:9000 + access_key_id = minioadmin + secret_access_key = minioadmin + ssl_verify = false # For local MinIO development +``` + +#### Vector Indexer Integration Configuration +```yaml +# src/vector_indexer/config/vector_indexer_config.yaml +vector_indexer: + diff_identifier: + enabled: true # Enable/disable diff identification + datasets_path: "datasets" # Relative path to datasets folder + metadata_filename: "processed-metadata.json" # S3 metadata file name + + # Performance Configuration + max_retries: 3 # Retry attempts for operations + max_delay_seconds: 8 # Exponential backoff maximum delay + + # Operational Configuration + cleanup_on_completion: true # Clean datasets folder after processing + fallback_on_error: true # Process all files if diff fails + + # Logging Configuration + log_level: "INFO" # DEBUG for detailed file operations + log_statistics: true # Include processing statistics in logs + log_file_operations: false # Log individual file operations (verbose) +``` + +### Configuration Validation & Error Handling + +#### Startup Validation Process +```python +# Configuration validation on startup +def validate_diff_config(config: DiffConfig) -> List[str]: + errors = [] + + # Required S3 configuration + if not config.s3_bucket_name: + errors.append("S3_DATA_BUCKET_NAME is required") + if not config.s3_endpoint_url: + errors.append("S3_ENDPOINT_URL is required") + + # Service connectivity validation + try: + response = requests.get(f"{config.s3_ferry_url}/health", timeout=5) + if response.status_code != 200: + errors.append(f"S3Ferry service unavailable at {config.s3_ferry_url}") + except requests.RequestException: + errors.append(f"Cannot connect to S3Ferry service at {config.s3_ferry_url}") + + return errors +``` + +#### Configuration Error Examples +```bash +# Missing Environment Variable Error +[ERROR] Missing required environment variables: S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY +[ERROR] Diff identifier cannot start without complete configuration +[ERROR] System will fall back to processing all files + +# Service Connectivity Error +[ERROR] S3Ferry service not responding at http://rag-s3-ferry:3000 +[WARN] Falling back to direct S3 operations (reduced functionality) + +# Invalid Configuration Error +[ERROR] Invalid S3 endpoint URL: invalid-url-format +[ERROR] Configuration validation failed - check .env file +``` + +## Usage Patterns & Integration + +### Production Deployment via CronManager + +#### Pipeline Script Execution +```bash +# DSL/CronManager/script/vector_indexer_pipeline.sh +export signedUrl="https://s3.amazonaws.com/datasets/daily-export.zip?signed-params" +export ENVIRONMENT="production" +export LOG_LEVEL="INFO" + +# Execute pipeline with diff identifier integration +./vector_indexer_pipeline.sh +``` + +**Pipeline Script Responsibilities:** +1. **Environment Setup:** Validates and exports required environment variables +2. **Dependency Management:** Ensures DVC v3.55.2 is installed and available +3. **Parameter Passing:** Forwards signed URL to main_indexer.py with `--signed-url` flag +4. **Error Handling:** Captures and logs any initialization or processing failures +5. **Resource Cleanup:** Ensures containers clean up temporary files and datasets + +#### Advanced Pipeline Configuration +```bash +# Enhanced pipeline execution with monitoring +export ENABLE_DIFF_IDENTIFIER="true" +export DIFF_IDENTIFIER_LOG_LEVEL="DEBUG" +export PROCESSING_TIMEOUT_MINUTES="30" +export CLEANUP_ON_FAILURE="true" + +# Execute with enhanced monitoring +./vector_indexer_pipeline.sh --enable-monitoring --diff-stats +``` + +### Development & Testing Modes + +#### Direct Python Execution (Development) +```bash +# Container execution context +cd /app +export PYTHONPATH=/app:/app/src/vector_indexer + +# Basic execution +python3 src/vector_indexer/main_indexer.py --signed-url "https://example.com/dataset.zip" + +# Debug mode with verbose logging +python3 src/vector_indexer/main_indexer.py \ + --signed-url "https://example.com/dataset.zip" \ + --log-level DEBUG \ + --enable-diff-stats + +# Dry-run mode (identify changes without processing) +python3 src/vector_indexer/main_indexer.py \ + --signed-url "https://example.com/dataset.zip" \ + --dry-run \ + --diff-only +``` + +#### Manual Component Testing +```python +# Test diff identifier components independently +from src.vector_indexer.diff_identifier import DiffDetector, create_diff_config + +# Initialize for testing +config = create_diff_config() +detector = DiffDetector(config) + +# Test change detection +diff_result = await detector.get_changed_files() +print(f"Found {len(diff_result.new_files)} changed files") + +# Test metadata operations +success = await detector.mark_files_processed(diff_result.new_files) +print(f"Metadata update successful: {success}") +``` + +### API Integration Patterns + +#### Programmatic Usage +```python +# Integration with external orchestration systems +class VectorIndexerOrchestrator: + def __init__(self): + self.diff_config = create_diff_config() + self.detector = DiffDetector(self.diff_config) + + async def process_dataset_update(self, dataset_url: str) -> ProcessingResult: + # Step 1: Download dataset (future implementation) + await self.download_dataset(dataset_url) + + # Step 2: Identify changes + diff_result = await self.detector.get_changed_files() + + if not diff_result.new_files: + return ProcessingResult(message="No changes detected", processed_count=0) + + # Step 3: Selective processing + processing_result = await self.process_files(diff_result.new_files) + + # Step 4: Update metadata + await self.detector.mark_files_processed(processing_result.processed_files) + + return processing_result +``` + +## Technical Implementation Details + +### DiffConfig Usage & Flow + +#### Configuration Object Creation +```python +# main_indexer.py - Entry point +diff_config = create_diff_config() # Creates config from environment variables +diff_detector = DiffDetector(diff_config) # Passes to main orchestrator + +# diff_detector.py - Configuration factory +config = DiffConfig( + s3_ferry_url=s3_ferry_url, # โ†’ Used by S3FerryClient + metadata_s3_path=metadata_s3_path, # โ†’ Used for S3Ferry operations + datasets_path=datasets_path, # โ†’ Used for file scanning + metadata_filename=metadata_filename, # โ†’ Used to build paths + dvc_remote_url=dvc_remote_url, # โ†’ Used by DVC setup + s3_endpoint_url=str(s3_endpoint_url), # โ†’ Used by DVC S3 config + s3_access_key_id=str(s3_access_key_id), # โ†’ Used by DVC authentication + s3_secret_access_key=str(s3_secret_access_key) # โ†’ Used by DVC authentication +) +``` + +#### Configuration Flow Through System +``` +main_indexer.py + โ†“ create_diff_config() +DiffConfig Object + โ†“ passed to +DiffDetector(config) + โ†“ self.config = config + โ†“ VersionManager(config) + โ†“ Uses: datasets_path, dvc_remote_url, s3_endpoint_url, s3_access_key_id, s3_secret_access_key + โ†“ S3FerryClient(config) + โ†“ Uses: s3_ferry_url, metadata_s3_path, max_retries, max_delay_seconds +``` + +#### Config Properties Usage Map +| **Property** | **Component** | **Specific Usage** | +|-------------|---------------|-------------------| +| `s3_ferry_url` | S3FerryClient | `S3Ferry(config.s3_ferry_url)` | +| `metadata_s3_path` | S3FerryClient | Upload/download destination path | +| `datasets_path` | VersionManager | `Path(config.datasets_path)` for file scanning | +| `metadata_filename` | DiffConfig | Used to build `metadata_s3_path` | +| `dvc_remote_url` | VersionManager | `dvc remote add rag-storage {url}` | +| `s3_endpoint_url` | VersionManager | `dvc remote modify endpointurl` | +| `s3_access_key_id` | VersionManager | `dvc remote modify access_key_id` | +| `s3_secret_access_key` | VersionManager | `dvc remote modify secret_access_key` | +| `max_retries` | S3FerryClient | Retry loop iterations | +| `max_delay_seconds` | S3FerryClient | Exponential backoff cap | + +### S3 Transfer Operations & Payloads + +#### 1. Metadata Upload (FS โ†’ S3) +**Location:** `s3_ferry_client.py:79-84` +**Trigger:** After processing files completion + +```python +# S3Ferry API Call +response = self.s3_ferry.transfer_file( + destinationFilePath="resources/datasets/processed-metadata.json", + destinationStorageType="S3", + sourceFilePath="/tmp/tmpABC123.json", # Temporary file + sourceStorageType="FS" +) +``` + +**HTTP Payload sent to S3Ferry:** +```json +POST http://rag-s3-ferry:3000 +Content-Type: application/json + +{ + "destinationFilePath": "resources/datasets/processed-metadata.json", + "destinationStorageType": "S3", + "sourceFilePath": "/tmp/tmpABC123.json", + "sourceStorageType": "FS" +} +``` + +#### 2. Metadata Download (S3 โ†’ FS) +**Location:** `s3_ferry_client.py:123-128` +**Trigger:** At start of processing to get previous state + +```python +# S3Ferry API Call +response = self.s3_ferry.transfer_file( + destinationFilePath="/tmp/tmpDEF456.json", # Temporary file + destinationStorageType="FS", + sourceFilePath="resources/datasets/processed-metadata.json", + sourceStorageType="S3" +) +``` + +**HTTP Payload sent to S3Ferry:** +```json +POST http://rag-s3-ferry:3000 +Content-Type: application/json + +{ + "destinationFilePath": "/tmp/tmpDEF456.json", + "destinationStorageType": "FS", + "sourceFilePath": "resources/datasets/processed-metadata.json", + "sourceStorageType": "S3" +} +``` + +### DVC S3 Operations & Commands + +#### DVC Initialization (First Run) +**Location:** `version_manager.py:54-70` + +```bash +# 1. Initialize DVC repository +dvc init --no-scm + +# 2. Add S3 remote storage +dvc remote add -d rag-storage s3://rag-search/resources/datasets/dvc-cache + +# 3. Configure S3 endpoint +dvc remote modify rag-storage endpointurl http://minio:9000 + +# 4. Configure S3 credentials +dvc remote modify rag-storage access_key_id minioadmin +dvc remote modify rag-storage secret_access_key minioadmin +``` + +**DVC Config File Created:** +```ini +# datasets/.dvc/config +[core] + remote = rag-storage + +['remote "rag-storage"'] + url = s3://rag-search/resources/datasets/dvc-cache + endpointurl = http://minio:9000 + access_key_id = minioadmin + secret_access_key = minioadmin +``` + +#### DVC Content Operations (After Processing) +**Location:** `version_manager.py:253-258` + +```bash +# 1. Track all files in datasets folder +dvc add . + +# 2. Upload content to S3 remote +dvc push +``` + +#### Underlying S3 API Calls Made by DVC +When `dvc push` executes, DVC makes direct S3 API calls: + +**Content Upload (PUT):** +```http +PUT /rag-search/resources/datasets/dvc-cache/ab/cdef1234567890abcdef1234567890abcdef12 HTTP/1.1 +Host: minio:9000 +Authorization: AWS4-HMAC-SHA256 Credential=minioadmin/20241015/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-date, Signature=... +Content-Type: application/octet-stream +Content-Length: 1024 + +[Binary file content] +``` + +**Existence Check (HEAD):** +```http +HEAD /rag-search/resources/datasets/dvc-cache/ab/cdef1234567890abcdef1234567890abcdef12 HTTP/1.1 +Host: minio:9000 +Authorization: AWS4-HMAC-SHA256 Credential=minioadmin/... +``` + +**Remote Listing (GET):** +```http +GET /rag-search/resources/datasets/dvc-cache?prefix=ab/ HTTP/1.1 +Host: minio:9000 +Authorization: AWS4-HMAC-SHA256 Credential=minioadmin/... +``` + +### S3 Storage Architecture + +#### Complete S3 Bucket Structure +``` +s3://rag-search/resources/datasets/ +โ”œโ”€โ”€ dvc-cache/ # DVC content-addressed storage +โ”‚ โ”œโ”€โ”€ ab/ # First 2 chars of SHA-256 hash +โ”‚ โ”‚ โ””โ”€โ”€ cdef1234567890abcdef12... # Remaining hash - actual file content +โ”‚ โ”œโ”€โ”€ cd/ +โ”‚ โ”‚ โ””โ”€โ”€ ef567890abcdef1234567890... +โ”‚ โ””โ”€โ”€ ... +โ””โ”€โ”€ processed-metadata.json # Processing state metadata (via S3Ferry) +``` + +#### Dual Access Pattern +- **DVC Operations**: Direct AWS S3 API calls with full authentication +- **Metadata Operations**: S3Ferry service with simple payloads +- **Content Deduplication**: Same file content = same hash = single storage + +### System Integration Flow + +#### Complete Processing Pipeline +``` +Environment Variables โ†’ create_diff_config() โ†’ DiffConfig + โ†“ +DiffDetector(config) โ†’ VersionManager(config) + S3FerryClient(config) + โ†“ โ†“ โ†“ +Change Detection DVC Operations Metadata Operations + โ†“ โ†“ โ†“ +File Filtering Direct S3 API S3Ferry HTTP API + โ†“ โ†“ โ†“ +Processing Content Storage State Tracking +``` + +## Real-World Processing Scenarios + +### Scenario 1: Initial System Deployment (First Run) + +**Context:** Fresh deployment with no previous processing history. + +**Execution Flow:** +``` +1. DiffDetector initializes and detects no .dvc/ folder in datasets/ +2. Calls VersionManager.initialize_dvc() to set up version control +3. Configures MinIO S3 as DVC remote storage backend +4. Scans all files in datasets/ folder (50 files discovered) +5. Returns ALL files for processing (expected behavior) +6. Post-processing: Creates initial metadata and uploads to S3 +``` + +**Detailed Logs:** +``` +[INFO] 2024-10-15 10:00:00 - Starting diff identification process... +[INFO] 2024-10-15 10:00:01 - DVC repository not found in datasets/ +[INFO] 2024-10-15 10:00:01 - Initializing DVC for first run... +[INFO] 2024-10-15 10:00:02 - DVC initialized successfully +[INFO] 2024-10-15 10:00:02 - Configuring MinIO S3 remote: s3://rag-search/resources/datasets/dvc-cache +[INFO] 2024-10-15 10:00:03 - DVC remote configured successfully +[INFO] 2024-10-15 10:00:03 - Scanning datasets folder for files... +[INFO] 2024-10-15 10:00:05 - File discovery complete: 50 files found +[INFO] 2024-10-15 10:00:05 - First run setup complete: processing all 50 files +[INFO] 2024-10-15 10:00:05 - Estimated processing time: ~15 minutes + +# ... processing occurs ... + +[INFO] 2024-10-15 10:14:32 - Processing completed: 50 files, 1,250 documents, 31,750 vectors +[INFO] 2024-10-15 10:14:33 - Uploading initial metadata to S3... +[INFO] 2024-10-15 10:14:35 - Metadata uploaded successfully: processed-metadata.json +[INFO] 2024-10-15 10:14:35 - First run baseline established for future comparisons +``` + +**Performance Metrics:** +- **Files Processed:** 50/50 (100%) +- **Processing Time:** 14m 32s +- **Efficiency Ratio:** N/A (baseline establishment) + +### Scenario 2: Daily Incremental Update (Typical Production) + +**Context:** Daily dataset update with minimal changes (5% change rate). + +**Execution Flow:** +``` +1. DiffDetector finds existing .dvc/ folder (previous run detected) +2. Downloads processed-metadata.json from S3 via S3Ferry +3. Scans current dataset: 52 files (2 new files added) +4. Compares file hashes: 50 unchanged, 2 new files +5. Returns only 2 changed files for processing +6. Processes 2 files instead of 52 (96% time savings) +``` + +**Detailed Logs:** +``` +[INFO] 2024-10-16 10:00:00 - Starting diff identification process... +[INFO] 2024-10-16 10:00:00 - Existing DVC repository detected +[INFO] 2024-10-16 10:00:01 - Downloading previous processing metadata... +[INFO] 2024-10-16 10:00:02 - Metadata downloaded: 50 previously processed files +[INFO] 2024-10-16 10:00:02 - Scanning current dataset files... +[INFO] 2024-10-16 10:00:04 - Current scan complete: 52 files found +[INFO] 2024-10-16 10:00:04 - Performing hash-based change detection... +[INFO] 2024-10-16 10:00:05 - Change analysis complete: 2 new/modified files identified +[INFO] 2024-10-16 10:00:05 - Processing efficiency: 96.1% (processing 2/52 files) + +# ... selective processing occurs ... + +[INFO] 2024-10-16 10:00:45 - Processing completed: 2 files, 48 documents, 1,240 vectors +[INFO] 2024-10-16 10:00:46 - Updating metadata with newly processed files... +[INFO] 2024-10-16 10:00:47 - Metadata updated successfully: 52 total processed files +[INFO] 2024-10-16 10:00:47 - Processing complete with 96% time savings +``` + +**Performance Metrics:** +- **Files Processed:** 2/52 (3.8%) +- **Processing Time:** 47s (vs. 15m estimated for full processing) +- **Efficiency Gain:** 96.1% time savings +- **Change Rate:** 3.8% (2 new files) + +### Scenario 3: No Changes Detected (Optimal Efficiency) + +**Context:** Dataset downloaded but no actual content changes occurred. + +**Execution Flow:** +``` +1. Normal diff identification process initiated +2. All current file hashes match processed metadata exactly +3. Zero files identified for processing +4. Skips entire processing pipeline +5. Cleans up datasets folder and exits +``` + +**Detailed Logs:** +``` +[INFO] 2024-10-17 10:00:00 - Starting diff identification process... +[INFO] 2024-10-17 10:00:01 - Downloading previous processing metadata... +[INFO] 2024-10-17 10:00:02 - Metadata downloaded: 52 previously processed files +[INFO] 2024-10-17 10:00:03 - Scanning current dataset files... +[INFO] 2024-10-17 10:00:05 - Current scan complete: 52 files found +[INFO] 2024-10-17 10:00:05 - Performing hash-based change detection... +[INFO] 2024-10-17 10:00:06 - No changes detected: all files match previous state +[INFO] 2024-10-17 10:00:06 - Processing efficiency: 100% (0 files need processing) +[INFO] 2024-10-17 10:00:06 - Skipping processing pipeline entirely +[INFO] 2024-10-17 10:00:07 - Cleaning up datasets folder... +[INFO] 2024-10-17 10:00:08 - Processing complete: no changes detected +``` + +**Performance Metrics:** +- **Files Processed:** 0/52 (0%) +- **Processing Time:** 8s (vs. 15m for full processing) +- **Efficiency Gain:** 99.9% time savings +- **Change Rate:** 0% (no changes) + +### Scenario 4: Large Dataset Update (Batch Changes) + +**Context:** Weekly comprehensive update with significant changes (30% change rate). + +**Execution Flow:** +``` +1. Dataset download includes substantial content updates +2. Hash comparison identifies 16 changed files out of 52 total +3. Processes substantial subset but still more efficient than full reprocessing +4. Updates metadata with batch of changes +``` + +**Detailed Logs:** +``` +[INFO] 2024-10-20 02:00:00 - Starting diff identification process... +[INFO] 2024-10-20 02:00:01 - Downloading previous processing metadata... +[INFO] 2024-10-20 02:00:03 - Metadata downloaded: 52 previously processed files +[INFO] 2024-10-20 02:00:03 - Scanning current dataset files... +[INFO] 2024-10-20 02:00:08 - Current scan complete: 52 files found +[INFO] 2024-10-20 02:00:08 - Performing hash-based change detection... +[INFO] 2024-10-20 02:00:10 - Change analysis complete: 16 modified files identified +[INFO] 2024-10-20 02:00:10 - Processing efficiency: 69.2% (processing 16/52 files) +[INFO] 2024-10-20 02:00:10 - Estimated processing time: ~5 minutes + +# ... batch processing occurs ... + +[INFO] 2024-10-20 02:04:45 - Processing completed: 16 files, 410 documents, 10,750 vectors +[INFO] 2024-10-20 02:04:46 - Updating metadata with batch changes... +[INFO] 2024-10-20 02:04:48 - Metadata updated successfully: 52 total processed files +[INFO] 2024-10-20 02:04:48 - Processing complete with 69% time savings +``` + +**Performance Metrics:** +- **Files Processed:** 16/52 (30.8%) +- **Processing Time:** 4m 48s (vs. 15m for full processing) +- **Efficiency Gain:** 68% time savings +- **Change Rate:** 30.8% (significant but manageable) + +### Scenario 5: Error Recovery & Fallback + +**Context:** S3Ferry service unavailable, diff identification fails gracefully. + +**Execution Flow:** +``` +1. DiffDetector attempts to download metadata via S3Ferry +2. S3Ferry service connection fails (network/service issue) +3. Graceful fallback: processes all files for safety +4. Logs failure but continues operation +5. System remains operational despite component failure +``` + +**Detailed Logs:** +``` +[INFO] 2024-10-18 10:00:00 - Starting diff identification process... +[ERROR] 2024-10-18 10:00:02 - S3Ferry connection failed: Connection refused to rag-s3-ferry:3000 +[ERROR] 2024-10-18 10:00:02 - Retry attempt 1/3 failed +[ERROR] 2024-10-18 10:00:04 - Retry attempt 2/3 failed +[ERROR] 2024-10-18 10:00:08 - Retry attempt 3/3 failed +[WARN] 2024-10-18 10:00:08 - Diff identification failed: unable to download metadata +[WARN] 2024-10-18 10:00:08 - Falling back to processing all files for safety +[INFO] 2024-10-18 10:00:09 - Fallback mode: scanning all files for processing +[INFO] 2024-10-18 10:00:11 - Fallback scan complete: 52 files will be processed + +# ... full processing occurs ... + +[INFO] 2024-10-18 10:14:50 - Processing completed in fallback mode: 52 files processed +[WARN] 2024-10-18 10:14:50 - Metadata update skipped due to S3Ferry unavailability +[INFO] 2024-10-18 10:14:50 - Processing complete despite diff identifier failure +``` + +**Performance Metrics:** +- **Files Processed:** 52/52 (100% - fallback mode) +- **Processing Time:** 14m 50s (full processing time) +- **Efficiency Gain:** 0% (fallback negates optimization) +- **Reliability:** 100% (system continues operation despite component failure) + +## Error Handling + +### Graceful Degradation + +If diff identification fails for any reason, the system falls back to processing all files: + +```python +try: + diff_result = await diff_detector.get_changed_files() +except DiffError as e: + logger.error(f"Diff identification failed: {e}") + logger.info("Falling back to processing all files") + # Process all files as safety measure +``` + +### Retry Logic + +All S3Ferry operations use exponential backoff: + +```python +# Retry delays: 0.5s, 1s, 2s, 4s, 8s (max) +await self._retry_with_backoff(operation, max_retries=3, max_delay=8) +``` + +### Missing Environment Variables + +System fails fast if required environment variables are missing: + +``` +[ERROR] Missing required environment variables: S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY +``` + +## Performance Benefits + +### Efficiency Gains + +- **First Run**: Processes all files (expected) +- **Incremental Runs**: Only processes changed files (potentially 90%+ reduction) +- **No Changes**: Skips processing entirely (near-instant completion) + +### Resource Optimization + +- **Network**: Only downloads small metadata file (vs. full dataset comparison) +- **CPU**: File hashing is single-pass and efficient +- **Storage**: Content-addressed DVC storage eliminates duplicates + +## Monitoring & Logging + +### Key Log Messages + +```bash +# Diff identification +[INFO] Starting diff identification process... +[INFO] Found 5 new/changed files out of 100 total + +# First run detection +[INFO] DVC not initialized - setting up for first run + +# No changes +[INFO] No new or changed files detected. Processing complete. + +# Fallback behavior +[ERROR] Diff identification failed: connection timeout +[INFO] Falling back to processing all files +``` + +### Statistics + +Each run provides comprehensive statistics: + +```python +DiffResult( + new_files=["datasets/collection1/abc123/cleaned.txt"], + total_files_scanned=100, + previously_processed_count=99, + is_first_run=False +) +``` + +## Troubleshooting + +### Common Issues + +1. **Missing Environment Variables** + - Check `.env` file has all required S3 variables + - Restart containers after environment changes + +2. **S3Ferry Connection Failed** + - Verify S3Ferry service is running: `docker ps | grep s3-ferry` + - Check S3Ferry logs: `docker logs rag-s3-ferry` + +3. **DVC Initialization Failed** + - Check datasets folder permissions + - Verify MinIO is accessible from container + +4. **Metadata Download Failed** + - Normal on first run (no metadata exists yet) + - Check S3 bucket permissions and credentials + +### Debug Mode + +Enable debug logging for detailed information: + +```bash +# In vector_indexer_config.yaml +logging: + level: "DEBUG" +``` + +This provides detailed file-by-file processing information and DVC command outputs. + +## Integration Points + +### Main Indexer Integration + +The diff identifier is seamlessly integrated as the first step in `main_indexer.py`: + +1. **Before**: Document discovery โ†’ Processing โ†’ Storage +2. **After**: Diff identification โ†’ Filtered document discovery โ†’ Processing โ†’ Tracking update โ†’ Storage โ†’ Cleanup + +### Document Loader Compatibility + +The existing `DocumentLoader` continues to work unchanged: +- If diff result available: Filter to specific paths +- If diff unavailable: Use existing `discover_all_documents()` + +### Future Enhancements + +- **Dataset Download**: Integration point ready for signed URL download implementation +- **Parallel Processing**: DVC operations can be parallelized for large datasets +- **Delta Sync**: Potential for incremental dataset synchronization + +## Conclusion + +The Diff Identifier transforms the Vector Indexer from a batch processing system to an efficient incremental system, providing: + +- **Performance**: Only process what changed +- **Reliability**: Graceful fallback ensures robustness +- **Scalability**: Efficient handling of large, frequently updated datasets +- **Transparency**: Comprehensive logging and statistics \ No newline at end of file diff --git a/src/vector_indexer/diff_identifier/__init__.py b/src/vector_indexer/diff_identifier/__init__.py new file mode 100644 index 0000000..ac1db30 --- /dev/null +++ b/src/vector_indexer/diff_identifier/__init__.py @@ -0,0 +1,16 @@ +"""Diff identifier module for detecting dataset changes.""" + +from diff_identifier.diff_detector import DiffDetector, create_diff_config +from diff_identifier.diff_models import DiffConfig, DiffResult, DiffError +from diff_identifier.version_manager import VersionManager +from diff_identifier.s3_ferry_client import S3FerryClient + +__all__ = [ + "DiffDetector", + "create_diff_config", + "DiffConfig", + "DiffResult", + "DiffError", + "VersionManager", + "S3FerryClient", +] diff --git a/src/vector_indexer/diff_identifier/diff_detector.py b/src/vector_indexer/diff_identifier/diff_detector.py index e69de29..a59b0af 100644 --- a/src/vector_indexer/diff_identifier/diff_detector.py +++ b/src/vector_indexer/diff_identifier/diff_detector.py @@ -0,0 +1,324 @@ +"""Main diff detector for identifying changed files.""" + +import os +from pathlib import Path +from typing import List, Optional, Dict, Any +from loguru import logger +import hashlib + +from diff_identifier.diff_models import DiffConfig, DiffError, DiffResult +from diff_identifier.version_manager import VersionManager +from dotenv import load_dotenv + +load_dotenv(".env") + + +class DiffDetector: + """Main orchestrator for diff identification.""" + + def __init__(self, config: DiffConfig): + self.config = config + self.version_manager = VersionManager(config) + + async def get_changed_files(self) -> DiffResult: + """ + Get list of files that need processing. + + Returns: + DiffResult with files to process and metadata + + Raises: + DiffError: If diff detection fails critically + """ + try: + logger.info("Starting diff identification process...") + + # First, check for existing processed files metadata (this is the source of truth) + logger.info("Checking for existing processed files metadata...") + processed_state = await self.version_manager.get_processed_files_metadata() + + # Initialize DVC if needed (but don't rely on it for first-run detection) + if not self.version_manager.is_dvc_initialized(): + logger.info("DVC not initialized - initializing now") + await self.version_manager.initialize_dvc() + + # Scan current files + logger.info("Scanning current dataset files...") + current_files = self.version_manager.scan_current_files() + + if not current_files: + logger.info("No files found in datasets directory") + return DiffResult( + new_files=[], + total_files_scanned=0, + previously_processed_count=0 + if processed_state is None + else processed_state.total_processed, + is_first_run=processed_state is None, + ) + + # Determine if this is truly a first run based on metadata existence + if processed_state is None: + logger.info("No previous metadata found - this is a first run") + return DiffResult( + new_files=list(current_files.values()), + total_files_scanned=len(current_files), + previously_processed_count=0, + is_first_run=True, + ) + + # This is an incremental run - identify all types of changes + logger.info( + f"Previous metadata found with {processed_state.total_processed} processed files" + ) + changes = self.version_manager.identify_comprehensive_changes( + current_files, processed_state + ) + + result = DiffResult( + new_files=changes["new_files"], + modified_files=changes["modified_files"], + deleted_files=changes["deleted_files"], + unchanged_files=changes["unchanged_files"], + total_files_scanned=len(current_files), + previously_processed_count=processed_state.total_processed, + is_first_run=False, + chunks_to_delete=changes["chunks_to_delete"], + estimated_cleanup_count=changes["estimated_cleanup_count"], + ) + + logger.info( + f"Diff identification complete: {len(result.new_files)} files need processing" + ) + return result + + except Exception as e: + # Log error but don't fail - fall back to processing all files + logger.error(f"Diff identification failed: {e}") + logger.info("Falling back to processing all files as safety measure") + + try: + # Get all files as fallback + current_files = self.version_manager.scan_current_files() + return DiffResult( + new_files=list(current_files.values()), + total_files_scanned=len(current_files), + previously_processed_count=0, + is_first_run=True, + ) + except Exception as fallback_error: + raise DiffError( + f"Both diff identification and fallback failed: {fallback_error}", e + ) + + async def mark_files_processed( + self, + processed_file_paths: List[str], + force_metadata_update: bool = False, + chunks_info: Optional[Dict[str, Dict[str, Any]]] = None, + ) -> None: + """ + Mark files as successfully processed and update metadata. + + Args: + processed_file_paths: List of file paths that were processed successfully + force_metadata_update: Force metadata update even if no new files processed (for cleanup operations) + chunks_info: Optional dict mapping document_hash to {"chunk_count": int} + + Raises: + DiffError: If marking files fails + """ + try: + # Handle cleanup-only scenarios (no new files processed) + if not processed_file_paths and force_metadata_update: + logger.info( + "Updating metadata for cleanup operations (no new files processed)..." + ) + await self.version_manager.update_processed_files_metadata({}) + logger.info("Metadata updated for cleanup operations") + return + + if not processed_file_paths: + logger.info("No files to mark as processed") + return + + logger.info(f"Marking {len(processed_file_paths)} files as processed...") + + # Log chunks_info received + if chunks_info: + logger.info(f"RECEIVED CHUNKS INFO: {len(chunks_info)} documents") + for doc_hash, info in chunks_info.items(): + logger.info( + f" {doc_hash[:12]}... -> {info.get('chunk_count', 0)} chunks" + ) + else: + logger.warning("No chunks_info provided to mark_files_processed") + + # Calculate hashes for processed files + processed_files: Dict[str, str] = {} + for file_path in processed_file_paths: + try: + full_path = Path(file_path) + if full_path.exists(): + # IMPORTANT: Read file exactly the same way as document_loader.py + with open(full_path, "r", encoding="utf-8") as f: + content = f.read().strip() # Match document_loader exactly + + file_hash = hashlib.sha256(content.encode("utf-8")).hexdigest() + processed_files[file_hash] = file_path + logger.debug( + f"PROCESSED FILE: {file_path} -> {file_hash[:12]}..." + ) + + # Debug: Check if this file_hash exists in chunks_info + if chunks_info and file_hash in chunks_info: + chunk_count = chunks_info[file_hash].get("chunk_count", 0) + logger.info( + f"MATCHED CHUNK INFO: {file_hash[:12]}... has {chunk_count} chunks" + ) + elif chunks_info: + logger.warning( + f"NO MATCH: {file_hash[:12]}... not found in chunks_info" + ) + logger.info( + f" Available chunks_info keys: {[k[:12] + '...' for k in chunks_info.keys()]}" + ) + + else: + logger.warning(f"Processed file not found: {file_path}") + except Exception as e: + logger.warning(f"Failed to hash processed file {file_path}: {e}") + + if not processed_files: + logger.warning("No valid processed files to record") + return + + # Update metadata + await self.version_manager.update_processed_files_metadata( + processed_files, chunks_info + ) + + # Commit to DVC if initialized + if self.version_manager.is_dvc_initialized(): + await self.version_manager.commit_dvc_changes() + + logger.info( + f"Successfully marked {len(processed_files)} files as processed" + ) + + except Exception as e: + raise DiffError(f"Failed to mark files as processed: {str(e)}", e) + + async def _handle_first_run(self) -> DiffResult: + """ + Handle first run setup. + + Returns: + DiffResult for first run + + Raises: + DiffError: If first run setup fails + """ + try: + logger.info("Setting up DVC for first run...") + + # Initialize DVC + await self.version_manager.initialize_dvc() + + # Get all files for processing + current_files = self.version_manager.scan_current_files() + + logger.info( + f"First run setup complete: {len(current_files)} files to process" + ) + + return DiffResult( + new_files=list(current_files.values()), + total_files_scanned=len(current_files), + previously_processed_count=0, + is_first_run=True, + ) + + except Exception as e: + raise DiffError(f"First run setup failed: {str(e)}", e) + + +def create_diff_config() -> DiffConfig: + """ + Create DiffConfig from environment variables. + + Hybrid approach: + - S3Ferry handles metadata operations (processed files tracking) + - DVC needs direct S3 access for version control operations + + Returns: + DiffConfig instance + + Raises: + DiffError: If required environment variables are missing + """ + try: + # S3Ferry Configuration + s3_ferry_url = os.getenv("S3_FERRY_URL") + if not s3_ferry_url: + raise DiffError("Missing required environment variable: S3_FERRY_URL") + + # Path configurations + datasets_path = os.getenv("DATASETS_PATH") + if not datasets_path: + raise DiffError("Missing required environment variable: DATASETS_PATH") + metadata_filename = os.getenv("METADATA_FILENAME") + if not metadata_filename: + raise DiffError("Missing required environment variable: METADATA_FILENAME") + + # S3 configuration (required for DVC operations) + s3_bucket_name = os.getenv("S3_DATA_BUCKET_NAME") + s3_bucket_path = os.getenv("S3_BUCKET_PATH") + s3_endpoint_url = os.getenv("S3_ENDPOINT_URL") + s3_access_key_id = os.getenv("S3_ACCESS_KEY_ID") + s3_secret_access_key = os.getenv("S3_SECRET_ACCESS_KEY") + + # Validate required S3 credentials for DVC + if not all( + [s3_bucket_name, s3_endpoint_url, s3_access_key_id, s3_secret_access_key] + ): + missing = [ + var + for var, val in [ + ("S3_DATA_BUCKET_NAME", s3_bucket_name), + ("S3_ENDPOINT_URL", s3_endpoint_url), + ("S3_ACCESS_KEY_ID", s3_access_key_id), + ("S3_SECRET_ACCESS_KEY", s3_secret_access_key), + ] + if not val + ] + raise DiffError( + f"Missing required S3 environment variables for DVC: {', '.join(missing)}" + ) + + # Build paths + # S3Ferry is already configured with bucket context, so no need for s3_bucket_path prefix + metadata_s3_path = f"datasets/{metadata_filename}" + dvc_remote_url = f"s3://{s3_bucket_name}/{s3_bucket_path}/datasets/dvc-cache" + + config = DiffConfig( + s3_ferry_url=s3_ferry_url, + metadata_s3_path=metadata_s3_path, + datasets_path=datasets_path, + metadata_filename=metadata_filename, + dvc_remote_url=dvc_remote_url, + s3_endpoint_url=str(s3_endpoint_url), + s3_access_key_id=str(s3_access_key_id), + s3_secret_access_key=str(s3_secret_access_key), + ) + + logger.info("Diff configuration loaded successfully") + logger.debug(f"S3Ferry URL: {config.s3_ferry_url}") + logger.debug(f"Metadata S3 Path: {config.metadata_s3_path}") + logger.debug(f"DVC Remote URL: {config.dvc_remote_url}") + logger.debug(f"Datasets Path: {config.datasets_path}") + + return config + + except Exception as e: + raise DiffError(f"Failed to create diff configuration: {str(e)}", e) diff --git a/src/vector_indexer/diff_identifier/diff_models.py b/src/vector_indexer/diff_identifier/diff_models.py index e69de29..6ec3161 100644 --- a/src/vector_indexer/diff_identifier/diff_models.py +++ b/src/vector_indexer/diff_identifier/diff_models.py @@ -0,0 +1,102 @@ +"""Data models for diff identifier.""" + +from typing import Dict, List, Optional, Any +from pydantic import BaseModel, Field + + +class ProcessedFileInfo(BaseModel): + """Information about a processed file.""" + + content_hash: str = Field(..., description="SHA256 hash of file content") + original_path: str = Field(..., description="Original path in datasets folder") + file_size: int = Field(..., description="File size in bytes") + processed_at: str = Field(..., description="ISO timestamp when file was processed") + chunk_count: int = Field( + default=0, description="Number of chunks created from this file" + ) + + +class DiffResult(BaseModel): + """Result of diff identification process.""" + + new_files: List[str] = Field(..., description="List of new file paths to process") + modified_files: List[str] = Field( + default_factory=list, description="List of modified file paths to reprocess" + ) + deleted_files: List[str] = Field( + default_factory=list, + description="List of deleted file paths (chunks to remove)", + ) + unchanged_files: List[str] = Field( + default_factory=list, + description="List of unchanged file paths (skip processing)", + ) + + total_files_scanned: int = Field( + ..., description="Total files found in current scan" + ) + previously_processed_count: int = Field( + ..., description="Number of previously processed files" + ) + is_first_run: bool = Field( + ..., description="Whether this is the first time running" + ) + + # Cleanup metadata + chunks_to_delete: Dict[str, str] = Field( + default_factory=dict, + description="Map of document_hash to original_path for deletion", + ) + estimated_cleanup_count: int = Field( + default=0, description="Total estimated chunks to be removed" + ) + + +class VersionState(BaseModel): + """Version state information.""" + + last_updated: str = Field(..., description="ISO timestamp of last update") + processed_files: Dict[str, ProcessedFileInfo] = Field( + ..., description="Map of hash to file info" + ) + total_processed: int = Field(..., description="Total number of processed files") + processing_stats: Dict[str, Any] = Field( + default_factory=dict, description="Statistics from last processing run" + ) + + +class DiffConfig(BaseModel): + """Configuration for diff identifier.""" + + # S3Ferry Configuration (handles metadata operations) + s3_ferry_url: str = Field(..., description="S3Ferry service URL") + + # Metadata Configuration + metadata_s3_path: str = Field(..., description="Full S3 path for metadata file") + + # DVC Configuration (requires direct S3 access for version control) + datasets_path: str = Field(..., description="Path to datasets folder") + metadata_filename: str = Field( + default="processed-metadata.json", description="Metadata file name" + ) + + # DVC S3 Remote Configuration (minimal - only for DVC operations) + dvc_remote_url: str = Field(..., description="DVC S3 remote URL") + s3_endpoint_url: str = Field(..., description="S3 endpoint URL for DVC") + s3_access_key_id: str = Field(..., description="S3 access key for DVC") + s3_secret_access_key: str = Field(..., description="S3 secret key for DVC") + + # Retry Configuration + max_retries: int = Field(default=3, description="Maximum retry attempts") + max_delay_seconds: int = Field( + default=8, description="Maximum delay between retries" + ) + + +class DiffError(Exception): + """Custom exception for diff identification errors.""" + + def __init__(self, message: str, cause: Optional[Exception] = None): + self.message = message + self.cause = cause + super().__init__(self.message) diff --git a/src/vector_indexer/diff_identifier/s3_ferry_client.py b/src/vector_indexer/diff_identifier/s3_ferry_client.py new file mode 100644 index 0000000..28481f5 --- /dev/null +++ b/src/vector_indexer/diff_identifier/s3_ferry_client.py @@ -0,0 +1,303 @@ +"""S3Ferry client for file transfer operations.""" + +import asyncio +import json +import time +from typing import Any, Dict, Optional +import requests +from loguru import logger + +from diff_identifier.diff_models import DiffConfig, DiffError +from constants import GET_S3_FERRY_PAYLOAD + + +class S3Ferry: + """Client for interacting with S3Ferry service.""" + + def __init__(self, url: str): + self.url = url + + def transfer_file( + self, + destinationFilePath: str, + destinationStorageType: str, + sourceFilePath: str, + sourceStorageType: str, + ) -> requests.Response: + """ + Transfer file using S3Ferry service. + + Args: + destinationFilePath: Path where file should be stored + destinationStorageType: "S3" or "FS" (filesystem) + sourceFilePath: Path of source file + sourceStorageType: "S3" or "FS" (filesystem) + + Returns: + requests.Response: Response from S3Ferry service + """ + payload = GET_S3_FERRY_PAYLOAD( + destinationFilePath, + destinationStorageType, + sourceFilePath, + sourceStorageType, + ) + + response = requests.post(self.url, json=payload) + + return response + + +class S3FerryClient: + """High-level client for S3Ferry operations with metadata handling. + + S3Ferry service handles all S3 configuration internally. + This client only needs to know the S3Ferry URL and metadata paths. + """ + + def __init__(self, config: DiffConfig): + self.config = config + self.s3_ferry = S3Ferry(config.s3_ferry_url) + + async def __aenter__(self): + """Async context manager entry.""" + return self + + async def __aexit__( + self, + exc_type: Optional[type], + exc_val: Optional[BaseException], + exc_tb: Optional[object], + ) -> None: + """Async context manager exit.""" + pass + + async def upload_metadata(self, metadata: Dict[str, Any]) -> bool: + """ + Upload metadata to S3 via S3Ferry. + + Args: + metadata: Dictionary containing metadata to upload + + Returns: + True if successful, False otherwise + + Raises: + DiffError: If upload fails + """ + try: + # Create temporary file with metadata (run in thread pool) + temp_file_path = await asyncio.to_thread( + self._create_temp_metadata_file, metadata + ) + + try: + # Transfer from FS to S3 using S3Ferry (run in thread pool) + # Convert absolute path to S3Ferry-relative path + s3ferry_source_path = self._convert_to_s3ferry_path(temp_file_path) + + response = await asyncio.to_thread( + self._retry_with_backoff, + lambda: self.s3_ferry.transfer_file( + destinationFilePath=self.config.metadata_s3_path, + destinationStorageType="S3", + sourceFilePath=s3ferry_source_path, + sourceStorageType="FS", + ), + ) + + if response.status_code in [ + 200, + 201, + ]: # Accept both 200 OK and 201 Created + logger.info( + f"Metadata uploaded successfully to {self.config.metadata_s3_path} (status: {response.status_code})" + ) + return True + else: + logger.error( + f"S3Ferry upload failed: {response.status_code} - {response.text}" + ) + return False + + finally: + # Clean up temporary file (run in thread pool) + await asyncio.to_thread(self._cleanup_temp_file, temp_file_path) + + except Exception as e: + raise DiffError(f"Failed to upload metadata: {str(e)}", e) + + async def download_metadata(self) -> Optional[Dict[str, Any]]: + """ + Download metadata from S3 via S3Ferry. + + Returns: + Dict containing metadata or None if not found + + Raises: + DiffError: If download fails (except for file not found) + """ + try: + # Create temporary file for download (run in thread pool) + temp_file_path = await asyncio.to_thread(self._create_temp_file) + + try: + # Transfer from S3 to FS using S3Ferry (run in thread pool) + # Convert absolute path to S3Ferry-relative path + s3ferry_dest_path = self._convert_to_s3ferry_path(temp_file_path) + + response = await asyncio.to_thread( + self._retry_with_backoff, + lambda: self.s3_ferry.transfer_file( + destinationFilePath=s3ferry_dest_path, + destinationStorageType="FS", + sourceFilePath=self.config.metadata_s3_path, + sourceStorageType="S3", + ), + ) + + if response.status_code in [ + 200, + 201, + ]: # Accept both 200 OK and 201 Created + # Read metadata from downloaded file (run in thread pool) + metadata = await asyncio.to_thread( + self._read_metadata_from_file, temp_file_path + ) + logger.info( + f"Metadata downloaded successfully from {self.config.metadata_s3_path} (status: {response.status_code})" + ) + return metadata + elif response.status_code == 404: + logger.info( + "No previous metadata found - this appears to be the first run" + ) + return None + else: + logger.error( + f"S3Ferry download failed: {response.status_code} - {response.text}" + ) + return None + + finally: + # Clean up temporary file (run in thread pool) + await asyncio.to_thread(self._cleanup_temp_file, temp_file_path) + + except json.JSONDecodeError as e: + raise DiffError(f"Failed to parse downloaded metadata JSON: {str(e)}", e) + except Exception as e: + # Don't raise for file not found - it's expected on first run + logger.warning(f"Failed to download metadata (may be first run): {str(e)}") + return None + + def _create_temp_metadata_file(self, metadata: Dict[str, Any]) -> str: + """Create a temporary file with metadata content in shared folder.""" + import os + import uuid + + # Create temp file in shared folder accessible by both containers + shared_dir = "/app/shared" + os.makedirs(shared_dir, exist_ok=True) + + temp_filename = f"temp_metadata_{uuid.uuid4().hex[:8]}.json" + temp_file_path = os.path.join(shared_dir, temp_filename) + + with open(temp_file_path, "w") as temp_file: + json.dump(metadata, temp_file, indent=2) + + # Set broad permissions so S3Ferry can read the file + os.chmod(temp_file_path, 0o666) # rw-rw-rw- + + return temp_file_path + + def _create_temp_file(self) -> str: + """Create an empty temporary file in shared folder.""" + import os + import uuid + + # Create temp file in shared folder accessible by both containers + shared_dir = "/app/shared" + os.makedirs(shared_dir, exist_ok=True) + + temp_filename = f"temp_download_{uuid.uuid4().hex[:8]}.json" + temp_file_path = os.path.join(shared_dir, temp_filename) + + # Create empty file + with open(temp_file_path, "w"): + pass # Create empty file + + # Set broad permissions so S3Ferry can write to the file + os.chmod(temp_file_path, 0o666) # rw-rw-rw- + + return temp_file_path + + def _read_metadata_from_file(self, file_path: str) -> Dict[str, Any]: + """Read metadata from a file.""" + with open(file_path, "r") as f: + return json.load(f) + + def _convert_to_s3ferry_path(self, absolute_path: str) -> str: + """Convert absolute path to S3Ferry-relative path. + + S3Ferry expects paths relative to /app/ working directory. + Converts: /app/shared/filename.json -> shared/filename.json + """ + if absolute_path.startswith("/app/"): + return absolute_path[5:] # Remove '/app/' prefix + return absolute_path + + def _cleanup_temp_file(self, file_path: str) -> None: + """Clean up a temporary file.""" + import os + + try: + os.unlink(file_path) + except Exception as cleanup_error: + logger.warning(f"Failed to cleanup temp file {file_path}: {cleanup_error}") + + def _retry_with_backoff(self, operation: Any) -> requests.Response: + """ + Retry an operation with exponential backoff. + + Args: + operation: Operation to retry + + Returns: + Response from the operation + + Raises: + DiffError: If all retries fail + """ + last_exception = None + + for attempt in range(self.config.max_retries): + try: + response = operation() + + # Consider non-2xx responses as failures for retry purposes + if response.status_code >= 400: + if attempt == self.config.max_retries - 1: + return response # Last attempt - return the error response + + delay = min(1 * (2**attempt), self.config.max_delay_seconds) + time.sleep(delay) + continue + + return response + + except Exception as e: + last_exception = e + + if attempt == self.config.max_retries - 1: + raise DiffError( + f"Operation failed after {self.config.max_retries} attempts: {str(e)}", + e, + ) + + delay = min(1 * (2**attempt), self.config.max_delay_seconds) + time.sleep(delay) + + raise DiffError( + f"Operation failed after {self.config.max_retries} attempts: {str(last_exception)}", + last_exception, + ) diff --git a/src/vector_indexer/diff_identifier/version_manager.py b/src/vector_indexer/diff_identifier/version_manager.py index e69de29..8ef23db 100644 --- a/src/vector_indexer/diff_identifier/version_manager.py +++ b/src/vector_indexer/diff_identifier/version_manager.py @@ -0,0 +1,522 @@ +"""Version manager for DVC operations and metadata handling.""" + +import asyncio +import hashlib +from datetime import datetime +from pathlib import Path +from typing import Dict, List, Optional, Set, Any +from loguru import logger + +from diff_identifier.diff_models import ( + DiffConfig, + DiffError, + ProcessedFileInfo, + VersionState, +) +from diff_identifier.s3_ferry_client import S3FerryClient + + +class VersionManager: + """Manages DVC operations and version tracking.""" + + def __init__(self, config: DiffConfig): + self.config = config + self.datasets_path = Path(config.datasets_path) + + async def __aenter__(self): + """Async context manager entry.""" + return self + + async def __aexit__( + self, + exc_type: Optional[type], + exc_val: Optional[BaseException], + exc_tb: Optional[object], + ) -> None: + """Async context manager exit.""" + pass + + def is_dvc_initialized(self) -> bool: + """Check if DVC is initialized in datasets directory.""" + dvc_dir = self.datasets_path / ".dvc" + return dvc_dir.exists() and dvc_dir.is_dir() + + async def initialize_dvc(self) -> None: + """ + Initialize DVC in datasets directory with S3 remote. + + Raises: + DiffError: If DVC initialization fails + """ + try: + logger.info("Initializing DVC in datasets directory...") + + # Ensure datasets directory exists + self.datasets_path.mkdir(parents=True, exist_ok=True) + + # Initialize DVC (no SCM integration) + await self._run_dvc_command(["dvc", "init", "--no-scm"]) + + # Add S3 remote + remote_url = self.config.dvc_remote_url + logger.info(f"Adding DVC remote: {remote_url}") + await self._run_dvc_command( + ["dvc", "remote", "add", "-d", "rag-storage", remote_url] + ) + + # Configure S3 credentials + await self._run_dvc_command( + [ + "dvc", + "remote", + "modify", + "rag-storage", + "endpointurl", + self.config.s3_endpoint_url, + ] + ) + await self._run_dvc_command( + [ + "dvc", + "remote", + "modify", + "rag-storage", + "access_key_id", + self.config.s3_access_key_id, + ] + ) + await self._run_dvc_command( + [ + "dvc", + "remote", + "modify", + "rag-storage", + "secret_access_key", + self.config.s3_secret_access_key, + ] + ) + + logger.info("DVC initialized successfully") + + except Exception as e: + raise DiffError(f"Failed to initialize DVC: {str(e)}", e) + + async def get_processed_files_metadata(self) -> Optional[VersionState]: + """ + Download and parse processed files metadata from S3. + + Returns: + VersionState if metadata exists, None if first run + + Raises: + DiffError: If metadata exists but cannot be parsed + """ + try: + async with S3FerryClient(self.config) as s3_client: + metadata_dict = await s3_client.download_metadata() + + if metadata_dict is None: + return None + + # Parse metadata into VersionState + return VersionState( + last_updated=metadata_dict["last_updated"], + processed_files={ + file_hash: ProcessedFileInfo(**file_info) + for file_hash, file_info in metadata_dict[ + "processed_files" + ].items() + }, + total_processed=metadata_dict.get( + "total_processed", len(metadata_dict["processed_files"]) + ), + ) + + except Exception as e: + raise DiffError(f"Failed to get processed files metadata: {str(e)}", e) + + async def update_processed_files_metadata( + self, + processed_files: Dict[str, str], + chunks_info: Optional[Dict[str, Dict[str, Any]]] = None, + ) -> None: + """ + Update processed files metadata and upload to S3. + + Args: + processed_files: Dict mapping file hash to file path for newly processed files + chunks_info: Optional dict mapping file_hash to {"chunk_count": int} + + Raises: + DiffError: If metadata update fails + """ + try: + # Get existing metadata or create new + existing_state = await self.get_processed_files_metadata() + processed_files_dict: Dict[str, ProcessedFileInfo] = ( + existing_state.processed_files.copy() if existing_state else {} + ) + processing_stats: Dict[str, Any] = ( + existing_state.processing_stats.copy() + if existing_state and existing_state.processing_stats + else {} + ) + + # Handle cleanup-only operation + if not processed_files and existing_state: + current_files = self.scan_current_files() + current_hashes: Set[str] = set(current_files.keys()) + deleted_count = sum( + 1 for h in processed_files_dict if h not in current_hashes + ) + processed_files_dict = { + h: info + for h, info in processed_files_dict.items() + if h in current_hashes + } + if deleted_count > 0: + logger.info(f"Removed {deleted_count} deleted files from metadata") + processing_stats["last_run_deleted_files"] = deleted_count + + # Build path-to-hash map for deduplication + path_to_hash: Dict[str, str] = { + info.original_path: h for h, info in processed_files_dict.items() + } + current_time = datetime.now().isoformat() + + # Add/update new and modified files + for file_hash, file_path in processed_files.items(): + file_stats = Path(file_path).stat() + + # Remove old entry if file was modified + if file_path in path_to_hash and path_to_hash[file_path] != file_hash: + old_hash = path_to_hash[file_path] + del processed_files_dict[old_hash] + logger.info( + f"DEDUPLICATING: {file_path} (old: {old_hash[:12]}..., new: {file_hash[:12]}...)" + ) + + # Get chunk count + chunk_count = ( + chunks_info.get(file_hash, {}).get("chunk_count", 0) + if chunks_info + else 0 + ) + if chunks_info and file_hash in chunks_info: + logger.info(f"Found {chunk_count} chunks for {file_hash[:12]}...") + + # Add/update file entry + processed_files_dict[file_hash] = ProcessedFileInfo( + content_hash=file_hash, + original_path=file_path, + file_size=file_stats.st_size, + processed_at=current_time, + chunk_count=chunk_count, + ) + path_to_hash[file_path] = file_hash + + # Update stats and create new state + if processed_files: + processing_stats["last_run_new_files"] = len(processed_files) + processing_stats["last_run_timestamp"] = current_time + + new_state = VersionState( + last_updated=current_time, + processed_files=processed_files_dict, + total_processed=len(processed_files_dict), + processing_stats=processing_stats, + ) + + # Upload to S3 + metadata_dict = { + "last_updated": new_state.last_updated, + "total_processed": new_state.total_processed, + "processing_stats": new_state.processing_stats, + "processed_files": { + fh: { + "content_hash": fi.content_hash, + "original_path": fi.original_path, + "file_size": fi.file_size, + "processed_at": fi.processed_at, + "chunk_count": fi.chunk_count, + } + for fh, fi in new_state.processed_files.items() + }, + } + + async with S3FerryClient(self.config) as s3_client: + if not await s3_client.upload_metadata(metadata_dict): + raise DiffError("Failed to upload metadata to S3") + + logger.info( + f"Updated processed files metadata: {len(processed_files)} new files" + ) + + except DiffError: + raise + except Exception as e: + raise DiffError(f"Failed to update processed files metadata: {str(e)}", e) + + def scan_current_files(self) -> Dict[str, str]: + """ + Scan datasets directory and calculate file hashes. + + Returns: + Dict mapping file hash to file path + + Raises: + DiffError: If file scanning fails + """ + try: + files_map: Dict[str, str] = {} + + if not self.datasets_path.exists(): + logger.warning(f"Datasets path does not exist: {self.datasets_path}") + return files_map + + # Find all cleaned.txt files + cleaned_files = list(self.datasets_path.glob("**/cleaned.txt")) + logger.info(f"Found {len(cleaned_files)} files to scan") + + for cleaned_file in cleaned_files: + try: + # Calculate file hash consistently with document_loader.py + # Use text mode and encode to match document processing pipeline + with open(cleaned_file, "r", encoding="utf-8") as f: + content = f.read().strip() + file_hash = hashlib.sha256(content.encode("utf-8")).hexdigest() + + # Store relative path from datasets directory + relative_path = str( + cleaned_file.relative_to(self.datasets_path.parent) + ) + files_map[file_hash] = relative_path + + logger.debug( + f"Scanned file: {relative_path} -> {file_hash[:12]}..." + ) + + except Exception as e: + logger.warning(f"Failed to process file {cleaned_file}: {e}") + continue + + logger.info(f"Successfully scanned {len(files_map)} files") + return files_map + + except Exception as e: + raise DiffError(f"Failed to scan current files: {str(e)}", e) + + def identify_comprehensive_changes( + self, current_files: Dict[str, str], processed_state: Optional[VersionState] + ) -> Dict[str, Any]: + """ + Identify all types of file changes: new, modified, deleted, unchanged. + + Args: + current_files: Current files map (hash -> path) + processed_state: Previously processed state + + Returns: + Dict with lists of different file change types and cleanup information + """ + if processed_state is None: + # First run - all files are new + logger.info("First run detected - all files are new") + return { + "new_files": list(current_files.values()), + "modified_files": [], + "deleted_files": [], + "unchanged_files": [], + "chunks_to_delete": {}, + "estimated_cleanup_count": 0, + } + + # Initialize result lists + new_files: List[str] = [] + modified_files: List[str] = [] + deleted_files: List[str] = [] + unchanged_files: List[str] = [] + chunks_to_delete: Dict[str, str] = {} + total_chunks_to_delete = 0 + + # Create lookup maps for efficient searching + current_hash_to_path: Dict[str, str] = current_files # hash -> path + processed_hash_to_info: Dict[str, ProcessedFileInfo] = ( + processed_state.processed_files + ) # hash -> ProcessedFileInfo + processed_path_to_hash: Dict[str, str] = { + info.original_path: hash + for hash, info in processed_state.processed_files.items() + } # path -> hash + + # 1. Find deleted files (in processed_state but not in current scan) + logger.debug("Identifying deleted files...") + for old_hash, old_info in processed_hash_to_info.items(): + if old_hash not in current_hash_to_path: + deleted_files.append(old_info.original_path) + # Use content hash (old_hash) as document_hash for cleanup - now they match! + chunks_to_delete[old_hash] = old_info.original_path + # Estimate chunks to delete (use chunk_count if available, otherwise assume some exist) + estimated_chunks = max( + old_info.chunk_count, 1 + ) # Assume at least 1 chunk if processed before + total_chunks_to_delete += estimated_chunks + logger.debug( + f"Deleted file: {old_info.original_path} (content_hash/document_hash: {old_hash[:12]}..., estimated chunks: {estimated_chunks})" + ) + + # 2. Find new, modified, and unchanged files + logger.debug("Identifying new, modified, and unchanged files...") + for current_hash, current_path in current_hash_to_path.items(): + if current_hash in processed_hash_to_info: + # File exists with same content hash - unchanged + unchanged_files.append(current_path) + logger.debug(f"Unchanged file: {current_path}") + else: + # Check if this is a modified file (same path, different hash) + if current_path in processed_path_to_hash: + old_hash = processed_path_to_hash[current_path] + old_info = processed_hash_to_info[old_hash] + modified_files.append(current_path) + # Mark old chunks for deletion - use content hash (old_hash) as document_hash + chunks_to_delete[old_hash] = old_info.original_path + total_chunks_to_delete += max(old_info.chunk_count, 1) + logger.debug( + f"Modified file: {current_path} (old_content_hash/document_hash: {old_hash[:12]}..., new_content_hash: {current_hash[:12]}..., estimated old chunks: {max(old_info.chunk_count, 1)})" + ) + else: + # Completely new file + new_files.append(current_path) + logger.debug(f"New file: {current_path}") + + # Log summary + logger.info("COMPREHENSIVE DIFF ANALYSIS COMPLETE:") + logger.info(f"New files: {len(new_files)}") + logger.info(f"Modified files: {len(modified_files)}") + logger.info(f"Deleted files: {len(deleted_files)}") + logger.info(f"Unchanged files: {len(unchanged_files)}") + logger.info(f"Total chunks to cleanup: {total_chunks_to_delete}") + + return { + "new_files": new_files, + "modified_files": modified_files, + "deleted_files": deleted_files, + "unchanged_files": unchanged_files, + "chunks_to_delete": chunks_to_delete, + "estimated_cleanup_count": total_chunks_to_delete, + } + + def identify_changed_files( + self, current_files: Dict[str, str], processed_state: Optional[VersionState] + ) -> Set[str]: + """ + Legacy method - kept for backward compatibility. + Use identify_comprehensive_changes for new functionality. + + Args: + current_files: Current files map (hash -> path) + processed_state: Previously processed state + + Returns: + Set of file paths that need processing + """ + changes = self.identify_comprehensive_changes(current_files, processed_state) + # Return new + modified files (files that need processing) + all_changed: List[str] = changes["new_files"] + changes["modified_files"] + return set(all_changed) + + async def commit_dvc_changes(self) -> None: + """ + Commit current datasets state to DVC and push to remote. + + Raises: + DiffError: If DVC operations fail + """ + try: + # Add all cleaned.txt files to DVC tracking instead of using "." + logger.info("Adding files to DVC tracking...") + + # Find all cleaned.txt files relative to datasets directory + cleaned_files = list(self.datasets_path.glob("**/cleaned.txt")) + if cleaned_files: + # Add each file individually using relative paths + for cleaned_file in cleaned_files: + try: + # Get relative path from datasets directory + relative_path = cleaned_file.relative_to(self.datasets_path) + logger.debug(f"Adding file to DVC: {relative_path}") + await self._run_dvc_command(["dvc", "add", str(relative_path)]) + except Exception as e: + logger.warning(f"Failed to add {cleaned_file} to DVC: {e}") + # Continue with other files + continue + + logger.info(f"Added {len(cleaned_files)} files to DVC tracking") + else: + logger.warning("No cleaned.txt files found to add to DVC") + + # Push to remote storage + logger.info("Pushing to DVC remote storage...") + await self._run_dvc_command(["dvc", "push"]) + + logger.info("DVC commit completed successfully") + + except Exception as e: + raise DiffError(f"Failed to commit DVC changes: {str(e)}", e) + + async def _run_dvc_command(self, command: List[str]) -> str: + """ + Run DVC command asynchronously. + + Args: + command: DVC command as list of strings + + Returns: + Command output + + Raises: + DiffError: If command fails + """ + try: + logger.debug(f"Running DVC command: {' '.join(command)}") + + # Ensure DVC commands run from the datasets directory + cwd = str(self.datasets_path.resolve()) + logger.debug(f"Running DVC command in directory: {cwd}") + logger.debug(f"datasets_path: {self.datasets_path}") + logger.debug(f"datasets_path.resolve(): {self.datasets_path.resolve()}") + logger.debug(f"datasets_path exists: {self.datasets_path.exists()}") + + process = await asyncio.create_subprocess_exec( + *command, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + cwd=cwd, + ) + + stdout, stderr = await process.communicate() + + if process.returncode != 0: + error_msg = stderr.decode().strip() if stderr else "Unknown error" + logger.error( + f"DVC command failed with return code {process.returncode}" + ) + logger.error(f"Command: {' '.join(command)}") + logger.error(f"Working directory: {cwd}") + logger.error(f"Stdout: {stdout.decode().strip()}") + logger.error(f"Stderr: {error_msg}") + raise DiffError( + f"DVC command failed: {' '.join(command)} - {error_msg}" + ) + + output = stdout.decode().strip() + logger.debug(f"DVC command output: {output}") + + return output + + except Exception as e: + if isinstance(e, DiffError): + raise + raise DiffError( + f"Failed to run DVC command {' '.join(command)}: {str(e)}", e + ) diff --git a/src/vector_indexer/document_loader.py b/src/vector_indexer/document_loader.py index 39ed7ba..a77142b 100644 --- a/src/vector_indexer/document_loader.py +++ b/src/vector_indexer/document_loader.py @@ -1,5 +1,6 @@ """Document loader for scanning and loading documents from datasets folder.""" +import hashlib import json from pathlib import Path from typing import List @@ -69,25 +70,39 @@ def discover_all_documents(self) -> List[DocumentInfo]: else: collection_name = collection_dir.name - document_hash = hash_dir.name + # This ensures document_hash is always the SHA-256 of file content + try: + with open(cleaned_file, "r", encoding="utf-8") as f: + content = f.read().strip() + + # Calculate SHA-256 hash of content (same method used everywhere) + content_hash = hashlib.sha256(content.encode("utf-8")).hexdigest() + + logger.debug( + f"Calculated content hash for {cleaned_file.name}: {content_hash[:12]}..." + ) + + except Exception as e: + logger.warning(f"Failed to calculate hash for {cleaned_file}: {e}") + continue # Check metadata file exists metadata_file = hash_dir / self.config.metadata_file if metadata_file.exists(): documents.append( DocumentInfo( - document_hash=document_hash, + document_hash=content_hash, # Use content hash consistently cleaned_txt_path=str(cleaned_file), source_meta_path=str(metadata_file), dataset_collection=collection_name, ) ) logger.debug( - f"Found document: {document_hash} in collection: {collection_name}" + f"Found document: {content_hash[:12]}... in collection: {collection_name}" ) else: logger.warning( - f"Skipping document {document_hash}: missing {self.config.metadata_file}" + f"Skipping document in {hash_dir.name}: missing {self.config.metadata_file}" ) logger.info(f"Discovered {len(documents)} documents for processing") @@ -98,7 +113,7 @@ def load_document(self, doc_info: DocumentInfo) -> ProcessingDocument: Load document content and metadata. Args: - doc_info: Document information + doc_info: Document information with content hash as document_hash Returns: ProcessingDocument with content and metadata @@ -122,24 +137,28 @@ def load_document(self, doc_info: DocumentInfo) -> ProcessingDocument: metadata["dataset_collection"] = doc_info.dataset_collection logger.debug( - f"Loaded document {doc_info.document_hash}: {len(content)} characters" + f"Loaded document {doc_info.document_hash[:12]}...: {len(content)} characters" ) + # It's already the content hash (calculated in discover_all_documents) + # No need to recalculate here - keeps the hash consistent return ProcessingDocument( - content=content, metadata=metadata, document_hash=doc_info.document_hash + content=content, + metadata=metadata, + document_hash=doc_info.document_hash, # Already the content hash ) except Exception as e: - error_msg = f"Failed to load document {doc_info.document_hash}: {e}" + error_msg = f"Failed to load document {doc_info.document_hash[:12]}...: {e}" logger.error(error_msg) raise DocumentLoadError(error_msg) from e def get_document_by_hash(self, document_hash: str) -> DocumentInfo: """ - Find document by hash. + Find document by content hash. Args: - document_hash: Document hash to find + document_hash: Document content hash to find Returns: DocumentInfo object @@ -153,7 +172,7 @@ def get_document_by_hash(self, document_hash: str) -> DocumentInfo: if doc_info.document_hash == document_hash: return doc_info - raise ValueError(f"Document not found: {document_hash}") + raise ValueError(f"Document not found with hash: {document_hash[:12]}...") def validate_document_structure(self, doc_info: DocumentInfo) -> bool: """ @@ -168,11 +187,15 @@ def validate_document_structure(self, doc_info: DocumentInfo) -> bool: try: # Check files exist if not Path(doc_info.cleaned_txt_path).exists(): - logger.error(f"Missing cleaned.txt for {doc_info.document_hash}") + logger.error( + f"Missing cleaned.txt for document {doc_info.document_hash[:12]}..." + ) return False if not Path(doc_info.source_meta_path).exists(): - logger.error(f"Missing source.meta.json for {doc_info.document_hash}") + logger.error( + f"Missing source.meta.json for document {doc_info.document_hash[:12]}..." + ) return False # Try to load content with configurable validation @@ -182,7 +205,8 @@ def validate_document_structure(self, doc_info: DocumentInfo) -> bool: content = f.read().strip() if len(content) < DocumentConstants.MIN_CONTENT_LENGTH: logger.error( - f"Content too short for {doc_info.document_hash}: {len(content)} chars (min: {DocumentConstants.MIN_CONTENT_LENGTH})" + f"Content too short for document {doc_info.document_hash[:12]}...: " + f"{len(content)} chars (min: {DocumentConstants.MIN_CONTENT_LENGTH})" ) return False @@ -191,7 +215,7 @@ def validate_document_structure(self, doc_info: DocumentInfo) -> bool: metadata = json.load(f) if not isinstance(metadata, dict): logger.error( - f"Invalid metadata format for {doc_info.document_hash}" + f"Invalid metadata format for document {doc_info.document_hash[:12]}..." ) return False @@ -199,6 +223,6 @@ def validate_document_structure(self, doc_info: DocumentInfo) -> bool: except Exception as e: logger.error( - f"Document validation failed for {doc_info.document_hash}: {e}" + f"Document validation failed for {doc_info.document_hash[:12]}...: {e}" ) return False diff --git a/src/vector_indexer/loki_logger.py b/src/vector_indexer/loki_logger.py new file mode 100644 index 0000000..e69de29 diff --git a/src/vector_indexer/main_indexer.py b/src/vector_indexer/main_indexer.py index ac3be23..ab376e8 100644 --- a/src/vector_indexer/main_indexer.py +++ b/src/vector_indexer/main_indexer.py @@ -1,11 +1,15 @@ """Main vector indexer script for processing documents with contextual retrieval.""" +import argparse import asyncio +import shutil import sys from pathlib import Path from datetime import datetime -from typing import List, Optional +from typing import List, Optional, Dict, Any from loguru import logger +import hashlib + # Add src to path for imports sys.path.append(str(Path(__file__).parent.parent)) @@ -16,18 +20,26 @@ from vector_indexer.qdrant_manager import QdrantManager from vector_indexer.error_logger import ErrorLogger from vector_indexer.models import ProcessingStats, DocumentInfo +from vector_indexer.diff_identifier import DiffDetector, create_diff_config, DiffError +from vector_indexer.diff_identifier.diff_models import DiffResult +from src.vector_indexer.dataset_download import download_and_extract_dataset class VectorIndexer: """Main vector indexer orchestrating the full pipeline.""" - def __init__(self, config_path: Optional[str] = None): + def __init__( + self, config_path: Optional[str] = None, signed_url: Optional[str] = None + ): # Load configuration self.config_path = ( config_path or "src/vector_indexer/config/vector_indexer_config.yaml" ) self.config = ConfigLoader.load_config(self.config_path) + # Store signed URL for future dataset download implementation + self.signed_url = signed_url + # Initialize components self.document_loader = DocumentLoader(self.config) self.error_logger = ErrorLogger(self.config) @@ -52,6 +64,9 @@ def __init__(self, config_path: Optional[str] = None): f"Max concurrent chunks: {self.config.max_concurrent_chunks_per_doc}" ) + if self.signed_url: + logger.info(f"Signed URL provided: {self.signed_url[:50]}...") + async def process_all_documents(self) -> ProcessingStats: """ Process all documents in the dataset with contextual retrieval. @@ -59,23 +74,94 @@ async def process_all_documents(self) -> ProcessingStats: Returns: ProcessingStats: Overall processing statistics """ - logger.info("=" * 60) logger.info("Starting Vector Indexer - Contextual Retrieval Pipeline") - logger.info("=" * 60) self.stats.start_time = datetime.now() try: + # Step 1: Dataset download + if self.signed_url: + logger.info("Dataset download URL provided - starting download") + try: + extraction_path, files_count = download_and_extract_dataset( + self.signed_url + ) + logger.info( + f"Dataset extracted: {files_count} files to {extraction_path}" + ) + # Update config to use the downloaded dataset + self.config.dataset_base_path = extraction_path + except Exception as e: + logger.error(f"Dataset download failed: {e}") + raise + + # Step 2: Diff identification - determine what files need processing + logger.info("Step 1: Identifying changed files...") + try: + diff_config = create_diff_config() + diff_detector = DiffDetector(diff_config) + diff_result = await diff_detector.get_changed_files() + + logger.info("Diff identification complete:") + logger.info( + f" โ€ข Total files scanned: {diff_result.total_files_scanned}" + ) + logger.info( + f" โ€ข Previously processed: {diff_result.previously_processed_count}" + ) + logger.info(f" โ€ข New files: {len(diff_result.new_files)}") + logger.info(f" โ€ข Modified files: {len(diff_result.modified_files)}") + logger.info(f" โ€ข Deleted files: {len(diff_result.deleted_files)}") + logger.info(f" โ€ข Unchanged files: {len(diff_result.unchanged_files)}") + logger.info(f" โ€ข Is first run: {diff_result.is_first_run}") + + files_to_process = diff_result.new_files + diff_result.modified_files + + except DiffError as e: + logger.error(f"Diff identification failed: {e}") + logger.info("Continuing with full document discovery as fallback") + diff_result = None + diff_detector = None + files_to_process = [] + # Initialize Qdrant collections async with QdrantManager(self.config) as qdrant_manager: await qdrant_manager.ensure_collections_exist() - # Discover all documents - logger.info("Discovering documents...") - documents = self.document_loader.discover_all_documents() + # Step 2.5: Execute cleanup operations for deleted/modified files + if diff_result and diff_result.chunks_to_delete: + logger.info("EXECUTING CLEANUP OPERATIONS...") + await self._execute_cleanup_operations(qdrant_manager, diff_result) + + # Early exit check AFTER cleanup operations + # Only exit if there's nothing to process AND no cleanup was needed + if diff_result and not files_to_process: + logger.info("No new or modified files to process.") + # ALWAYS update metadata when there were deletions or modifications + if diff_detector and ( + diff_result.deleted_files or diff_result.modified_files + ): + logger.info("Updating metadata to reflect file changes...") + await diff_detector.mark_files_processed( + [], force_metadata_update=True + ) + logger.info("Metadata updated successfully.") + else: + logger.info("No changes detected - no metadata update needed.") + return self.stats + + # Step 3: Document discovery (filtered by diff results if available) + logger.info("Step 2: Discovering documents...") + if diff_result and files_to_process: + # Filter documents to only those identified as changed + documents = self._filter_documents_by_paths(files_to_process) + else: + # Fallback: discover all documents + documents = self.document_loader.discover_all_documents() if not documents: logger.warning("No documents found to process") + self._cleanup_datasets() return self.stats logger.info(f"Found {len(documents)} documents to process") @@ -83,7 +169,7 @@ async def process_all_documents(self) -> ProcessingStats: # Process documents with controlled concurrency semaphore = asyncio.Semaphore(self.config.max_concurrent_documents) - tasks: List[asyncio.Task[int]] = [] + tasks: List[asyncio.Task[tuple[int, str]]] = [] for doc_info in documents: task = asyncio.create_task( @@ -100,6 +186,9 @@ async def process_all_documents(self) -> ProcessingStats: results = await asyncio.gather(*tasks, return_exceptions=True) # Collect results and handle exceptions + chunks_info: Dict[ + str, Dict[str, Any] + ] = {} # Track chunk counts for metadata update for i, result in enumerate(results): if isinstance(result, Exception): doc_info = documents[i] @@ -111,18 +200,77 @@ async def process_all_documents(self) -> ProcessingStats: doc_info.document_hash, str(result) ) else: - # Result should be number of chunks processed + # Result should be tuple of (chunk_count, content_hash) + doc_info = documents[i] self.stats.documents_processed += 1 - if isinstance(result, int): - self.stats.total_chunks_processed += result + if isinstance(result, tuple) and len(result) == 2: + chunk_count, content_hash = result + self.stats.total_chunks_processed += chunk_count + # Track chunk count using content_hash (not directory hash) + chunks_info[content_hash] = {"chunk_count": chunk_count} + logger.info( + f"CHUNK COUNT: Document {doc_info.document_hash[:12]}... (content: {content_hash[:12]}...) -> {chunk_count} chunks" + ) + + # Log the complete chunks_info dictionary + logger.info( + f"CHUNKS INFO SUMMARY: {len(chunks_info)} documents tracked" + ) + for doc_hash, info in chunks_info.items(): + logger.info( + f" {doc_hash[:12]}... -> {info['chunk_count']} chunks" + ) # Calculate final statistics self.stats.end_time = datetime.now() + # Step 4: Update processed files tracking (even if no new documents processed) + if diff_detector: + try: + # Update metadata for newly processed files + if documents: + processed_paths = [ + doc.cleaned_txt_path for doc in documents + ] + if processed_paths: + logger.debug( + f"Passing chunks_info with keys: {[k[:12] + '...' for k in chunks_info.keys()]} to mark_files_processed" + ) + await diff_detector.mark_files_processed( + processed_paths, chunks_info=chunks_info + ) + logger.info( + "Updated processed files tracking for new documents" + ) + + # CRITICAL: Update metadata even when only cleanup operations occurred + # This ensures deleted files are removed from metadata + elif diff_result and ( + diff_result.deleted_files or diff_result.modified_files + ): + logger.info( + "Updating metadata to reflect file deletions/modifications..." + ) + # Force metadata update for cleanup operations + await diff_detector.mark_files_processed( + [], force_metadata_update=True + ) + logger.info( + "Updated processed files tracking for cleanup operations" + ) + + except Exception as e: + logger.warning( + f"Failed to update processed files tracking: {e}" + ) + # Log final statistics self.error_logger.log_processing_stats(self.stats) self._log_final_summary() + # Step 5: Cleanup datasets folder after successful processing + self._cleanup_datasets() + return self.stats except Exception as e: @@ -142,7 +290,7 @@ async def _process_single_document( doc_info: DocumentInfo, qdrant_manager: QdrantManager, semaphore: asyncio.Semaphore, - ) -> int: + ) -> tuple[int, str]: """ Process a single document with contextual retrieval. @@ -152,7 +300,7 @@ async def _process_single_document( semaphore: Concurrency control semaphore Returns: - int: Number of chunks processed + tuple: (chunk_count: int, content_hash: str) or Exception on error """ async with semaphore: logger.info(f"Processing document: {doc_info.document_hash}") @@ -163,7 +311,7 @@ async def _process_single_document( if not document: logger.warning(f"Could not load document: {doc_info.document_hash}") - return 0 + return (0, doc_info.document_hash) # Process document with contextual retrieval contextual_chunks = await self.contextual_processor.process_document( @@ -174,7 +322,7 @@ async def _process_single_document( logger.warning( f"No chunks created for document: {doc_info.document_hash}" ) - return 0 + return (0, document.document_hash) # Store chunks in Qdrant await qdrant_manager.store_chunks(contextual_chunks) @@ -184,7 +332,7 @@ async def _process_single_document( f"{len(contextual_chunks)} chunks" ) - return len(contextual_chunks) + return (len(contextual_chunks), document.document_hash) except Exception as e: logger.error(f"Error processing document {doc_info.document_hash}: {e}") @@ -207,15 +355,15 @@ def _log_final_summary(self): success_rate = ( self.stats.documents_processed / self.stats.total_documents ) * 100 - logger.info(f" โ€ข Success Rate: {success_rate:.1f}%") + logger.info(f"Success Rate: {success_rate:.1f}%") - logger.info(f" โ€ข Processing Duration: {self.stats.duration}") + logger.info(f"Processing Duration: {self.stats.duration}") if self.stats.documents_failed > 0: logger.warning( f" {self.stats.documents_failed} documents failed processing" ) - logger.info(" Check failure logs for details") + logger.info("Check failure logs for details") async def run_health_check(self) -> bool: """ @@ -231,14 +379,12 @@ async def run_health_check(self) -> bool: async with QdrantManager(self.config) as qdrant_manager: # Test basic Qdrant connectivity by trying to list collections try: - qdrant_url = getattr( - self.config, "qdrant_url", "http://localhost:6333" - ) + qdrant_url = getattr(self.config, "qdrant_url") response = await qdrant_manager.client.get( f"{qdrant_url}/collections" ) if response.status_code == 200: - logger.info(" Qdrant server: Connected") + logger.info("Qdrant server: Connected") # Check if collections exist, create them if they don't collections_info = {} @@ -252,43 +398,41 @@ async def run_health_check(self) -> bool: ) collections_info[collection_name] = count logger.info( - f" Qdrant collection '{collection_name}': {count} points" + f"Qdrant collection '{collection_name}': {count} points" ) else: logger.info( - f" Qdrant collection '{collection_name}': Not found (will be created automatically)" + f"Qdrant collection '{collection_name}': Not found (will be created automatically)" ) else: logger.error( - f" Qdrant server not accessible: {response.status_code}" + f"Qdrant server not accessible: {response.status_code}" ) return False except Exception as e: - logger.error(f" Qdrant connection failed: {e}") + logger.error(f"Qdrant connection failed: {e}") return False # Check API client connectivity api_healthy = await self.api_client.health_check() if api_healthy: - logger.info(" LLM Orchestration Service API: Connected") + logger.info("LLM Orchestration Service API: Connected") else: - logger.error(" LLM Orchestration Service API: Not accessible") + logger.error("LLM Orchestration Service API: Not accessible") return False # Check dataset path if Path(self.config.dataset_base_path).exists(): - logger.info(f" Dataset path: {self.config.dataset_base_path}") + logger.info(f"Dataset path: {self.config.dataset_base_path}") else: - logger.error( - f" Dataset path not found: {self.config.dataset_base_path}" - ) + logger.error(f"Dataset path not found: {self.config.dataset_base_path}") return False - logger.info(" All health checks passed!") + logger.info("All health checks passed!") return True except Exception as e: - logger.error(f" Health check failed: {e}") + logger.error(f"Health check failed: {e}") return False # NOTE: Don't close API client here - it will be used by main processing @@ -300,10 +444,202 @@ async def cleanup(self): except Exception as e: logger.warning(f"Error closing API client: {e}") + def _filter_documents_by_paths(self, file_paths: List[str]) -> List[DocumentInfo]: + """ + Filter documents by specific file paths. + + IMPORTANT: This method now uses discover_all_documents() to get the correct + content hashes that were already calculated, instead of recalculating them. + This ensures consistency throughout the pipeline. + + Args: + file_paths: List of file paths to process + + Returns: + List of DocumentInfo for matching files + """ + documents: List[DocumentInfo] = [] + + # FIX: Discover ALL documents first to get their content hashes + # This ensures we use the same hash that was calculated in discover_all_documents() + logger.debug("Discovering all documents to get content hashes...") + all_documents = self.document_loader.discover_all_documents() + + # Create a lookup map: file_path -> DocumentInfo + path_to_doc_map: Dict[str, DocumentInfo] = { + doc.cleaned_txt_path: doc for doc in all_documents + } + logger.debug(f"Created path lookup map with {len(path_to_doc_map)} documents") + + for file_path in file_paths: + # Check if this file path exists in our discovered documents + if file_path in path_to_doc_map: + # Use the DocumentInfo that was already discovered (with correct content hash) + doc_info = path_to_doc_map[file_path] + documents.append(doc_info) + logger.debug( + f"Added document: {doc_info.document_hash[:12]}... from {file_path}" + ) + else: + logger.warning( + f"File path {file_path} not found in discovered documents" + ) + + logger.info( + f"Filtered to {len(documents)} documents from {len(file_paths)} paths" + ) + return documents + + async def _execute_cleanup_operations( + self, qdrant_manager: QdrantManager, diff_result: "DiffResult" + ) -> int: + """ + Execute cleanup operations for deleted and modified files. + + Args: + qdrant_manager: Qdrant manager instance + diff_result: DiffResult containing cleanup information + + Returns: + Total number of chunks deleted + """ + total_deleted = 0 + + if not diff_result.chunks_to_delete: + logger.info("No cleanup operations needed") + return total_deleted + + logger.info( + f"STARTING CLEANUP: {len(diff_result.chunks_to_delete)} documents with chunks to delete" + ) + + # Get appropriate collection names + collections_to_clean = ["contextual_chunks_azure", "contextual_chunks_aws"] + + for document_hash, original_path in diff_result.chunks_to_delete.items(): + logger.info( + f"ATTEMPTING CLEANUP for document {document_hash[:12]}... (path: {original_path})" + ) + logger.debug(f"DEBUG: Full document_hash for deletion: {document_hash}") + logger.info( + "DEBUG: This could be a retry if chunks were deleted in a previous run but metadata wasn't updated" + ) + + chunks_deleted_for_doc = 0 + fallback_hash = None + + for collection_name in collections_to_clean: + try: + # Try with current document_hash first + deleted_count = await qdrant_manager.delete_chunks_by_document_hash( + collection_name, document_hash + ) + chunks_deleted_for_doc += deleted_count + + if deleted_count > 0: + logger.info( + f"Deleted {deleted_count} chunks from {collection_name}" + ) + else: + # If no chunks found with current hash, try fallback with old hash calculation method + if fallback_hash is None and Path(original_path).exists(): + try: + # Calculate hash using old method (read_bytes) for backward compatibility + + file_content = Path(original_path).read_bytes() + fallback_hash = hashlib.sha256(file_content).hexdigest() + logger.info( + f"Trying fallback hash calculation for backward compatibility: {fallback_hash[:12]}..." + ) + except Exception as fallback_error: + logger.warning( + f"Could not calculate fallback hash: {fallback_error}" + ) + fallback_hash = "FAILED" + + if ( + fallback_hash + and fallback_hash != "FAILED" + and fallback_hash != document_hash + ): + fallback_deleted = ( + await qdrant_manager.delete_chunks_by_document_hash( + collection_name, fallback_hash + ) + ) + chunks_deleted_for_doc += fallback_deleted + if fallback_deleted > 0: + logger.info( + f" โœ… Deleted {fallback_deleted} chunks from {collection_name} using fallback hash" + ) + + except Exception as e: + logger.error(f"Failed to delete chunks from {collection_name}: {e}") + continue + + total_deleted += chunks_deleted_for_doc + if chunks_deleted_for_doc > 0: + logger.info( + f"Total deleted for document {document_hash[:12]}...: {chunks_deleted_for_doc} chunks" + ) + else: + if ( + fallback_hash + and fallback_hash != "FAILED" + and fallback_hash != document_hash + ): + logger.info( + f"No chunks found for document {document_hash[:12]}... or fallback hash {fallback_hash[:12]}... (may have been deleted previously or stored with different hash)" + ) + else: + logger.info( + f"No chunks found for document {document_hash[:12]}... (file tracked in metadata but chunks not in vector store)" + ) + + if total_deleted > 0: + logger.info( + f"CLEANUP COMPLETED: {total_deleted} total chunks removed from {len(diff_result.chunks_to_delete)} documents" + ) + else: + logger.info( + f"CLEANUP COMPLETED: No chunks removed (0 chunks found in vector store for {len(diff_result.chunks_to_delete)} tracked documents)" + ) + + # Log cleanup summary by file type + deleted_files = diff_result.deleted_files + modified_files = diff_result.modified_files + + if deleted_files: + logger.info(f"Processed cleanup for {len(deleted_files)} deleted files") + if modified_files: + logger.info(f"Processed cleanup for {len(modified_files)} modified files") + + return total_deleted + + def _cleanup_datasets(self): + """Remove datasets folder after processing.""" + try: + datasets_path = Path(self.config.dataset_base_path) + if datasets_path.exists(): + shutil.rmtree(str(datasets_path)) + logger.info(f"Datasets folder cleaned up: {datasets_path}") + else: + logger.debug(f"Datasets folder does not exist: {datasets_path}") + except Exception as e: + logger.warning(f"Failed to cleanup datasets folder: {e}") + # Non-critical error - don't fail the entire process + async def main(): """Main entry point for the vector indexer.""" + # Parse command line arguments + parser = argparse.ArgumentParser( + description="Vector Indexer with Diff Identification" + ) + parser.add_argument("--signed-url", help="Signed URL for dataset download") + args = parser.parse_args() + # Configure logging logger.remove() # Remove default handler logger.add( @@ -323,8 +659,8 @@ async def main(): indexer = None try: - # Initialize vector indexer - indexer = VectorIndexer() + # Initialize vector indexer with signed URL + indexer = VectorIndexer(signed_url=args.signed_url) # Run health check first logger.info("Performing pre-processing health check...") @@ -332,8 +668,7 @@ async def main(): if not health_ok: logger.error("Health check failed. Aborting processing.") - await indexer.cleanup() - sys.exit(1) + return 1 # Return exit code instead of sys.exit() # Process all documents logger.info("Health check passed. Starting document processing...") @@ -354,11 +689,17 @@ async def main(): return 130 except Exception as e: logger.error(f"Fatal error: {e}") + import traceback + + logger.error(traceback.format_exc()) return 1 finally: # Ensure cleanup happens if indexer: - await indexer.cleanup() + try: + await indexer.cleanup() + except Exception as e: + logger.error(f"Error during cleanup: {e}") if __name__ == "__main__": diff --git a/src/vector_indexer/qdrant_manager.py b/src/vector_indexer/qdrant_manager.py index 93aacd8..be9dc92 100644 --- a/src/vector_indexer/qdrant_manager.py +++ b/src/vector_indexer/qdrant_manager.py @@ -307,6 +307,269 @@ async def count_points(self, collection_name: str) -> int: logger.error(f"Error counting points in {collection_name}: {e}") return 0 + async def delete_chunks_by_document_hash( + self, collection_name: str, document_hash: str + ) -> int: + """ + Delete all chunks associated with a specific document hash. + + Args: + collection_name: Name of the Qdrant collection + document_hash: SHA256 hash of the document to delete chunks for + + Returns: + Number of chunks deleted (estimated as 1 if deletion successful, 0 if nothing to delete) + + Raises: + QdrantOperationError: If deletion fails + """ + try: + logger.info( + f"๐Ÿ—‘๏ธ Attempting to delete chunks for document: {document_hash[:12]}... from {collection_name}" + ) + + # Step 1: Check if chunks exist BEFORE deletion (for accurate reporting) + pre_check_payload = { + "filter": { + "must": [ + {"key": "document_hash", "match": {"value": document_hash}} + ] + }, + "limit": 100, # Get up to 100 to count + "with_payload": False, + "with_vector": False, + } + + pre_check_response = await self.client.post( + f"{self.qdrant_url}/collections/{collection_name}/points/scroll", + json=pre_check_payload, + ) + + chunks_found_before = 0 + if pre_check_response.status_code == 200: + pre_check_data = pre_check_response.json() + chunks_found_before = len( + pre_check_data.get("result", {}).get("points", []) + ) + logger.info(f"๐Ÿ” Found {chunks_found_before} chunks to delete") + else: + logger.warning( + f"โš ๏ธ Pre-check query failed with status {pre_check_response.status_code}" + ) + + # Step 2: Execute deletion using filter + delete_payload = { + "filter": { + "must": [ + {"key": "document_hash", "match": {"value": document_hash}} + ] + } + } + + logger.debug(f"๐Ÿ” Executing delete with filter: {delete_payload}") + + response = await self.client.post( + f"{self.qdrant_url}/collections/{collection_name}/points/delete", + json=delete_payload, + ) + + if response.status_code in [200, 201]: + result = response.json() + + if result.get("status") == "ok": + # Step 3: Verify deletion by checking if chunks still exist + verify_payload = { + "filter": { + "must": [ + { + "key": "document_hash", + "match": {"value": document_hash}, + } + ] + }, + "limit": 1, + "with_payload": False, + "with_vector": False, + } + + verify_response = await self.client.post( + f"{self.qdrant_url}/collections/{collection_name}/points/scroll", + json=verify_payload, + ) + + if verify_response.status_code == 200: + verify_data = verify_response.json() + remaining_chunks = len( + verify_data.get("result", {}).get("points", []) + ) + + if remaining_chunks == 0: + if chunks_found_before > 0: + logger.info( + f"โœ… Successfully deleted {chunks_found_before} chunk(s) from {collection_name}" + ) + return chunks_found_before + else: + logger.info( + f"โ„น๏ธ No chunks found for document {document_hash[:12]}... in {collection_name}" + ) + return 0 + else: + logger.error( + f"โŒ Delete verification failed: {remaining_chunks} chunk(s) still exist!" + ) + return 0 + else: + # Verification query failed, but delete was accepted + # Assume success based on pre-check count + if chunks_found_before > 0: + logger.warning( + f"โš ๏ธ Delete succeeded but verification failed - assuming {chunks_found_before} chunks deleted" + ) + return chunks_found_before + else: + logger.info( + f"โ„น๏ธ Delete completed (verification query failed, found {chunks_found_before} before)" + ) + return 0 + else: + raise QdrantOperationError( + f"Qdrant delete returned error status: {result}" + ) + else: + raise QdrantOperationError( + f"Delete request failed with HTTP {response.status_code}: {response.text}" + ) + + except QdrantOperationError: + # Re-raise QdrantOperationError as-is + raise + except Exception as e: + logger.error( + f"โŒ Failed to delete chunks for document {document_hash[:12]}...: {e}" + ) + raise QdrantOperationError( + f"Failed to delete chunks by document hash: {str(e)}" + ) + + async def delete_chunks_by_file_path( + self, collection_name: str, file_path: str + ) -> int: + """ + Delete all chunks associated with a specific file path (fallback method). + + Args: + collection_name: Name of the Qdrant collection + file_path: Original file path to delete chunks for + + Returns: + Number of chunks deleted + + Raises: + QdrantOperationError: If deletion fails + """ + try: + logger.info( + f"๐Ÿ—‘๏ธ Deleting chunks for file path: {file_path} from {collection_name}" + ) + + # Count chunks first + scroll_payload = { + "filter": { + "must": [{"key": "document_url", "match": {"value": file_path}}] + }, + "limit": 1000, + "with_payload": False, + "with_vector": False, + } + + scroll_response = await self.client.post( + f"{self.qdrant_url}/collections/{collection_name}/points/scroll", + json=scroll_payload, + ) + + chunks_to_delete = 0 + if scroll_response.status_code == 200: + scroll_data = scroll_response.json() + chunks_to_delete = len(scroll_data.get("result", {}).get("points", [])) + + # Delete chunks using filter + delete_payload = { + "filter": { + "must": [{"key": "document_url", "match": {"value": file_path}}] + } + } + + response = await self.client.post( + f"{self.qdrant_url}/collections/{collection_name}/points/delete", + json=delete_payload, + ) + + if response.status_code in [200, 201]: + result = response.json() + if result.get("status") == "ok": + logger.info( + f"โœ… Successfully deleted {chunks_to_delete} chunks for file {file_path}" + ) + return chunks_to_delete + else: + raise QdrantOperationError(f"Qdrant returned error: {result}") + else: + raise QdrantOperationError( + f"HTTP {response.status_code}: {response.text}" + ) + + except Exception as e: + logger.error(f"Failed to delete chunks for file {file_path}: {e}") + raise QdrantOperationError( + f"Failed to delete chunks by file path: {str(e)}" + ) + + async def get_chunks_for_document( + self, collection_name: str, document_hash: str + ) -> List[Dict[str, Any]]: + """ + Get all chunks associated with a specific document hash. + + Args: + collection_name: Name of the Qdrant collection + document_hash: SHA256 hash of the document + + Returns: + List of chunk records with their metadata + """ + try: + scroll_payload = { + "filter": { + "must": [ + {"key": "document_hash", "match": {"value": document_hash}} + ] + }, + "limit": 1000, + "with_payload": True, + "with_vector": False, + } + + response = await self.client.post( + f"{self.qdrant_url}/collections/{collection_name}/points/scroll", + json=scroll_payload, + ) + + if response.status_code == 200: + result = response.json() + return result.get("result", {}).get("points", []) + else: + logger.warning( + f"Failed to get chunks for document {document_hash[:12]}...: HTTP {response.status_code}" + ) + return [] + + except Exception as e: + logger.warning( + f"Error getting chunks for document {document_hash[:12]}...: {e}" + ) + return [] + async def delete_collection(self, collection_name: str) -> bool: """Delete a collection (for cleanup/testing).""" diff --git a/uv.lock b/uv.lock index 58bb127..7db130c 100644 --- a/uv.lock +++ b/uv.lock @@ -13,7 +13,7 @@ wheels = [ [[package]] name = "aiohttp" -version = "3.13.0" +version = "3.13.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohappyeyeballs" }, @@ -24,25 +24,25 @@ dependencies = [ { name = "propcache" }, { name = "yarl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/62/f1/8515650ac3121a9e55c7b217c60e7fae3e0134b5acfe65691781b5356929/aiohttp-3.13.0.tar.gz", hash = "sha256:378dbc57dd8cf341ce243f13fa1fa5394d68e2e02c15cd5f28eae35a70ec7f67", size = 7832348, upload-time = "2025-10-06T19:58:48.089Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/fa/3ae643cd525cf6844d3dc810481e5748107368eb49563c15a5fb9f680750/aiohttp-3.13.1.tar.gz", hash = "sha256:4b7ee9c355015813a6aa085170b96ec22315dabc3d866fd77d147927000e9464", size = 7835344, upload-time = "2025-10-17T14:03:29.337Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/95/7e8bdfa6e79099a086d59d42589492f1fe9d29aae3cefb58b676015ce278/aiohttp-3.13.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1c272a9a18a5ecc48a7101882230046b83023bb2a662050ecb9bfcb28d9ab53a", size = 735585, upload-time = "2025-10-06T19:55:43.401Z" }, - { url = "https://files.pythonhosted.org/packages/9f/20/2f1d3ee06ee94eafe516810705219bff234d09f135d6951661661d5595ae/aiohttp-3.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:97891a23d7fd4e1afe9c2f4473e04595e4acb18e4733b910b6577b74e7e21985", size = 490613, upload-time = "2025-10-06T19:55:45.237Z" }, - { url = "https://files.pythonhosted.org/packages/74/15/ab8600ef6dc1dcd599009a81acfed2ea407037e654d32e47e344e0b08c34/aiohttp-3.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:475bd56492ce5f4cffe32b5533c6533ee0c406d1d0e6924879f83adcf51da0ae", size = 489750, upload-time = "2025-10-06T19:55:46.937Z" }, - { url = "https://files.pythonhosted.org/packages/33/59/752640c2b86ca987fe5703a01733b00d375e6cd2392bc7574489934e64e5/aiohttp-3.13.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c32ada0abb4bc94c30be2b681c42f058ab104d048da6f0148280a51ce98add8c", size = 1736812, upload-time = "2025-10-06T19:55:48.917Z" }, - { url = "https://files.pythonhosted.org/packages/3d/c6/dd6b86ddb852a7fdbcdc7a45b6bdc80178aef713c08279afcaee7a5a9f07/aiohttp-3.13.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4af1f8877ca46ecdd0bc0d4a6b66d4b2bddc84a79e2e8366bc0d5308e76bceb8", size = 1698535, upload-time = "2025-10-06T19:55:50.75Z" }, - { url = "https://files.pythonhosted.org/packages/33/e2/27c92d205b9e8cee7661670e8e9f187931b71e26d42796b153d2a0ba6949/aiohttp-3.13.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e04ab827ec4f775817736b20cdc8350f40327f9b598dec4e18c9ffdcbea88a93", size = 1766573, upload-time = "2025-10-06T19:55:53.106Z" }, - { url = "https://files.pythonhosted.org/packages/df/6a/1fc1ad71d130a30f7a207d8d958a41224c29b834463b5185efb2dbff6ad4/aiohttp-3.13.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a6d9487b9471ec36b0faedf52228cd732e89be0a2bbd649af890b5e2ce422353", size = 1865229, upload-time = "2025-10-06T19:55:55.01Z" }, - { url = "https://files.pythonhosted.org/packages/14/51/d0c1701a79fcb0109cff5304da16226581569b89a282d8e7f1549a7e3ec0/aiohttp-3.13.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e66c57416352f36bf98f6641ddadd47c93740a22af7150d3e9a1ef6e983f9a8", size = 1750379, upload-time = "2025-10-06T19:55:57.219Z" }, - { url = "https://files.pythonhosted.org/packages/ae/3d/2ec4b934f85856de1c0c18e90adc8902adadbfac2b3c0b831bfeb7214fc8/aiohttp-3.13.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:469167d5372f5bb3aedff4fc53035d593884fff2617a75317740e885acd48b04", size = 1560798, upload-time = "2025-10-06T19:55:58.888Z" }, - { url = "https://files.pythonhosted.org/packages/38/56/e23d9c3e13006e599fdce3851517c70279e177871e3e567d22cf3baf5d6c/aiohttp-3.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a9f3546b503975a69b547c9fd1582cad10ede1ce6f3e313a2f547c73a3d7814f", size = 1697552, upload-time = "2025-10-06T19:56:01.172Z" }, - { url = "https://files.pythonhosted.org/packages/56/cb/caa32c2ccaeca0a3dc39129079fd2ad02f9406c3a5f7924340435b87d4cd/aiohttp-3.13.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6b4174fcec98601f0cfdf308ee29a6ae53c55f14359e848dab4e94009112ee7d", size = 1718609, upload-time = "2025-10-06T19:56:03.102Z" }, - { url = "https://files.pythonhosted.org/packages/fb/c0/5911856fef9e40fd1ccbb8c54a90116875d5753a92c1cac66ce2059b390d/aiohttp-3.13.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a533873a7a4ec2270fb362ee5a0d3b98752e4e1dc9042b257cd54545a96bd8ed", size = 1735887, upload-time = "2025-10-06T19:56:04.841Z" }, - { url = "https://files.pythonhosted.org/packages/0e/48/8d6f4757a24c02f0a454c043556593a00645d10583859f7156db44d8b7d3/aiohttp-3.13.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ce887c5e54411d607ee0959cac15bb31d506d86a9bcaddf0b7e9d63325a7a802", size = 1553079, upload-time = "2025-10-06T19:56:07.197Z" }, - { url = "https://files.pythonhosted.org/packages/39/fa/e82c9445e40b50e46770702b5b6ca2f767966d53e1a5eef03583ceac6df6/aiohttp-3.13.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:d871f6a30d43e32fc9252dc7b9febe1a042b3ff3908aa83868d7cf7c9579a59b", size = 1762750, upload-time = "2025-10-06T19:56:09.376Z" }, - { url = "https://files.pythonhosted.org/packages/3d/e6/9d30554e7f1e700bfeae4ab6b153d5dc7441606a9ec5e929288fa93a1477/aiohttp-3.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:222c828243b4789d79a706a876910f656fad4381661691220ba57b2ab4547865", size = 1717461, upload-time = "2025-10-06T19:56:11.551Z" }, - { url = "https://files.pythonhosted.org/packages/1f/e5/29cca547990a59ea54f0674fc01de98519fc628cfceeab6175711750eca7/aiohttp-3.13.0-cp312-cp312-win32.whl", hash = "sha256:682d2e434ff2f1108314ff7f056ce44e457f12dbed0249b24e106e385cf154b9", size = 424633, upload-time = "2025-10-06T19:56:13.316Z" }, - { url = "https://files.pythonhosted.org/packages/8b/68/46dd042d7bc62eab30bafdb8569f55ef125c3a88bb174270324224f8df56/aiohttp-3.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:0a2be20eb23888df130214b91c262a90e2de1553d6fb7de9e9010cec994c0ff2", size = 451401, upload-time = "2025-10-06T19:56:15.188Z" }, + { url = "https://files.pythonhosted.org/packages/1a/72/d463a10bf29871f6e3f63bcf3c91362dc4d72ed5917a8271f96672c415ad/aiohttp-3.13.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0760bd9a28efe188d77b7c3fe666e6ef74320d0f5b105f2e931c7a7e884c8230", size = 736218, upload-time = "2025-10-17T14:00:03.51Z" }, + { url = "https://files.pythonhosted.org/packages/26/13/f7bccedbe52ea5a6eef1e4ebb686a8d7765319dfd0a5939f4238cb6e79e6/aiohttp-3.13.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7129a424b441c3fe018a414401bf1b9e1d49492445f5676a3aecf4f74f67fcdb", size = 491251, upload-time = "2025-10-17T14:00:05.756Z" }, + { url = "https://files.pythonhosted.org/packages/0c/7c/7ea51b5aed6cc69c873f62548da8345032aa3416336f2d26869d4d37b4a2/aiohttp-3.13.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e1cb04ae64a594f6ddf5cbb024aba6b4773895ab6ecbc579d60414f8115e9e26", size = 490394, upload-time = "2025-10-17T14:00:07.504Z" }, + { url = "https://files.pythonhosted.org/packages/31/05/1172cc4af4557f6522efdee6eb2b9f900e1e320a97e25dffd3c5a6af651b/aiohttp-3.13.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:782d656a641e755decd6bd98d61d2a8ea062fd45fd3ff8d4173605dd0d2b56a1", size = 1737455, upload-time = "2025-10-17T14:00:09.403Z" }, + { url = "https://files.pythonhosted.org/packages/24/3d/ce6e4eca42f797d6b1cd3053cf3b0a22032eef3e4d1e71b9e93c92a3f201/aiohttp-3.13.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f92ad8169767429a6d2237331726c03ccc5f245222f9373aa045510976af2b35", size = 1699176, upload-time = "2025-10-17T14:00:11.314Z" }, + { url = "https://files.pythonhosted.org/packages/25/04/7127ba55653e04da51477372566b16ae786ef854e06222a1c96b4ba6c8ef/aiohttp-3.13.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0e778f634ca50ec005eefa2253856921c429581422d887be050f2c1c92e5ce12", size = 1767216, upload-time = "2025-10-17T14:00:13.668Z" }, + { url = "https://files.pythonhosted.org/packages/b8/3b/43bca1e75847e600f40df829a6b2f0f4e1d4c70fb6c4818fdc09a462afd5/aiohttp-3.13.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9bc36b41cf4aab5d3b34d22934a696ab83516603d1bc1f3e4ff9930fe7d245e5", size = 1865870, upload-time = "2025-10-17T14:00:15.852Z" }, + { url = "https://files.pythonhosted.org/packages/9e/69/b204e5d43384197a614c88c1717c324319f5b4e7d0a1b5118da583028d40/aiohttp-3.13.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3fd4570ea696aee27204dd524f287127ed0966d14d309dc8cc440f474e3e7dbd", size = 1751021, upload-time = "2025-10-17T14:00:18.297Z" }, + { url = "https://files.pythonhosted.org/packages/1c/af/845dc6b6fdf378791d720364bf5150f80d22c990f7e3a42331d93b337cc7/aiohttp-3.13.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7bda795f08b8a620836ebfb0926f7973972a4bf8c74fdf9145e489f88c416811", size = 1561448, upload-time = "2025-10-17T14:00:20.152Z" }, + { url = "https://files.pythonhosted.org/packages/7a/91/d2ab08cd77ed76a49e4106b1cfb60bce2768242dd0c4f9ec0cb01e2cbf94/aiohttp-3.13.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:055a51d90e351aae53dcf324d0eafb2abe5b576d3ea1ec03827d920cf81a1c15", size = 1698196, upload-time = "2025-10-17T14:00:22.131Z" }, + { url = "https://files.pythonhosted.org/packages/5e/d1/082f0620dc428ecb8f21c08a191a4694915cd50f14791c74a24d9161cc50/aiohttp-3.13.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:d4131df864cbcc09bb16d3612a682af0db52f10736e71312574d90f16406a867", size = 1719252, upload-time = "2025-10-17T14:00:24.453Z" }, + { url = "https://files.pythonhosted.org/packages/fc/78/2af2f44491be7b08e43945b72d2b4fd76f0a14ba850ba9e41d28a7ce716a/aiohttp-3.13.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:163d3226e043f79bf47c87f8dfc89c496cc7bc9128cb7055ce026e435d551720", size = 1736529, upload-time = "2025-10-17T14:00:26.567Z" }, + { url = "https://files.pythonhosted.org/packages/b0/34/3e919ecdc93edaea8d140138049a0d9126141072e519535e2efa38eb7a02/aiohttp-3.13.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:a2370986a3b75c1a5f3d6f6d763fc6be4b430226577b0ed16a7c13a75bf43d8f", size = 1553723, upload-time = "2025-10-17T14:00:28.592Z" }, + { url = "https://files.pythonhosted.org/packages/21/4b/d8003aeda2f67f359b37e70a5a4b53fee336d8e89511ac307ff62aeefcdb/aiohttp-3.13.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:d7c14de0c7c9f1e6e785ce6cbe0ed817282c2af0012e674f45b4e58c6d4ea030", size = 1763394, upload-time = "2025-10-17T14:00:31.051Z" }, + { url = "https://files.pythonhosted.org/packages/4c/7b/1dbe6a39e33af9baaafc3fc016a280663684af47ba9f0e5d44249c1f72ec/aiohttp-3.13.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb611489cf0db10b99beeb7280bd39e0ef72bc3eb6d8c0f0a16d8a56075d1eb7", size = 1718104, upload-time = "2025-10-17T14:00:33.407Z" }, + { url = "https://files.pythonhosted.org/packages/5c/88/bd1b38687257cce67681b9b0fa0b16437be03383fa1be4d1a45b168bef25/aiohttp-3.13.1-cp312-cp312-win32.whl", hash = "sha256:f90fe0ee75590f7428f7c8b5479389d985d83c949ea10f662ab928a5ed5cf5e6", size = 425303, upload-time = "2025-10-17T14:00:35.829Z" }, + { url = "https://files.pythonhosted.org/packages/0e/e3/4481f50dd6f27e9e58c19a60cff44029641640237e35d32b04aaee8cf95f/aiohttp-3.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:3461919a9dca272c183055f2aab8e6af0adc810a1b386cce28da11eb00c859d9", size = 452071, upload-time = "2025-10-17T14:00:37.764Z" }, ] [[package]] @@ -89,7 +89,7 @@ sdist = { url = "https://files.pythonhosted.org/packages/07/38/e321b0e05d8cc068a [[package]] name = "anthropic" -version = "0.69.0" +version = "0.71.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -101,9 +101,9 @@ dependencies = [ { name = "sniffio" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c8/9d/9ad1778b95f15c5b04e7d328c1b5f558f1e893857b7c33cd288c19c0057a/anthropic-0.69.0.tar.gz", hash = "sha256:c604d287f4d73640f40bd2c0f3265a2eb6ce034217ead0608f6b07a8bc5ae5f2", size = 480622, upload-time = "2025-09-29T16:53:45.282Z" } +sdist = { url = "https://files.pythonhosted.org/packages/82/4f/70682b068d897841f43223df82d96ec1d617435a8b759c4a2d901a50158b/anthropic-0.71.0.tar.gz", hash = "sha256:eb8e6fa86d049061b3ef26eb4cbae0174ebbff21affa6de7b3098da857d8de6a", size = 489102, upload-time = "2025-10-16T15:54:40.08Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/38/75129688de5637eb5b383e5f2b1570a5cc3aecafa4de422da8eea4b90a6c/anthropic-0.69.0-py3-none-any.whl", hash = "sha256:1f73193040f33f11e27c2cd6ec25f24fe7c3f193dc1c5cde6b7a08b18a16bcc5", size = 337265, upload-time = "2025-09-29T16:53:43.686Z" }, + { url = "https://files.pythonhosted.org/packages/5d/77/073e8ac488f335aec7001952825275582fb8f433737e90f24eeef9d878f6/anthropic-0.71.0-py3-none-any.whl", hash = "sha256:85c5015fcdbdc728390f11b17642a65a4365d03b12b799b18b6cc57e71fdb327", size = 355035, upload-time = "2025-10-16T15:54:38.238Z" }, ] [[package]] @@ -143,16 +143,15 @@ wheels = [ [[package]] name = "azure-core" -version = "1.35.1" +version = "1.36.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "requests" }, - { name = "six" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/15/6b/2653adc0f33adba8f11b1903701e6b1c10d34ce5d8e25dfa13a422f832b0/azure_core-1.35.1.tar.gz", hash = "sha256:435d05d6df0fff2f73fb3c15493bb4721ede14203f1ff1382aa6b6b2bdd7e562", size = 345290, upload-time = "2025-09-11T22:58:04.481Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/c4/d4ff3bc3ddf155156460bff340bbe9533f99fac54ddea165f35a8619f162/azure_core-1.36.0.tar.gz", hash = "sha256:22e5605e6d0bf1d229726af56d9e92bc37b6e726b141a18be0b4d424131741b7", size = 351139, upload-time = "2025-10-15T00:33:49.083Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/27/52/805980aa1ba18282077c484dba634ef0ede1e84eec8be9c92b2e162d0ed6/azure_core-1.35.1-py3-none-any.whl", hash = "sha256:12da0c9e08e48e198f9158b56ddbe33b421477e1dc98c2e1c8f9e254d92c468b", size = 211800, upload-time = "2025-09-11T22:58:06.281Z" }, + { url = "https://files.pythonhosted.org/packages/b1/3c/b90d5afc2e47c4a45f4bba00f9c3193b0417fad5ad3bb07869f9d12832aa/azure_core-1.36.0-py3-none-any.whl", hash = "sha256:fee9923a3a753e94a259563429f3644aaf05c486d45b1215d098115102d91d3b", size = 213302, upload-time = "2025-10-15T00:33:51.058Z" }, ] [[package]] @@ -182,39 +181,39 @@ wheels = [ [[package]] name = "boto3" -version = "1.40.50" +version = "1.40.55" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore" }, { name = "jmespath" }, { name = "s3transfer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ba/41/d4d73f55b367899ee377cd77c228748c18698ea3507c2a95b328f9152017/boto3-1.40.50.tar.gz", hash = "sha256:ae34363e8f34a49ab130d10c507a611926c1101d5d14d70be5598ca308e13266", size = 111605, upload-time = "2025-10-10T20:12:35.202Z" } +sdist = { url = "https://files.pythonhosted.org/packages/50/d8/a279c054e0c9731172f05b3d118f3ffc9d74806657f84fc0c93c42d1bb5d/boto3-1.40.55.tar.gz", hash = "sha256:27e35b4fa9edd414ce06c1a748bf57cacd8203271847d93fc1053e4a4ec6e1a9", size = 111590, upload-time = "2025-10-17T19:34:56.753Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a2/0d/c2c0c244a89c329c5e388d56f475b076a1da314203862897a131dee4a8cc/boto3-1.40.50-py3-none-any.whl", hash = "sha256:62901bc616c64236700001f530fc66b659ecd1acb4f541ddac6fcae3a1d37ea6", size = 139345, upload-time = "2025-10-10T20:12:33.29Z" }, + { url = "https://files.pythonhosted.org/packages/42/8c/559c6145d857ed953536a83f3a94915bbd5d3d2d406db1abf8bf40be7645/boto3-1.40.55-py3-none-any.whl", hash = "sha256:2e30f5a0d49e107b8a5c0c487891afd300bfa410e1d918bf187ae45ac3839332", size = 139322, upload-time = "2025-10-17T19:34:55.028Z" }, ] [[package]] name = "botocore" -version = "1.40.50" +version = "1.40.55" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jmespath" }, { name = "python-dateutil" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5b/66/21d9ac0d37e5c4e55171466351cfc77404d8d664ccc17d4add6dba1dee99/botocore-1.40.50.tar.gz", hash = "sha256:1d3d5b5759c9cb30202cd5ad231ec8afb1abe5be0c088a1707195c2cbae0e742", size = 14417510, upload-time = "2025-10-10T20:12:24.656Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a4/92/dce4842b2e215d213d34b064fcdd13c6a782c43344e77336bcde586e9229/botocore-1.40.55.tar.gz", hash = "sha256:79b6472e2de92b3519d44fc1eec8c5feced7f99a0d10fdea6dc93133426057c1", size = 14446917, upload-time = "2025-10-17T19:34:47.44Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/af/4f817b49558785e969aa2852ae6c3bba8d372169ab5631a004288d2fac20/botocore-1.40.50-py3-none-any.whl", hash = "sha256:53126c153fae0670dc54f03d01c89b1af144acedb1020199b133dedb309e434d", size = 14087905, upload-time = "2025-10-10T20:12:21.872Z" }, + { url = "https://files.pythonhosted.org/packages/21/30/f13bbc36e83b78777ff1abf50a084efcc3336b808e76560d8c5a0c9219e0/botocore-1.40.55-py3-none-any.whl", hash = "sha256:cdc38f7a4ddb30a2cd1cdd4fabde2a5a16e41b5a642292e1c30de5c4e46f5d44", size = 14116107, upload-time = "2025-10-17T19:34:44.398Z" }, ] [[package]] name = "cachetools" -version = "6.2.0" +version = "6.2.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9d/61/e4fad8155db4a04bfb4734c7c8ff0882f078f24294d42798b3568eb63bff/cachetools-6.2.0.tar.gz", hash = "sha256:38b328c0889450f05f5e120f56ab68c8abaf424e1275522b138ffc93253f7e32", size = 30988, upload-time = "2025-08-25T18:57:30.924Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/7e/b975b5814bd36faf009faebe22c1072a1fa1168db34d285ef0ba071ad78c/cachetools-6.2.1.tar.gz", hash = "sha256:3f391e4bd8f8bf0931169baf7456cc822705f4e2a31f840d218f445b9a854201", size = 31325, upload-time = "2025-10-12T14:55:30.139Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6c/56/3124f61d37a7a4e7cc96afc5492c78ba0cb551151e530b54669ddd1436ef/cachetools-6.2.0-py3-none-any.whl", hash = "sha256:1c76a8960c0041fcc21097e357f882197c79da0dbff766e7317890a65d7d8ba6", size = 11276, upload-time = "2025-08-25T18:57:29.684Z" }, + { url = "https://files.pythonhosted.org/packages/96/c5/1e741d26306c42e2bf6ab740b2202872727e0f606033c9dd713f8b93f5a8/cachetools-6.2.1-py3-none-any.whl", hash = "sha256:09868944b6dde876dfd44e1d47e18484541eaf12f26f29b7af91b26cc892d701", size = 11280, upload-time = "2025-10-12T14:55:28.382Z" }, ] [[package]] @@ -260,34 +259,39 @@ wheels = [ [[package]] name = "charset-normalizer" -version = "3.4.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371, upload-time = "2025-08-09T07:57:28.46Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/5e/14c94999e418d9b87682734589404a25854d5f5d0408df68bc15b6ff54bb/charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1", size = 205655, upload-time = "2025-08-09T07:56:08.475Z" }, - { url = "https://files.pythonhosted.org/packages/7d/a8/c6ec5d389672521f644505a257f50544c074cf5fc292d5390331cd6fc9c3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884", size = 146223, upload-time = "2025-08-09T07:56:09.708Z" }, - { url = "https://files.pythonhosted.org/packages/fc/eb/a2ffb08547f4e1e5415fb69eb7db25932c52a52bed371429648db4d84fb1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018", size = 159366, upload-time = "2025-08-09T07:56:11.326Z" }, - { url = "https://files.pythonhosted.org/packages/82/10/0fd19f20c624b278dddaf83b8464dcddc2456cb4b02bb902a6da126b87a1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392", size = 157104, upload-time = "2025-08-09T07:56:13.014Z" }, - { url = "https://files.pythonhosted.org/packages/16/ab/0233c3231af734f5dfcf0844aa9582d5a1466c985bbed6cedab85af9bfe3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f", size = 151830, upload-time = "2025-08-09T07:56:14.428Z" }, - { url = "https://files.pythonhosted.org/packages/ae/02/e29e22b4e02839a0e4a06557b1999d0a47db3567e82989b5bb21f3fbbd9f/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154", size = 148854, upload-time = "2025-08-09T07:56:16.051Z" }, - { url = "https://files.pythonhosted.org/packages/05/6b/e2539a0a4be302b481e8cafb5af8792da8093b486885a1ae4d15d452bcec/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491", size = 160670, upload-time = "2025-08-09T07:56:17.314Z" }, - { url = "https://files.pythonhosted.org/packages/31/e7/883ee5676a2ef217a40ce0bffcc3d0dfbf9e64cbcfbdf822c52981c3304b/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93", size = 158501, upload-time = "2025-08-09T07:56:18.641Z" }, - { url = "https://files.pythonhosted.org/packages/c1/35/6525b21aa0db614cf8b5792d232021dca3df7f90a1944db934efa5d20bb1/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f", size = 153173, upload-time = "2025-08-09T07:56:20.289Z" }, - { url = "https://files.pythonhosted.org/packages/50/ee/f4704bad8201de513fdc8aac1cabc87e38c5818c93857140e06e772b5892/charset_normalizer-3.4.3-cp312-cp312-win32.whl", hash = "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37", size = 99822, upload-time = "2025-08-09T07:56:21.551Z" }, - { url = "https://files.pythonhosted.org/packages/39/f5/3b3836ca6064d0992c58c7561c6b6eee1b3892e9665d650c803bd5614522/charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc", size = 107543, upload-time = "2025-08-09T07:56:23.115Z" }, - { url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" }, +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, + { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, + { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, + { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, + { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, + { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, + { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, + { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, ] [[package]] name = "click" -version = "8.3.0" +version = "8.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/46/61/de6cd827efad202d7057d93e0fed9294b96952e188f7384832791c7b2254/click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4", size = 276943, upload-time = "2025-09-18T17:32:23.696Z" } +sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295, upload-time = "2025-09-18T17:32:22.42Z" }, + { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, ] [[package]] @@ -322,55 +326,55 @@ wheels = [ [[package]] name = "colorlog" -version = "6.9.0" +version = "6.10.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d3/7a/359f4d5df2353f26172b3cc39ea32daa39af8de522205f512f458923e677/colorlog-6.9.0.tar.gz", hash = "sha256:bfba54a1b93b94f54e1f4fe48395725a3d92fd2a4af702f6bd70946bdc0c6ac2", size = 16624, upload-time = "2024-10-29T18:34:51.011Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/61/f083b5ac52e505dfc1c624eafbf8c7589a0d7f32daa398d2e7590efa5fda/colorlog-6.10.1.tar.gz", hash = "sha256:eb4ae5cb65fe7fec7773c2306061a8e63e02efc2c72eba9d27b0fa23c94f1321", size = 17162, upload-time = "2025-10-16T16:14:11.978Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e3/51/9b208e85196941db2f0654ad0357ca6388ab3ed67efdbfc799f35d1f83aa/colorlog-6.9.0-py3-none-any.whl", hash = "sha256:5906e71acd67cb07a71e779c47c4bcb45fb8c2993eebe9e5adcd6a6f1b283eff", size = 11424, upload-time = "2024-10-29T18:34:49.815Z" }, + { url = "https://files.pythonhosted.org/packages/6d/c1/e419ef3723a074172b68aaa89c9f3de486ed4c2399e2dbd8113a4fdcaf9e/colorlog-6.10.1-py3-none-any.whl", hash = "sha256:2d7e8348291948af66122cff006c9f8da6255d224e7cf8e37d8de2df3bad8c9c", size = 11743, upload-time = "2025-10-16T16:14:10.512Z" }, ] [[package]] name = "cryptography" -version = "46.0.2" +version = "46.0.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4a/9b/e301418629f7bfdf72db9e80ad6ed9d1b83c487c471803eaa6464c511a01/cryptography-46.0.2.tar.gz", hash = "sha256:21b6fc8c71a3f9a604f028a329e5560009cc4a3a828bfea5fcba8eb7647d88fe", size = 749293, upload-time = "2025-10-01T00:29:11.856Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/98/7a8df8c19a335c8028414738490fc3955c0cecbfdd37fcc1b9c3d04bd561/cryptography-46.0.2-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:f3e32ab7dd1b1ef67b9232c4cf5e2ee4cd517d4316ea910acaaa9c5712a1c663", size = 7261255, upload-time = "2025-10-01T00:27:22.947Z" }, - { url = "https://files.pythonhosted.org/packages/c6/38/b2adb2aa1baa6706adc3eb746691edd6f90a656a9a65c3509e274d15a2b8/cryptography-46.0.2-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1fd1a69086926b623ef8126b4c33d5399ce9e2f3fac07c9c734c2a4ec38b6d02", size = 4297596, upload-time = "2025-10-01T00:27:25.258Z" }, - { url = "https://files.pythonhosted.org/packages/e4/27/0f190ada240003119488ae66c897b5e97149292988f556aef4a6a2a57595/cryptography-46.0.2-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb7fb9cd44c2582aa5990cf61a4183e6f54eea3172e54963787ba47287edd135", size = 4450899, upload-time = "2025-10-01T00:27:27.458Z" }, - { url = "https://files.pythonhosted.org/packages/85/d5/e4744105ab02fdf6bb58ba9a816e23b7a633255987310b4187d6745533db/cryptography-46.0.2-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9066cfd7f146f291869a9898b01df1c9b0e314bfa182cef432043f13fc462c92", size = 4300382, upload-time = "2025-10-01T00:27:29.091Z" }, - { url = "https://files.pythonhosted.org/packages/33/fb/bf9571065c18c04818cb07de90c43fc042c7977c68e5de6876049559c72f/cryptography-46.0.2-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:97e83bf4f2f2c084d8dd792d13841d0a9b241643151686010866bbd076b19659", size = 4017347, upload-time = "2025-10-01T00:27:30.767Z" }, - { url = "https://files.pythonhosted.org/packages/35/72/fc51856b9b16155ca071080e1a3ad0c3a8e86616daf7eb018d9565b99baa/cryptography-46.0.2-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:4a766d2a5d8127364fd936572c6e6757682fc5dfcbdba1632d4554943199f2fa", size = 4983500, upload-time = "2025-10-01T00:27:32.741Z" }, - { url = "https://files.pythonhosted.org/packages/c1/53/0f51e926799025e31746d454ab2e36f8c3f0d41592bc65cb9840368d3275/cryptography-46.0.2-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:fab8f805e9675e61ed8538f192aad70500fa6afb33a8803932999b1049363a08", size = 4482591, upload-time = "2025-10-01T00:27:34.869Z" }, - { url = "https://files.pythonhosted.org/packages/86/96/4302af40b23ab8aa360862251fb8fc450b2a06ff24bc5e261c2007f27014/cryptography-46.0.2-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:1e3b6428a3d56043bff0bb85b41c535734204e599c1c0977e1d0f261b02f3ad5", size = 4300019, upload-time = "2025-10-01T00:27:37.029Z" }, - { url = "https://files.pythonhosted.org/packages/9b/59/0be12c7fcc4c5e34fe2b665a75bc20958473047a30d095a7657c218fa9e8/cryptography-46.0.2-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:1a88634851d9b8de8bb53726f4300ab191d3b2f42595e2581a54b26aba71b7cc", size = 4950006, upload-time = "2025-10-01T00:27:40.272Z" }, - { url = "https://files.pythonhosted.org/packages/55/1d/42fda47b0111834b49e31590ae14fd020594d5e4dadd639bce89ad790fba/cryptography-46.0.2-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:be939b99d4e091eec9a2bcf41aaf8f351f312cd19ff74b5c83480f08a8a43e0b", size = 4482088, upload-time = "2025-10-01T00:27:42.668Z" }, - { url = "https://files.pythonhosted.org/packages/17/50/60f583f69aa1602c2bdc7022dae86a0d2b837276182f8c1ec825feb9b874/cryptography-46.0.2-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f13b040649bc18e7eb37936009b24fd31ca095a5c647be8bb6aaf1761142bd1", size = 4425599, upload-time = "2025-10-01T00:27:44.616Z" }, - { url = "https://files.pythonhosted.org/packages/d1/57/d8d4134cd27e6e94cf44adb3f3489f935bde85f3a5508e1b5b43095b917d/cryptography-46.0.2-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9bdc25e4e01b261a8fda4e98618f1c9515febcecebc9566ddf4a70c63967043b", size = 4697458, upload-time = "2025-10-01T00:27:46.209Z" }, - { url = "https://files.pythonhosted.org/packages/d1/2b/531e37408573e1da33adfb4c58875013ee8ac7d548d1548967d94a0ae5c4/cryptography-46.0.2-cp311-abi3-win32.whl", hash = "sha256:8b9bf67b11ef9e28f4d78ff88b04ed0929fcd0e4f70bb0f704cfc32a5c6311ee", size = 3056077, upload-time = "2025-10-01T00:27:48.424Z" }, - { url = "https://files.pythonhosted.org/packages/a8/cd/2f83cafd47ed2dc5a3a9c783ff5d764e9e70d3a160e0df9a9dcd639414ce/cryptography-46.0.2-cp311-abi3-win_amd64.whl", hash = "sha256:758cfc7f4c38c5c5274b55a57ef1910107436f4ae842478c4989abbd24bd5acb", size = 3512585, upload-time = "2025-10-01T00:27:50.521Z" }, - { url = "https://files.pythonhosted.org/packages/00/36/676f94e10bfaa5c5b86c469ff46d3e0663c5dc89542f7afbadac241a3ee4/cryptography-46.0.2-cp311-abi3-win_arm64.whl", hash = "sha256:218abd64a2e72f8472c2102febb596793347a3e65fafbb4ad50519969da44470", size = 2927474, upload-time = "2025-10-01T00:27:52.91Z" }, - { url = "https://files.pythonhosted.org/packages/d5/bb/fa95abcf147a1b0bb94d95f53fbb09da77b24c776c5d87d36f3d94521d2c/cryptography-46.0.2-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a08e7401a94c002e79dc3bc5231b6558cd4b2280ee525c4673f650a37e2c7685", size = 7248090, upload-time = "2025-10-01T00:28:22.846Z" }, - { url = "https://files.pythonhosted.org/packages/b7/66/f42071ce0e3ffbfa80a88feadb209c779fda92a23fbc1e14f74ebf72ef6b/cryptography-46.0.2-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d30bc11d35743bf4ddf76674a0a369ec8a21f87aaa09b0661b04c5f6c46e8d7b", size = 4293123, upload-time = "2025-10-01T00:28:25.072Z" }, - { url = "https://files.pythonhosted.org/packages/a8/5d/1fdbd2e5c1ba822828d250e5a966622ef00185e476d1cd2726b6dd135e53/cryptography-46.0.2-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bca3f0ce67e5a2a2cf524e86f44697c4323a86e0fd7ba857de1c30d52c11ede1", size = 4439524, upload-time = "2025-10-01T00:28:26.808Z" }, - { url = "https://files.pythonhosted.org/packages/c8/c1/5e4989a7d102d4306053770d60f978c7b6b1ea2ff8c06e0265e305b23516/cryptography-46.0.2-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ff798ad7a957a5021dcbab78dfff681f0cf15744d0e6af62bd6746984d9c9e9c", size = 4297264, upload-time = "2025-10-01T00:28:29.327Z" }, - { url = "https://files.pythonhosted.org/packages/28/78/b56f847d220cb1d6d6aef5a390e116ad603ce13a0945a3386a33abc80385/cryptography-46.0.2-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:cb5e8daac840e8879407acbe689a174f5ebaf344a062f8918e526824eb5d97af", size = 4011872, upload-time = "2025-10-01T00:28:31.479Z" }, - { url = "https://files.pythonhosted.org/packages/e1/80/2971f214b066b888944f7b57761bf709ee3f2cf805619a18b18cab9b263c/cryptography-46.0.2-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:3f37aa12b2d91e157827d90ce78f6180f0c02319468a0aea86ab5a9566da644b", size = 4978458, upload-time = "2025-10-01T00:28:33.267Z" }, - { url = "https://files.pythonhosted.org/packages/a5/84/0cb0a2beaa4f1cbe63ebec4e97cd7e0e9f835d0ba5ee143ed2523a1e0016/cryptography-46.0.2-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:5e38f203160a48b93010b07493c15f2babb4e0f2319bbd001885adb3f3696d21", size = 4472195, upload-time = "2025-10-01T00:28:36.039Z" }, - { url = "https://files.pythonhosted.org/packages/30/8b/2b542ddbf78835c7cd67b6fa79e95560023481213a060b92352a61a10efe/cryptography-46.0.2-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:d19f5f48883752b5ab34cff9e2f7e4a7f216296f33714e77d1beb03d108632b6", size = 4296791, upload-time = "2025-10-01T00:28:37.732Z" }, - { url = "https://files.pythonhosted.org/packages/78/12/9065b40201b4f4876e93b9b94d91feb18de9150d60bd842a16a21565007f/cryptography-46.0.2-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:04911b149eae142ccd8c9a68892a70c21613864afb47aba92d8c7ed9cc001023", size = 4939629, upload-time = "2025-10-01T00:28:39.654Z" }, - { url = "https://files.pythonhosted.org/packages/f6/9e/6507dc048c1b1530d372c483dfd34e7709fc542765015425f0442b08547f/cryptography-46.0.2-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:8b16c1ede6a937c291d41176934268e4ccac2c6521c69d3f5961c5a1e11e039e", size = 4471988, upload-time = "2025-10-01T00:28:41.822Z" }, - { url = "https://files.pythonhosted.org/packages/b1/86/d025584a5f7d5c5ec8d3633dbcdce83a0cd579f1141ceada7817a4c26934/cryptography-46.0.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:747b6f4a4a23d5a215aadd1d0b12233b4119c4313df83ab4137631d43672cc90", size = 4422989, upload-time = "2025-10-01T00:28:43.608Z" }, - { url = "https://files.pythonhosted.org/packages/4b/39/536370418b38a15a61bbe413006b79dfc3d2b4b0eafceb5581983f973c15/cryptography-46.0.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6b275e398ab3a7905e168c036aad54b5969d63d3d9099a0a66cc147a3cc983be", size = 4685578, upload-time = "2025-10-01T00:28:45.361Z" }, - { url = "https://files.pythonhosted.org/packages/15/52/ea7e2b1910f547baed566c866fbb86de2402e501a89ecb4871ea7f169a81/cryptography-46.0.2-cp38-abi3-win32.whl", hash = "sha256:0b507c8e033307e37af61cb9f7159b416173bdf5b41d11c4df2e499a1d8e007c", size = 3036711, upload-time = "2025-10-01T00:28:47.096Z" }, - { url = "https://files.pythonhosted.org/packages/71/9e/171f40f9c70a873e73c2efcdbe91e1d4b1777a03398fa1c4af3c56a2477a/cryptography-46.0.2-cp38-abi3-win_amd64.whl", hash = "sha256:f9b2dc7668418fb6f221e4bf701f716e05e8eadb4f1988a2487b11aedf8abe62", size = 3500007, upload-time = "2025-10-01T00:28:48.967Z" }, - { url = "https://files.pythonhosted.org/packages/3e/7c/15ad426257615f9be8caf7f97990cf3dcbb5b8dd7ed7e0db581a1c4759dd/cryptography-46.0.2-cp38-abi3-win_arm64.whl", hash = "sha256:91447f2b17e83c9e0c89f133119d83f94ce6e0fb55dd47da0a959316e6e9cfa1", size = 2918153, upload-time = "2025-10-01T00:28:51.003Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258, upload-time = "2025-10-15T23:18:31.74Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/42/9c391dd801d6cf0d561b5890549d4b27bafcc53b39c31a817e69d87c625b/cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a", size = 7225004, upload-time = "2025-10-15T23:16:52.239Z" }, + { url = "https://files.pythonhosted.org/packages/1c/67/38769ca6b65f07461eb200e85fc1639b438bdc667be02cf7f2cd6a64601c/cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc", size = 4296667, upload-time = "2025-10-15T23:16:54.369Z" }, + { url = "https://files.pythonhosted.org/packages/5c/49/498c86566a1d80e978b42f0d702795f69887005548c041636df6ae1ca64c/cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", size = 4450807, upload-time = "2025-10-15T23:16:56.414Z" }, + { url = "https://files.pythonhosted.org/packages/4b/0a/863a3604112174c8624a2ac3c038662d9e59970c7f926acdcfaed8d61142/cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", size = 4299615, upload-time = "2025-10-15T23:16:58.442Z" }, + { url = "https://files.pythonhosted.org/packages/64/02/b73a533f6b64a69f3cd3872acb6ebc12aef924d8d103133bb3ea750dc703/cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849", size = 4016800, upload-time = "2025-10-15T23:17:00.378Z" }, + { url = "https://files.pythonhosted.org/packages/25/d5/16e41afbfa450cde85a3b7ec599bebefaef16b5c6ba4ec49a3532336ed72/cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8", size = 4984707, upload-time = "2025-10-15T23:17:01.98Z" }, + { url = "https://files.pythonhosted.org/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec", size = 4482541, upload-time = "2025-10-15T23:17:04.078Z" }, + { url = "https://files.pythonhosted.org/packages/78/f6/50736d40d97e8483172f1bb6e698895b92a223dba513b0ca6f06b2365339/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91", size = 4299464, upload-time = "2025-10-15T23:17:05.483Z" }, + { url = "https://files.pythonhosted.org/packages/00/de/d8e26b1a855f19d9994a19c702fa2e93b0456beccbcfe437eda00e0701f2/cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e", size = 4950838, upload-time = "2025-10-15T23:17:07.425Z" }, + { url = "https://files.pythonhosted.org/packages/8f/29/798fc4ec461a1c9e9f735f2fc58741b0daae30688f41b2497dcbc9ed1355/cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926", size = 4481596, upload-time = "2025-10-15T23:17:09.343Z" }, + { url = "https://files.pythonhosted.org/packages/15/8d/03cd48b20a573adfff7652b76271078e3045b9f49387920e7f1f631d125e/cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71", size = 4426782, upload-time = "2025-10-15T23:17:11.22Z" }, + { url = "https://files.pythonhosted.org/packages/fa/b1/ebacbfe53317d55cf33165bda24c86523497a6881f339f9aae5c2e13e57b/cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac", size = 4698381, upload-time = "2025-10-15T23:17:12.829Z" }, + { url = "https://files.pythonhosted.org/packages/96/92/8a6a9525893325fc057a01f654d7efc2c64b9de90413adcf605a85744ff4/cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018", size = 3055988, upload-time = "2025-10-15T23:17:14.65Z" }, + { url = "https://files.pythonhosted.org/packages/7e/bf/80fbf45253ea585a1e492a6a17efcb93467701fa79e71550a430c5e60df0/cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb", size = 3514451, upload-time = "2025-10-15T23:17:16.142Z" }, + { url = "https://files.pythonhosted.org/packages/2e/af/9b302da4c87b0beb9db4e756386a7c6c5b8003cd0e742277888d352ae91d/cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c", size = 2928007, upload-time = "2025-10-15T23:17:18.04Z" }, + { url = "https://files.pythonhosted.org/packages/fd/23/45fe7f376a7df8daf6da3556603b36f53475a99ce4faacb6ba2cf3d82021/cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936", size = 7218248, upload-time = "2025-10-15T23:17:46.294Z" }, + { url = "https://files.pythonhosted.org/packages/27/32/b68d27471372737054cbd34c84981f9edbc24fe67ca225d389799614e27f/cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683", size = 4294089, upload-time = "2025-10-15T23:17:48.269Z" }, + { url = "https://files.pythonhosted.org/packages/26/42/fa8389d4478368743e24e61eea78846a0006caffaf72ea24a15159215a14/cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", size = 4440029, upload-time = "2025-10-15T23:17:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/5f/eb/f483db0ec5ac040824f269e93dd2bd8a21ecd1027e77ad7bdf6914f2fd80/cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", size = 4297222, upload-time = "2025-10-15T23:17:51.357Z" }, + { url = "https://files.pythonhosted.org/packages/fd/cf/da9502c4e1912cb1da3807ea3618a6829bee8207456fbbeebc361ec38ba3/cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc", size = 4012280, upload-time = "2025-10-15T23:17:52.964Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8f/9adb86b93330e0df8b3dcf03eae67c33ba89958fc2e03862ef1ac2b42465/cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3", size = 4978958, upload-time = "2025-10-15T23:17:54.965Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a0/5fa77988289c34bdb9f913f5606ecc9ada1adb5ae870bd0d1054a7021cc4/cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971", size = 4473714, upload-time = "2025-10-15T23:17:56.754Z" }, + { url = "https://files.pythonhosted.org/packages/14/e5/fc82d72a58d41c393697aa18c9abe5ae1214ff6f2a5c18ac470f92777895/cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac", size = 4296970, upload-time = "2025-10-15T23:17:58.588Z" }, + { url = "https://files.pythonhosted.org/packages/78/06/5663ed35438d0b09056973994f1aec467492b33bd31da36e468b01ec1097/cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04", size = 4940236, upload-time = "2025-10-15T23:18:00.897Z" }, + { url = "https://files.pythonhosted.org/packages/fc/59/873633f3f2dcd8a053b8dd1d38f783043b5fce589c0f6988bf55ef57e43e/cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506", size = 4472642, upload-time = "2025-10-15T23:18:02.749Z" }, + { url = "https://files.pythonhosted.org/packages/3d/39/8e71f3930e40f6877737d6f69248cf74d4e34b886a3967d32f919cc50d3b/cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963", size = 4423126, upload-time = "2025-10-15T23:18:04.85Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c7/f65027c2810e14c3e7268353b1681932b87e5a48e65505d8cc17c99e36ae/cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4", size = 4686573, upload-time = "2025-10-15T23:18:06.908Z" }, + { url = "https://files.pythonhosted.org/packages/0a/6e/1c8331ddf91ca4730ab3086a0f1be19c65510a33b5a441cb334e7a2d2560/cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df", size = 3036695, upload-time = "2025-10-15T23:18:08.672Z" }, + { url = "https://files.pythonhosted.org/packages/90/45/b0d691df20633eff80955a0fc7695ff9051ffce8b69741444bd9ed7bd0db/cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f", size = 3501720, upload-time = "2025-10-15T23:18:10.632Z" }, + { url = "https://files.pythonhosted.org/packages/e8/cb/2da4cc83f5edb9c3257d09e1e7ab7b23f049c7962cae8d842bbef0a9cec9/cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372", size = 2918740, upload-time = "2025-10-15T23:18:12.277Z" }, ] [[package]] @@ -388,7 +392,7 @@ wheels = [ [[package]] name = "deepeval" -version = "3.6.6" +version = "3.6.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp" }, @@ -424,9 +428,9 @@ dependencies = [ { name = "typer" }, { name = "wheel" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ba/21/888e266a86efaa2796e9cc60589fec424efd776450acf52d98724304b84f/deepeval-3.6.6.tar.gz", hash = "sha256:18bd0c167fc3586512c301ce8c38b8ea1488933e9caaec0db1afaee04b5d7761", size = 433824, upload-time = "2025-10-08T18:34:55.172Z" } +sdist = { url = "https://files.pythonhosted.org/packages/90/f9/090957836bd7e9ab0dd5052c3802041cd4868dc26cb58955c6d83597e166/deepeval-3.6.7.tar.gz", hash = "sha256:4bb2266c35d7b2521a1f9def4562236728c9bbbaf373d956fba2e69eb8061f31", size = 447687, upload-time = "2025-10-15T18:44:23.903Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1f/b2/43276ed47c0f15a29106fb44b25b6f72a9b094264741a58f4b4a22daf898/deepeval-3.6.6-py3-none-any.whl", hash = "sha256:9233363f6ec03b41c6c75c31cc931c0835abccbf4bb65fa59b7cc47313661844", size = 627653, upload-time = "2025-10-08T18:34:53.218Z" }, + { url = "https://files.pythonhosted.org/packages/54/63/e04cb87aa45c903be71706d9378954a6607a8240a693fb5a5f436ab5eb3f/deepeval-3.6.7-py3-none-any.whl", hash = "sha256:46fe72869359a7afb7baa34880300ed3660e9b387dfd3341a461e1d96bc5f021", size = 645173, upload-time = "2025-10-15T18:44:21.029Z" }, ] [[package]] @@ -542,16 +546,16 @@ wheels = [ [[package]] name = "fastapi" -version = "0.119.0" +version = "0.119.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "starlette" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0a/f9/5c5bcce82a7997cc0eb8c47b7800f862f6b56adc40486ed246e5010d443b/fastapi-0.119.0.tar.gz", hash = "sha256:451082403a2c1f0b99c6bd57c09110ed5463856804c8078d38e5a1f1035dbbb7", size = 336756, upload-time = "2025-10-11T17:13:40.53Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/f4/152127681182e6413e7a89684c434e19e7414ed7ac0c632999c3c6980640/fastapi-0.119.1.tar.gz", hash = "sha256:a5e3426edce3fe221af4e1992c6d79011b247e3b03cc57999d697fe76cbf8ae0", size = 338616, upload-time = "2025-10-20T11:30:27.734Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/70/584c4d7cad80f5e833715c0a29962d7c93b4d18eed522a02981a6d1b6ee5/fastapi-0.119.0-py3-none-any.whl", hash = "sha256:90a2e49ed19515320abb864df570dd766be0662c5d577688f1600170f7f73cf2", size = 107095, upload-time = "2025-10-11T17:13:39.048Z" }, + { url = "https://files.pythonhosted.org/packages/b1/26/e6d959b4ac959fdb3e9c4154656fc160794db6af8e64673d52759456bf07/fastapi-0.119.1-py3-none-any.whl", hash = "sha256:0b8c2a2cce853216e150e9bd4faaed88227f8eb37de21cb200771f491586a27f", size = 108123, upload-time = "2025-10-20T11:30:26.185Z" }, ] [[package]] @@ -577,21 +581,21 @@ wheels = [ [[package]] name = "fastuuid" -version = "0.13.5" +version = "0.14.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/15/80/3c16a1edad2e6cd82fbd15ac998cc1b881f478bf1f80ca717d941c441874/fastuuid-0.13.5.tar.gz", hash = "sha256:d4976821ab424d41542e1ea39bc828a9d454c3f8a04067c06fca123c5b95a1a1", size = 18255, upload-time = "2025-09-26T09:05:38.281Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/7d/d9daedf0f2ebcacd20d599928f8913e9d2aea1d56d2d355a93bfa2b611d7/fastuuid-0.14.0.tar.gz", hash = "sha256:178947fc2f995b38497a74172adee64fdeb8b7ec18f2a5934d037641ba265d26", size = 18232, upload-time = "2025-10-19T22:19:22.402Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/21/36/434f137c5970cac19e57834e1f7680e85301619d49891618c00666700c61/fastuuid-0.13.5-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:35fe8045e866bc6846f8de6fa05acb1de0c32478048484a995e96d31e21dff2a", size = 494638, upload-time = "2025-09-26T09:14:58.695Z" }, - { url = "https://files.pythonhosted.org/packages/ca/3c/083de2ac007b2b305523b9c006dba5051e5afd87a626ef1a39f76e2c6b82/fastuuid-0.13.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:02a460333f52d731a006d18a52ef6fcb2d295a1f5b1a5938d30744191b2f77b7", size = 253138, upload-time = "2025-09-26T09:13:33.283Z" }, - { url = "https://files.pythonhosted.org/packages/73/5e/630cffa1c8775db526e39e9e4c5c7db0c27be0786bb21ba82c912ae19f63/fastuuid-0.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:74b0e4f8c307b9f477a5d7284db4431ce53a3c1e3f4173db7a97db18564a6202", size = 244521, upload-time = "2025-09-26T09:14:40.682Z" }, - { url = "https://files.pythonhosted.org/packages/4d/51/55d78705f4fbdadf88fb40f382f508d6c7a4941ceddd7825fafebb4cc778/fastuuid-0.13.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6955a99ef455c2986f3851f4e0ccc35dec56ac1a7720f2b92e88a75d6684512e", size = 271557, upload-time = "2025-09-26T09:15:09.75Z" }, - { url = "https://files.pythonhosted.org/packages/6a/2b/1b89e90a8635e5587ccdbbeb169c590672ce7637880f2c047482a0359950/fastuuid-0.13.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f10c77b826738c1a27dcdaa92ea4dc1ec9d869748a99e1fde54f1379553d4854", size = 272334, upload-time = "2025-09-26T09:07:48.865Z" }, - { url = "https://files.pythonhosted.org/packages/0c/06/4c8207894eeb30414999e5c3f66ac039bc4003437eb4060d8a1bceb4cc6f/fastuuid-0.13.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bb25dccbeb249d16d5e664f65f17ebec05136821d5ef462c4110e3f76b86fb86", size = 290594, upload-time = "2025-09-26T09:12:54.124Z" }, - { url = "https://files.pythonhosted.org/packages/50/69/96d221931a31d77a47cc2487bdfacfb3091edfc2e7a04b1795df1aec05df/fastuuid-0.13.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:a5becc646a3eeafb76ce0a6783ba190cd182e3790a8b2c78ca9db2b5e87af952", size = 452835, upload-time = "2025-09-26T09:14:00.994Z" }, - { url = "https://files.pythonhosted.org/packages/25/ef/bf045f0a47dcec96247497ef3f7a31d86ebc074330e2dccc34b8dbc0468a/fastuuid-0.13.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:69b34363752d06e9bb0dbdf02ae391ec56ac948c6f2eb00be90dad68e80774b9", size = 468225, upload-time = "2025-09-26T09:13:38.585Z" }, - { url = "https://files.pythonhosted.org/packages/30/46/4817ab5a3778927155a4bde92540d4c4fa996161ec8b8e080c8928b0984e/fastuuid-0.13.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:57d0768afcad0eab8770c9b8cf904716bd3c547e8b9a4e755ee8a673b060a3a3", size = 444907, upload-time = "2025-09-26T09:14:30.163Z" }, - { url = "https://files.pythonhosted.org/packages/80/27/ab284117ce4dc9b356a7196bdbf220510285f201d27f1f078592cdc8187b/fastuuid-0.13.5-cp312-cp312-win32.whl", hash = "sha256:8ac6c6f5129d52eaa6ef9ea4b6e2f7c69468a053f3ab8e439661186b9c06bb85", size = 145415, upload-time = "2025-09-26T09:08:59.494Z" }, - { url = "https://files.pythonhosted.org/packages/f4/0c/f970a4222773b248931819f8940800b760283216ca3dda173ed027e94bdd/fastuuid-0.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:ad630e97715beefef07ec37c9c162336e500400774e2c1cbe1a0df6f80d15b9a", size = 150840, upload-time = "2025-09-26T09:13:46.115Z" }, + { url = "https://files.pythonhosted.org/packages/02/a2/e78fcc5df65467f0d207661b7ef86c5b7ac62eea337c0c0fcedbeee6fb13/fastuuid-0.14.0-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:77e94728324b63660ebf8adb27055e92d2e4611645bf12ed9d88d30486471d0a", size = 510164, upload-time = "2025-10-19T22:31:45.635Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b3/c846f933f22f581f558ee63f81f29fa924acd971ce903dab1a9b6701816e/fastuuid-0.14.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:caa1f14d2102cb8d353096bc6ef6c13b2c81f347e6ab9d6fbd48b9dea41c153d", size = 261837, upload-time = "2025-10-19T22:38:38.53Z" }, + { url = "https://files.pythonhosted.org/packages/54/ea/682551030f8c4fa9a769d9825570ad28c0c71e30cf34020b85c1f7ee7382/fastuuid-0.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d23ef06f9e67163be38cece704170486715b177f6baae338110983f99a72c070", size = 251370, upload-time = "2025-10-19T22:40:26.07Z" }, + { url = "https://files.pythonhosted.org/packages/14/dd/5927f0a523d8e6a76b70968e6004966ee7df30322f5fc9b6cdfb0276646a/fastuuid-0.14.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c9ec605ace243b6dbe3bd27ebdd5d33b00d8d1d3f580b39fdd15cd96fd71796", size = 277766, upload-time = "2025-10-19T22:37:23.779Z" }, + { url = "https://files.pythonhosted.org/packages/16/6e/c0fb547eef61293153348f12e0f75a06abb322664b34a1573a7760501336/fastuuid-0.14.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:808527f2407f58a76c916d6aa15d58692a4a019fdf8d4c32ac7ff303b7d7af09", size = 278105, upload-time = "2025-10-19T22:26:56.821Z" }, + { url = "https://files.pythonhosted.org/packages/2d/b1/b9c75e03b768f61cf2e84ee193dc18601aeaf89a4684b20f2f0e9f52b62c/fastuuid-0.14.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2fb3c0d7fef6674bbeacdd6dbd386924a7b60b26de849266d1ff6602937675c8", size = 301564, upload-time = "2025-10-19T22:30:31.604Z" }, + { url = "https://files.pythonhosted.org/packages/fc/fa/f7395fdac07c7a54f18f801744573707321ca0cee082e638e36452355a9d/fastuuid-0.14.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab3f5d36e4393e628a4df337c2c039069344db5f4b9d2a3c9cea48284f1dd741", size = 459659, upload-time = "2025-10-19T22:31:32.341Z" }, + { url = "https://files.pythonhosted.org/packages/66/49/c9fd06a4a0b1f0f048aacb6599e7d96e5d6bc6fa680ed0d46bf111929d1b/fastuuid-0.14.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:b9a0ca4f03b7e0b01425281ffd44e99d360e15c895f1907ca105854ed85e2057", size = 478430, upload-time = "2025-10-19T22:26:22.962Z" }, + { url = "https://files.pythonhosted.org/packages/be/9c/909e8c95b494e8e140e8be6165d5fc3f61fdc46198c1554df7b3e1764471/fastuuid-0.14.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3acdf655684cc09e60fb7e4cf524e8f42ea760031945aa8086c7eae2eeeabeb8", size = 450894, upload-time = "2025-10-19T22:27:01.647Z" }, + { url = "https://files.pythonhosted.org/packages/90/eb/d29d17521976e673c55ef7f210d4cdd72091a9ec6755d0fd4710d9b3c871/fastuuid-0.14.0-cp312-cp312-win32.whl", hash = "sha256:9579618be6280700ae36ac42c3efd157049fe4dd40ca49b021280481c78c3176", size = 154374, upload-time = "2025-10-19T22:29:19.879Z" }, + { url = "https://files.pythonhosted.org/packages/cc/fc/f5c799a6ea6d877faec0472d0b27c079b47c86b1cdc577720a5386483b36/fastuuid-0.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:d9e4332dc4ba054434a9594cbfaf7823b57993d7d8e7267831c3e059857cf397", size = 156550, upload-time = "2025-10-19T22:27:49.658Z" }, ] [[package]] @@ -671,7 +675,7 @@ wheels = [ [[package]] name = "google-genai" -version = "1.42.0" +version = "1.45.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -683,21 +687,21 @@ dependencies = [ { name = "typing-extensions" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/18/03/84d04ce446d885eb978abb4b7c785f54a39435f02b182f457a996f5c9eb4/google_genai-1.42.0.tar.gz", hash = "sha256:0cef624c725a358f182e6988632371205bed9be1b1dbcf4296dbbd4eb4a9fb5d", size = 235620, upload-time = "2025-10-08T22:13:36.654Z" } +sdist = { url = "https://files.pythonhosted.org/packages/91/77/776b92f6f7cf7d7d3bc77b44a323605ae0f94f807cf9a4977c90d296b6b4/google_genai-1.45.0.tar.gz", hash = "sha256:96ec32ae99a30b5a1b54cb874b577ec6e41b5d5b808bf0f10ed4620e867f9386", size = 238198, upload-time = "2025-10-15T23:03:07.713Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f2/0a/8519cb752c10254899608de5c8cf5ff5ae05260a4ad5db0087fa466ddf46/google_genai-1.42.0-py3-none-any.whl", hash = "sha256:1e45c3ecc630a358c153a08b10d5b03d7c70cf3342fd116ac8a6cc4262cd81e8", size = 236204, upload-time = "2025-10-08T22:13:34.059Z" }, + { url = "https://files.pythonhosted.org/packages/11/8f/922116dabe3d0312f08903d324db6ac9d406832cf57707550bc61151d91b/google_genai-1.45.0-py3-none-any.whl", hash = "sha256:e755295063e5fd5a4c44acff782a569e37fa8f76a6c75d0ede3375c70d916b7f", size = 238495, upload-time = "2025-10-15T23:03:05.926Z" }, ] [[package]] name = "googleapis-common-protos" -version = "1.70.0" +version = "1.71.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/39/24/33db22342cf4a2ea27c9955e6713140fedd51e8b141b5ce5260897020f1a/googleapis_common_protos-1.70.0.tar.gz", hash = "sha256:0e1b44e0ea153e6594f9f394fef15193a68aaaea2d843f83e2742717ca753257", size = 145903, upload-time = "2025-04-14T10:17:02.924Z" } +sdist = { url = "https://files.pythonhosted.org/packages/30/43/b25abe02db2911397819003029bef768f68a974f2ece483e6084d1a5f754/googleapis_common_protos-1.71.0.tar.gz", hash = "sha256:1aec01e574e29da63c80ba9f7bbf1ccfaacf1da877f23609fe236ca7c72a2e2e", size = 146454, upload-time = "2025-10-20T14:58:08.732Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/86/f1/62a193f0227cf15a920390abe675f386dec35f7ae3ffe6da582d3ade42c7/googleapis_common_protos-1.70.0-py3-none-any.whl", hash = "sha256:b8bfcca8c25a2bb253e0e0b0adaf8c00773e5e6af6fd92397576680b807e0fd8", size = 294530, upload-time = "2025-04-14T10:17:01.271Z" }, + { url = "https://files.pythonhosted.org/packages/25/e8/eba9fece11d57a71e3e22ea672742c8f3cf23b35730c9e96db768b295216/googleapis_common_protos-1.71.0-py3-none-any.whl", hash = "sha256:59034a1d849dc4d18971997a72ac56246570afdd17f9369a0ff68218d50ab78c", size = 294576, upload-time = "2025-10-20T14:56:21.295Z" }, ] [[package]] @@ -889,11 +893,11 @@ wheels = [ [[package]] name = "idna" -version = "3.10" +version = "3.11" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, ] [[package]] @@ -910,11 +914,11 @@ wheels = [ [[package]] name = "iniconfig" -version = "2.1.0" +version = "2.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] [[package]] @@ -931,22 +935,27 @@ wheels = [ [[package]] name = "jiter" -version = "0.11.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9d/c0/a3bb4cc13aced219dd18191ea66e874266bd8aa7b96744e495e1c733aa2d/jiter-0.11.0.tar.gz", hash = "sha256:1d9637eaf8c1d6a63d6562f2a6e5ab3af946c66037eb1b894e8fad75422266e4", size = 167094, upload-time = "2025-09-15T09:20:38.212Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ba/b5/3009b112b8f673e568ef79af9863d8309a15f0a8cdcc06ed6092051f377e/jiter-0.11.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:2fb7b377688cc3850bbe5c192a6bd493562a0bc50cbc8b047316428fbae00ada", size = 305510, upload-time = "2025-09-15T09:19:25.893Z" }, - { url = "https://files.pythonhosted.org/packages/fe/82/15514244e03b9e71e086bbe2a6de3e4616b48f07d5f834200c873956fb8c/jiter-0.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a1b7cbe3f25bd0d8abb468ba4302a5d45617ee61b2a7a638f63fee1dc086be99", size = 316521, upload-time = "2025-09-15T09:19:27.525Z" }, - { url = "https://files.pythonhosted.org/packages/92/94/7a2e905f40ad2d6d660e00b68d818f9e29fb87ffe82774f06191e93cbe4a/jiter-0.11.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c0a7f0ec81d5b7588c5cade1eb1925b91436ae6726dc2df2348524aeabad5de6", size = 338214, upload-time = "2025-09-15T09:19:28.727Z" }, - { url = "https://files.pythonhosted.org/packages/a8/9c/5791ed5bdc76f12110158d3316a7a3ec0b1413d018b41c5ed399549d3ad5/jiter-0.11.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07630bb46ea2a6b9c6ed986c6e17e35b26148cce2c535454b26ee3f0e8dcaba1", size = 361280, upload-time = "2025-09-15T09:19:30.013Z" }, - { url = "https://files.pythonhosted.org/packages/d4/7f/b7d82d77ff0d2cb06424141000176b53a9e6b16a1125525bb51ea4990c2e/jiter-0.11.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7764f27d28cd4a9cbc61704dfcd80c903ce3aad106a37902d3270cd6673d17f4", size = 487895, upload-time = "2025-09-15T09:19:31.424Z" }, - { url = "https://files.pythonhosted.org/packages/42/44/10a1475d46f1fc1fd5cc2e82c58e7bca0ce5852208e0fa5df2f949353321/jiter-0.11.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1d4a6c4a737d486f77f842aeb22807edecb4a9417e6700c7b981e16d34ba7c72", size = 378421, upload-time = "2025-09-15T09:19:32.746Z" }, - { url = "https://files.pythonhosted.org/packages/9a/5f/0dc34563d8164d31d07bc09d141d3da08157a68dcd1f9b886fa4e917805b/jiter-0.11.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf408d2a0abd919b60de8c2e7bc5eeab72d4dafd18784152acc7c9adc3291591", size = 347932, upload-time = "2025-09-15T09:19:34.612Z" }, - { url = "https://files.pythonhosted.org/packages/f7/de/b68f32a4fcb7b4a682b37c73a0e5dae32180140cd1caf11aef6ad40ddbf2/jiter-0.11.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cdef53eda7d18e799625023e1e250dbc18fbc275153039b873ec74d7e8883e09", size = 386959, upload-time = "2025-09-15T09:19:35.994Z" }, - { url = "https://files.pythonhosted.org/packages/76/0a/c08c92e713b6e28972a846a81ce374883dac2f78ec6f39a0dad9f2339c3a/jiter-0.11.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:53933a38ef7b551dd9c7f1064f9d7bb235bb3168d0fa5f14f0798d1b7ea0d9c5", size = 517187, upload-time = "2025-09-15T09:19:37.426Z" }, - { url = "https://files.pythonhosted.org/packages/89/b5/4a283bec43b15aad54fcae18d951f06a2ec3f78db5708d3b59a48e9c3fbd/jiter-0.11.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:11840d2324c9ab5162fc1abba23bc922124fedcff0d7b7f85fffa291e2f69206", size = 509461, upload-time = "2025-09-15T09:19:38.761Z" }, - { url = "https://files.pythonhosted.org/packages/34/a5/f8bad793010534ea73c985caaeef8cc22dfb1fedb15220ecdf15c623c07a/jiter-0.11.0-cp312-cp312-win32.whl", hash = "sha256:4f01a744d24a5f2bb4a11657a1b27b61dc038ae2e674621a74020406e08f749b", size = 206664, upload-time = "2025-09-15T09:19:40.096Z" }, - { url = "https://files.pythonhosted.org/packages/ed/42/5823ec2b1469395a160b4bf5f14326b4a098f3b6898fbd327366789fa5d3/jiter-0.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:29fff31190ab3a26de026da2f187814f4b9c6695361e20a9ac2123e4d4378a4c", size = 203520, upload-time = "2025-09-15T09:19:41.798Z" }, +version = "0.11.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/68/0357982493a7b20925aece061f7fb7a2678e3b232f8d73a6edb7e5304443/jiter-0.11.1.tar.gz", hash = "sha256:849dcfc76481c0ea0099391235b7ca97d7279e0fa4c86005457ac7c88e8b76dc", size = 168385, upload-time = "2025-10-17T11:31:15.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/8b/318e8af2c904a9d29af91f78c1e18f0592e189bbdb8a462902d31fe20682/jiter-0.11.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:c92148eec91052538ce6823dfca9525f5cfc8b622d7f07e9891a280f61b8c96c", size = 305655, upload-time = "2025-10-17T11:29:18.859Z" }, + { url = "https://files.pythonhosted.org/packages/f7/29/6c7de6b5d6e511d9e736312c0c9bfcee8f9b6bef68182a08b1d78767e627/jiter-0.11.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ecd4da91b5415f183a6be8f7158d127bdd9e6a3174138293c0d48d6ea2f2009d", size = 315645, upload-time = "2025-10-17T11:29:20.889Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5f/ef9e5675511ee0eb7f98dd8c90509e1f7743dbb7c350071acae87b0145f3/jiter-0.11.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7e3ac25c00b9275684d47aa42febaa90a9958e19fd1726c4ecf755fbe5e553b", size = 348003, upload-time = "2025-10-17T11:29:22.712Z" }, + { url = "https://files.pythonhosted.org/packages/56/1b/abe8c4021010b0a320d3c62682769b700fb66f92c6db02d1a1381b3db025/jiter-0.11.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:57d7305c0a841858f866cd459cd9303f73883fb5e097257f3d4a3920722c69d4", size = 365122, upload-time = "2025-10-17T11:29:24.408Z" }, + { url = "https://files.pythonhosted.org/packages/2a/2d/4a18013939a4f24432f805fbd5a19893e64650b933edb057cd405275a538/jiter-0.11.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e86fa10e117dce22c547f31dd6d2a9a222707d54853d8de4e9a2279d2c97f239", size = 488360, upload-time = "2025-10-17T11:29:25.724Z" }, + { url = "https://files.pythonhosted.org/packages/f0/77/38124f5d02ac4131f0dfbcfd1a19a0fac305fa2c005bc4f9f0736914a1a4/jiter-0.11.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ae5ef1d48aec7e01ee8420155d901bb1d192998fa811a65ebb82c043ee186711", size = 376884, upload-time = "2025-10-17T11:29:27.056Z" }, + { url = "https://files.pythonhosted.org/packages/7b/43/59fdc2f6267959b71dd23ce0bd8d4aeaf55566aa435a5d00f53d53c7eb24/jiter-0.11.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb68e7bf65c990531ad8715e57d50195daf7c8e6f1509e617b4e692af1108939", size = 358827, upload-time = "2025-10-17T11:29:28.698Z" }, + { url = "https://files.pythonhosted.org/packages/7d/d0/b3cc20ff5340775ea3bbaa0d665518eddecd4266ba7244c9cb480c0c82ec/jiter-0.11.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:43b30c8154ded5845fa454ef954ee67bfccce629b2dea7d01f795b42bc2bda54", size = 385171, upload-time = "2025-10-17T11:29:30.078Z" }, + { url = "https://files.pythonhosted.org/packages/d2/bc/94dd1f3a61f4dc236f787a097360ec061ceeebebf4ea120b924d91391b10/jiter-0.11.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:586cafbd9dd1f3ce6a22b4a085eaa6be578e47ba9b18e198d4333e598a91db2d", size = 518359, upload-time = "2025-10-17T11:29:31.464Z" }, + { url = "https://files.pythonhosted.org/packages/7e/8c/12ee132bd67e25c75f542c227f5762491b9a316b0dad8e929c95076f773c/jiter-0.11.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:677cc2517d437a83bb30019fd4cf7cad74b465914c56ecac3440d597ac135250", size = 509205, upload-time = "2025-10-17T11:29:32.895Z" }, + { url = "https://files.pythonhosted.org/packages/39/d5/9de848928ce341d463c7e7273fce90ea6d0ea4343cd761f451860fa16b59/jiter-0.11.1-cp312-cp312-win32.whl", hash = "sha256:fa992af648fcee2b850a3286a35f62bbbaeddbb6dbda19a00d8fbc846a947b6e", size = 205448, upload-time = "2025-10-17T11:29:34.217Z" }, + { url = "https://files.pythonhosted.org/packages/ee/b0/8002d78637e05009f5e3fb5288f9d57d65715c33b5d6aa20fd57670feef5/jiter-0.11.1-cp312-cp312-win_amd64.whl", hash = "sha256:88b5cae9fa51efeb3d4bd4e52bfd4c85ccc9cac44282e2a9640893a042ba4d87", size = 204285, upload-time = "2025-10-17T11:29:35.446Z" }, + { url = "https://files.pythonhosted.org/packages/9f/a2/bb24d5587e4dff17ff796716542f663deee337358006a80c8af43ddc11e5/jiter-0.11.1-cp312-cp312-win_arm64.whl", hash = "sha256:9a6cae1ab335551917f882f2c3c1efe7617b71b4c02381e4382a8fc80a02588c", size = 188712, upload-time = "2025-10-17T11:29:37.027Z" }, + { url = "https://files.pythonhosted.org/packages/a6/bc/950dd7f170c6394b6fdd73f989d9e729bd98907bcc4430ef080a72d06b77/jiter-0.11.1-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:0d4d6993edc83cf75e8c6828a8d6ce40a09ee87e38c7bfba6924f39e1337e21d", size = 302626, upload-time = "2025-10-17T11:31:09.645Z" }, + { url = "https://files.pythonhosted.org/packages/3a/65/43d7971ca82ee100b7b9b520573eeef7eabc0a45d490168ebb9a9b5bb8b2/jiter-0.11.1-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:f78d151c83a87a6cf5461d5ee55bc730dd9ae227377ac6f115b922989b95f838", size = 297034, upload-time = "2025-10-17T11:31:10.975Z" }, + { url = "https://files.pythonhosted.org/packages/19/4c/000e1e0c0c67e96557a279f8969487ea2732d6c7311698819f977abae837/jiter-0.11.1-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9022974781155cd5521d5cb10997a03ee5e31e8454c9d999dcdccd253f2353f", size = 337328, upload-time = "2025-10-17T11:31:12.399Z" }, + { url = "https://files.pythonhosted.org/packages/d9/71/71408b02c6133153336d29fa3ba53000f1e1a3f78bb2fc2d1a1865d2e743/jiter-0.11.1-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18c77aaa9117510d5bdc6a946baf21b1f0cfa58ef04d31c8d016f206f2118960", size = 343697, upload-time = "2025-10-17T11:31:13.773Z" }, ] [[package]] @@ -969,11 +978,11 @@ wheels = [ [[package]] name = "json-repair" -version = "0.52.0" +version = "0.52.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f3/63/2c3c3c8cc1c28a0a20a9ab0eff5439c989ce3cc5956d8a4c7cf1eae0a06e/json_repair-0.52.0.tar.gz", hash = "sha256:0eee59cb3145b462b0734d4cf3246b797686caa669d52eee8dd30e09ea6d7876", size = 35384, upload-time = "2025-10-05T17:18:12.387Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/93/5220c447b9ce20ed14ab33bae9a29772be895a8949bb723eaa30cc42a4e1/json_repair-0.52.2.tar.gz", hash = "sha256:1c83e1811d7e57092ad531b333f083166bdf398b042c95f3cd62b30d74dc7ecd", size = 35584, upload-time = "2025-10-20T07:24:20.221Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/7f/3a4e456da9a0f9ac54d9842ed51e96960826a98456f0826a9b3e808713c4/json_repair-0.52.0-py3-none-any.whl", hash = "sha256:c783069906a456f62e2a553fbef32a420a4745ff943e2014411728edcc7bf60a", size = 26350, upload-time = "2025-10-05T17:18:10.859Z" }, + { url = "https://files.pythonhosted.org/packages/87/20/1935a6082988efea16432cecfdb757111122c32a07acaa595ccd78a55c47/json_repair-0.52.2-py3-none-any.whl", hash = "sha256:c7bb514d3f59d49364653717233eb4466bda0f4fdd511b4dc268aa877d406c81", size = 26512, upload-time = "2025-10-20T07:24:18.893Z" }, ] [[package]] @@ -1097,7 +1106,7 @@ wheels = [ [[package]] name = "langsmith" -version = "0.4.34" +version = "0.4.37" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx" }, @@ -1108,9 +1117,9 @@ dependencies = [ { name = "requests-toolbelt" }, { name = "zstandard" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e2/5d/38887a18b68aa7acbac040c1fad2f2217c55d3eef7784d0412261fe37513/langsmith-0.4.34.tar.gz", hash = "sha256:5b90c0b49ab03f78331005df1591abd86b41afceda6ac7144ad7d23693c62f31", size = 964392, upload-time = "2025-10-09T23:34:26.359Z" } +sdist = { url = "https://files.pythonhosted.org/packages/09/51/58d561dd40ec564509724f0a6a7148aa8090143208ef5d06b73b7fc90d31/langsmith-0.4.37.tar.gz", hash = "sha256:d9a0eb6dd93f89843ac982c9f92be93cf2bcabbe19957f362c547766c7366c71", size = 959089, upload-time = "2025-10-15T22:33:59.465Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8e/a4/db5903757d710c4c401e7a87f6ba53a8242c580e8c1df5869b7acb949b2d/langsmith-0.4.34-py3-none-any.whl", hash = "sha256:3b83b2544f99bb8f6fca2681ee80fe6a44b0578c29e809e5a4e72fdee4db9146", size = 386981, upload-time = "2025-10-09T23:34:24.386Z" }, + { url = "https://files.pythonhosted.org/packages/14/e8/edff4de49cf364eb9ee88d13da0a555844df32438413bf53d90d507b97cd/langsmith-0.4.37-py3-none-any.whl", hash = "sha256:e34a94ce7277646299e4703a0f6e2d2c43647a28e8b800bb7ef82fd87a0ec766", size = 396111, upload-time = "2025-10-15T22:33:57.392Z" }, ] [[package]] @@ -1124,7 +1133,7 @@ wheels = [ [[package]] name = "litellm" -version = "1.78.0" +version = "1.78.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp" }, @@ -1140,9 +1149,9 @@ dependencies = [ { name = "tiktoken" }, { name = "tokenizers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fd/3e/1a96a3caeeb6092d85e70904e2caa98598abb7179cefe734e2fbffac6978/litellm-1.78.0.tar.gz", hash = "sha256:020e40e0d6e16009bb3a6b156d4c1d98cb5c33704aa340fdf9ffd014bfd31f3b", size = 10684595, upload-time = "2025-10-11T19:28:27.369Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/5c/4d893ab43dd2fb23d3dae951c551bd529ab2e50c0f195e6b1bcfd4f41577/litellm-1.78.5.tar.gz", hash = "sha256:1f90a712c3e136e37bce98b3b839e40cd644ead8d90ce07257c7c302a58a4cd5", size = 10818833, upload-time = "2025-10-18T22:24:39.032Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fc/fb/38a48efe3e05a8e9a9765b991740282e0358a83fb896ec00d70bf1448791/litellm-1.78.0-py3-none-any.whl", hash = "sha256:a9d6deee882de8df38ca24beb930689f49209340137ff8a3dcab0c5fc4a0513d", size = 9677983, upload-time = "2025-10-11T19:28:23.242Z" }, + { url = "https://files.pythonhosted.org/packages/e6/f6/6aeedf8c6e75bfca08b9c73385186016446e8286803b381fcb9cac9c1594/litellm-1.78.5-py3-none-any.whl", hash = "sha256:aa716e9f2dfec406f1fb33831f3e49bc8bc6df73aa736aae21790516b7bb7832", size = 9827414, upload-time = "2025-10-18T22:24:35.398Z" }, ] [[package]] @@ -1387,21 +1396,21 @@ wheels = [ [[package]] name = "numpy" -version = "2.3.3" +version = "2.3.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d0/19/95b3d357407220ed24c139018d2518fab0a61a948e68286a25f1a4d049ff/numpy-2.3.3.tar.gz", hash = "sha256:ddc7c39727ba62b80dfdbedf400d1c10ddfa8eefbd7ec8dcb118be8b56d31029", size = 20576648, upload-time = "2025-09-09T16:54:12.543Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/f4/098d2270d52b41f1bd7db9fc288aaa0400cb48c2a3e2af6fa365d9720947/numpy-2.3.4.tar.gz", hash = "sha256:a7d018bfedb375a8d979ac758b120ba846a7fe764911a64465fd87b8729f4a6a", size = 20582187, upload-time = "2025-10-15T16:18:11.77Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/51/5d/bb7fc075b762c96329147799e1bcc9176ab07ca6375ea976c475482ad5b3/numpy-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cfdd09f9c84a1a934cde1eec2267f0a43a7cd44b2cca4ff95b7c0d14d144b0bf", size = 20957014, upload-time = "2025-09-09T15:56:29.966Z" }, - { url = "https://files.pythonhosted.org/packages/6b/0e/c6211bb92af26517acd52125a237a92afe9c3124c6a68d3b9f81b62a0568/numpy-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb32e3cf0f762aee47ad1ddc6672988f7f27045b0783c887190545baba73aa25", size = 14185220, upload-time = "2025-09-09T15:56:32.175Z" }, - { url = "https://files.pythonhosted.org/packages/22/f2/07bb754eb2ede9073f4054f7c0286b0d9d2e23982e090a80d478b26d35ca/numpy-2.3.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:396b254daeb0a57b1fe0ecb5e3cff6fa79a380fa97c8f7781a6d08cd429418fe", size = 5113918, upload-time = "2025-09-09T15:56:34.175Z" }, - { url = "https://files.pythonhosted.org/packages/81/0a/afa51697e9fb74642f231ea36aca80fa17c8fb89f7a82abd5174023c3960/numpy-2.3.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:067e3d7159a5d8f8a0b46ee11148fc35ca9b21f61e3c49fbd0a027450e65a33b", size = 6647922, upload-time = "2025-09-09T15:56:36.149Z" }, - { url = "https://files.pythonhosted.org/packages/5d/f5/122d9cdb3f51c520d150fef6e87df9279e33d19a9611a87c0d2cf78a89f4/numpy-2.3.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c02d0629d25d426585fb2e45a66154081b9fa677bc92a881ff1d216bc9919a8", size = 14281991, upload-time = "2025-09-09T15:56:40.548Z" }, - { url = "https://files.pythonhosted.org/packages/51/64/7de3c91e821a2debf77c92962ea3fe6ac2bc45d0778c1cbe15d4fce2fd94/numpy-2.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9192da52b9745f7f0766531dcfa978b7763916f158bb63bdb8a1eca0068ab20", size = 16641643, upload-time = "2025-09-09T15:56:43.343Z" }, - { url = "https://files.pythonhosted.org/packages/30/e4/961a5fa681502cd0d68907818b69f67542695b74e3ceaa513918103b7e80/numpy-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cd7de500a5b66319db419dc3c345244404a164beae0d0937283b907d8152e6ea", size = 16056787, upload-time = "2025-09-09T15:56:46.141Z" }, - { url = "https://files.pythonhosted.org/packages/99/26/92c912b966e47fbbdf2ad556cb17e3a3088e2e1292b9833be1dfa5361a1a/numpy-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:93d4962d8f82af58f0b2eb85daaf1b3ca23fe0a85d0be8f1f2b7bb46034e56d7", size = 18579598, upload-time = "2025-09-09T15:56:49.844Z" }, - { url = "https://files.pythonhosted.org/packages/17/b6/fc8f82cb3520768718834f310c37d96380d9dc61bfdaf05fe5c0b7653e01/numpy-2.3.3-cp312-cp312-win32.whl", hash = "sha256:5534ed6b92f9b7dca6c0a19d6df12d41c68b991cef051d108f6dbff3babc4ebf", size = 6320800, upload-time = "2025-09-09T15:56:52.499Z" }, - { url = "https://files.pythonhosted.org/packages/32/ee/de999f2625b80d043d6d2d628c07d0d5555a677a3cf78fdf868d409b8766/numpy-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:497d7cad08e7092dba36e3d296fe4c97708c93daf26643a1ae4b03f6294d30eb", size = 12786615, upload-time = "2025-09-09T15:56:54.422Z" }, - { url = "https://files.pythonhosted.org/packages/49/6e/b479032f8a43559c383acb20816644f5f91c88f633d9271ee84f3b3a996c/numpy-2.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:ca0309a18d4dfea6fc6262a66d06c26cfe4640c3926ceec90e57791a82b6eee5", size = 10195936, upload-time = "2025-09-09T15:56:56.541Z" }, + { url = "https://files.pythonhosted.org/packages/96/7a/02420400b736f84317e759291b8edaeee9dc921f72b045475a9cbdb26b17/numpy-2.3.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ef1b5a3e808bc40827b5fa2c8196151a4c5abe110e1726949d7abddfe5c7ae11", size = 20957727, upload-time = "2025-10-15T16:15:44.9Z" }, + { url = "https://files.pythonhosted.org/packages/18/90/a014805d627aa5750f6f0e878172afb6454552da929144b3c07fcae1bb13/numpy-2.3.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c2f91f496a87235c6aaf6d3f3d89b17dba64996abadccb289f48456cff931ca9", size = 14187262, upload-time = "2025-10-15T16:15:47.761Z" }, + { url = "https://files.pythonhosted.org/packages/c7/e4/0a94b09abe89e500dc748e7515f21a13e30c5c3fe3396e6d4ac108c25fca/numpy-2.3.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:f77e5b3d3da652b474cc80a14084927a5e86a5eccf54ca8ca5cbd697bf7f2667", size = 5115992, upload-time = "2025-10-15T16:15:50.144Z" }, + { url = "https://files.pythonhosted.org/packages/88/dd/db77c75b055c6157cbd4f9c92c4458daef0dd9cbe6d8d2fe7f803cb64c37/numpy-2.3.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:8ab1c5f5ee40d6e01cbe96de5863e39b215a4d24e7d007cad56c7184fdf4aeef", size = 6648672, upload-time = "2025-10-15T16:15:52.442Z" }, + { url = "https://files.pythonhosted.org/packages/e1/e6/e31b0d713719610e406c0ea3ae0d90760465b086da8783e2fd835ad59027/numpy-2.3.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77b84453f3adcb994ddbd0d1c5d11db2d6bda1a2b7fd5ac5bd4649d6f5dc682e", size = 14284156, upload-time = "2025-10-15T16:15:54.351Z" }, + { url = "https://files.pythonhosted.org/packages/f9/58/30a85127bfee6f108282107caf8e06a1f0cc997cb6b52cdee699276fcce4/numpy-2.3.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4121c5beb58a7f9e6dfdee612cb24f4df5cd4db6e8261d7f4d7450a997a65d6a", size = 16641271, upload-time = "2025-10-15T16:15:56.67Z" }, + { url = "https://files.pythonhosted.org/packages/06/f2/2e06a0f2adf23e3ae29283ad96959267938d0efd20a2e25353b70065bfec/numpy-2.3.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:65611ecbb00ac9846efe04db15cbe6186f562f6bb7e5e05f077e53a599225d16", size = 16059531, upload-time = "2025-10-15T16:15:59.412Z" }, + { url = "https://files.pythonhosted.org/packages/b0/e7/b106253c7c0d5dc352b9c8fab91afd76a93950998167fa3e5afe4ef3a18f/numpy-2.3.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dabc42f9c6577bcc13001b8810d300fe814b4cfbe8a92c873f269484594f9786", size = 18578983, upload-time = "2025-10-15T16:16:01.804Z" }, + { url = "https://files.pythonhosted.org/packages/73/e3/04ecc41e71462276ee867ccbef26a4448638eadecf1bc56772c9ed6d0255/numpy-2.3.4-cp312-cp312-win32.whl", hash = "sha256:a49d797192a8d950ca59ee2d0337a4d804f713bb5c3c50e8db26d49666e351dc", size = 6291380, upload-time = "2025-10-15T16:16:03.938Z" }, + { url = "https://files.pythonhosted.org/packages/3d/a8/566578b10d8d0e9955b1b6cd5db4e9d4592dd0026a941ff7994cedda030a/numpy-2.3.4-cp312-cp312-win_amd64.whl", hash = "sha256:985f1e46358f06c2a09921e8921e2c98168ed4ae12ccd6e5e87a4f1857923f32", size = 12787999, upload-time = "2025-10-15T16:16:05.801Z" }, + { url = "https://files.pythonhosted.org/packages/58/22/9c903a957d0a8071b607f5b1bff0761d6e608b9a965945411f867d515db1/numpy-2.3.4-cp312-cp312-win_arm64.whl", hash = "sha256:4635239814149e06e2cb9db3dd584b2fa64316c96f10656983b8026a82e6e4db", size = 10197412, upload-time = "2025-10-15T16:16:07.854Z" }, ] [[package]] @@ -1508,10 +1517,10 @@ wheels = [ [[package]] name = "nvidia-nccl-cu12" -version = "2.27.3" +version = "2.27.5" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/5b/4e4fff7bad39adf89f735f2bc87248c81db71205b62bcc0d5ca5b606b3c3/nvidia_nccl_cu12-2.27.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:adf27ccf4238253e0b826bce3ff5fa532d65fc42322c8bfdfaf28024c0fbe039", size = 322364134, upload-time = "2025-06-03T21:58:04.013Z" }, + { url = "https://files.pythonhosted.org/packages/6e/89/f7a07dc961b60645dbbf42e80f2bc85ade7feb9a491b11a1e973aa00071f/nvidia_nccl_cu12-2.27.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ad730cf15cb5d25fe849c6e6ca9eb5b76db16a80f13f425ac68d8e2e55624457", size = 322348229, upload-time = "2025-06-26T04:11:28.385Z" }, ] [[package]] @@ -1522,6 +1531,14 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f6/74/86a07f1d0f42998ca31312f998bd3b9a7eff7f52378f4f270c8679c77fb9/nvidia_nvjitlink_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:81ff63371a7ebd6e6451970684f916be2eab07321b73c9d244dc2b4da7f73b88", size = 39254836, upload-time = "2025-03-07T01:49:55.661Z" }, ] +[[package]] +name = "nvidia-nvshmem-cu12" +version = "3.3.20" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/6c/99acb2f9eb85c29fc6f3a7ac4dccfd992e22666dd08a642b303311326a97/nvidia_nvshmem_cu12-3.3.20-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d00f26d3f9b2e3c3065be895e3059d6479ea5c638a3f38c9fec49b1b9dd7c1e5", size = 124657145, upload-time = "2025-08-04T20:25:19.995Z" }, +] + [[package]] name = "nvidia-nvtx-cu12" version = "12.8.90" @@ -1565,7 +1582,7 @@ wheels = [ [[package]] name = "openai" -version = "2.3.0" +version = "2.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -1577,39 +1594,39 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/de/90/8f26554d24d63ed4f94d33c24271559863223a67e624f4d2e65ba8e48dca/openai-2.3.0.tar.gz", hash = "sha256:8d213ee5aaf91737faea2d7fc1cd608657a5367a18966372a3756ceaabfbd812", size = 589616, upload-time = "2025-10-10T01:12:50.851Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/c7/e42bcd89dfd47fec8a30b9e20f93e512efdbfbb3391b05bbb79a2fb295fa/openai-2.6.0.tar.gz", hash = "sha256:f119faf7fc07d7e558c1e7c32c873e241439b01bd7480418234291ee8c8f4b9d", size = 592904, upload-time = "2025-10-20T17:17:24.588Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/5b/4be258ff072ed8ee15f6bfd8d5a1a4618aa4704b127c0c5959212ad177d6/openai-2.3.0-py3-none-any.whl", hash = "sha256:a7aa83be6f7b0ab2e4d4d7bcaf36e3d790874c0167380c5d0afd0ed99a86bd7b", size = 999768, upload-time = "2025-10-10T01:12:48.647Z" }, + { url = "https://files.pythonhosted.org/packages/c0/0a/58e9dcd34abe273eaeac3807a8483073767b5609d01bb78ea2f048e515a0/openai-2.6.0-py3-none-any.whl", hash = "sha256:f33fa12070fe347b5787a7861c8dd397786a4a17e1c3186e239338dac7e2e743", size = 1005403, upload-time = "2025-10-20T17:17:22.091Z" }, ] [[package]] name = "opentelemetry-api" -version = "1.37.0" +version = "1.38.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "importlib-metadata" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/63/04/05040d7ce33a907a2a02257e601992f0cdf11c73b33f13c4492bf6c3d6d5/opentelemetry_api-1.37.0.tar.gz", hash = "sha256:540735b120355bd5112738ea53621f8d5edb35ebcd6fe21ada3ab1c61d1cd9a7", size = 64923, upload-time = "2025-09-11T10:29:01.662Z" } +sdist = { url = "https://files.pythonhosted.org/packages/08/d8/0f354c375628e048bd0570645b310797299754730079853095bf000fba69/opentelemetry_api-1.38.0.tar.gz", hash = "sha256:f4c193b5e8acb0912b06ac5b16321908dd0843d75049c091487322284a3eea12", size = 65242, upload-time = "2025-10-16T08:35:50.25Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/91/48/28ed9e55dcf2f453128df738210a980e09f4e468a456fa3c763dbc8be70a/opentelemetry_api-1.37.0-py3-none-any.whl", hash = "sha256:accf2024d3e89faec14302213bc39550ec0f4095d1cf5ca688e1bfb1c8612f47", size = 65732, upload-time = "2025-09-11T10:28:41.826Z" }, + { url = "https://files.pythonhosted.org/packages/ae/a2/d86e01c28300bd41bab8f18afd613676e2bd63515417b77636fc1add426f/opentelemetry_api-1.38.0-py3-none-any.whl", hash = "sha256:2891b0197f47124454ab9f0cf58f3be33faca394457ac3e09daba13ff50aa582", size = 65947, upload-time = "2025-10-16T08:35:30.23Z" }, ] [[package]] name = "opentelemetry-exporter-otlp-proto-common" -version = "1.37.0" +version = "1.38.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-proto" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/dc/6c/10018cbcc1e6fff23aac67d7fd977c3d692dbe5f9ef9bb4db5c1268726cc/opentelemetry_exporter_otlp_proto_common-1.37.0.tar.gz", hash = "sha256:c87a1bdd9f41fdc408d9cc9367bb53f8d2602829659f2b90be9f9d79d0bfe62c", size = 20430, upload-time = "2025-09-11T10:29:03.605Z" } +sdist = { url = "https://files.pythonhosted.org/packages/19/83/dd4660f2956ff88ed071e9e0e36e830df14b8c5dc06722dbde1841accbe8/opentelemetry_exporter_otlp_proto_common-1.38.0.tar.gz", hash = "sha256:e333278afab4695aa8114eeb7bf4e44e65c6607d54968271a249c180b2cb605c", size = 20431, upload-time = "2025-10-16T08:35:53.285Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/08/13/b4ef09837409a777f3c0af2a5b4ba9b7af34872bc43609dda0c209e4060d/opentelemetry_exporter_otlp_proto_common-1.37.0-py3-none-any.whl", hash = "sha256:53038428449c559b0c564b8d718df3314da387109c4d36bd1b94c9a641b0292e", size = 18359, upload-time = "2025-09-11T10:28:44.939Z" }, + { url = "https://files.pythonhosted.org/packages/a7/9e/55a41c9601191e8cd8eb626b54ee6827b9c9d4a46d736f32abc80d8039fc/opentelemetry_exporter_otlp_proto_common-1.38.0-py3-none-any.whl", hash = "sha256:03cb76ab213300fe4f4c62b7d8f17d97fcfd21b89f0b5ce38ea156327ddda74a", size = 18359, upload-time = "2025-10-16T08:35:34.099Z" }, ] [[package]] name = "opentelemetry-exporter-otlp-proto-grpc" -version = "1.37.0" +version = "1.38.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "googleapis-common-protos" }, @@ -1620,48 +1637,48 @@ dependencies = [ { name = "opentelemetry-sdk" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d1/11/4ad0979d0bb13ae5a845214e97c8d42da43980034c30d6f72d8e0ebe580e/opentelemetry_exporter_otlp_proto_grpc-1.37.0.tar.gz", hash = "sha256:f55bcb9fc848ce05ad3dd954058bc7b126624d22c4d9e958da24d8537763bec5", size = 24465, upload-time = "2025-09-11T10:29:04.172Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/c0/43222f5b97dc10812bc4f0abc5dc7cd0a2525a91b5151d26c9e2e958f52e/opentelemetry_exporter_otlp_proto_grpc-1.38.0.tar.gz", hash = "sha256:2473935e9eac71f401de6101d37d6f3f0f1831db92b953c7dcc912536158ebd6", size = 24676, upload-time = "2025-10-16T08:35:53.83Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/39/17/46630b74751031a658706bef23ac99cdc2953cd3b2d28ec90590a0766b3e/opentelemetry_exporter_otlp_proto_grpc-1.37.0-py3-none-any.whl", hash = "sha256:aee5104835bf7993b7ddaaf380b6467472abaedb1f1dbfcc54a52a7d781a3890", size = 19305, upload-time = "2025-09-11T10:28:45.776Z" }, + { url = "https://files.pythonhosted.org/packages/28/f0/bd831afbdba74ca2ce3982142a2fad707f8c487e8a3b6fef01f1d5945d1b/opentelemetry_exporter_otlp_proto_grpc-1.38.0-py3-none-any.whl", hash = "sha256:7c49fd9b4bd0dbe9ba13d91f764c2d20b0025649a6e4ac35792fb8d84d764bc7", size = 19695, upload-time = "2025-10-16T08:35:35.053Z" }, ] [[package]] name = "opentelemetry-proto" -version = "1.37.0" +version = "1.38.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/dd/ea/a75f36b463a36f3c5a10c0b5292c58b31dbdde74f6f905d3d0ab2313987b/opentelemetry_proto-1.37.0.tar.gz", hash = "sha256:30f5c494faf66f77faeaefa35ed4443c5edb3b0aa46dad073ed7210e1a789538", size = 46151, upload-time = "2025-09-11T10:29:11.04Z" } +sdist = { url = "https://files.pythonhosted.org/packages/51/14/f0c4f0f6371b9cb7f9fa9ee8918bfd59ac7040c7791f1e6da32a1839780d/opentelemetry_proto-1.38.0.tar.gz", hash = "sha256:88b161e89d9d372ce723da289b7da74c3a8354a8e5359992be813942969ed468", size = 46152, upload-time = "2025-10-16T08:36:01.612Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c4/25/f89ea66c59bd7687e218361826c969443c4fa15dfe89733f3bf1e2a9e971/opentelemetry_proto-1.37.0-py3-none-any.whl", hash = "sha256:8ed8c066ae8828bbf0c39229979bdf583a126981142378a9cbe9d6fd5701c6e2", size = 72534, upload-time = "2025-09-11T10:28:56.831Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6a/82b68b14efca5150b2632f3692d627afa76b77378c4999f2648979409528/opentelemetry_proto-1.38.0-py3-none-any.whl", hash = "sha256:b6ebe54d3217c42e45462e2a1ae28c3e2bf2ec5a5645236a490f55f45f1a0a18", size = 72535, upload-time = "2025-10-16T08:35:45.749Z" }, ] [[package]] name = "opentelemetry-sdk" -version = "1.37.0" +version = "1.38.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-semantic-conventions" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f4/62/2e0ca80d7fe94f0b193135375da92c640d15fe81f636658d2acf373086bc/opentelemetry_sdk-1.37.0.tar.gz", hash = "sha256:cc8e089c10953ded765b5ab5669b198bbe0af1b3f89f1007d19acd32dc46dda5", size = 170404, upload-time = "2025-09-11T10:29:11.779Z" } +sdist = { url = "https://files.pythonhosted.org/packages/85/cb/f0eee1445161faf4c9af3ba7b848cc22a50a3d3e2515051ad8628c35ff80/opentelemetry_sdk-1.38.0.tar.gz", hash = "sha256:93df5d4d871ed09cb4272305be4d996236eedb232253e3ab864c8620f051cebe", size = 171942, upload-time = "2025-10-16T08:36:02.257Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/62/9f4ad6a54126fb00f7ed4bb5034964c6e4f00fcd5a905e115bd22707e20d/opentelemetry_sdk-1.37.0-py3-none-any.whl", hash = "sha256:8f3c3c22063e52475c5dbced7209495c2c16723d016d39287dfc215d1771257c", size = 131941, upload-time = "2025-09-11T10:28:57.83Z" }, + { url = "https://files.pythonhosted.org/packages/2f/2e/e93777a95d7d9c40d270a371392b6d6f1ff170c2a3cb32d6176741b5b723/opentelemetry_sdk-1.38.0-py3-none-any.whl", hash = "sha256:1c66af6564ecc1553d72d811a01df063ff097cdc82ce188da9951f93b8d10f6b", size = 132349, upload-time = "2025-10-16T08:35:46.995Z" }, ] [[package]] name = "opentelemetry-semantic-conventions" -version = "0.58b0" +version = "0.59b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/aa/1b/90701d91e6300d9f2fb352153fb1721ed99ed1f6ea14fa992c756016e63a/opentelemetry_semantic_conventions-0.58b0.tar.gz", hash = "sha256:6bd46f51264279c433755767bb44ad00f1c9e2367e1b42af563372c5a6fa0c25", size = 129867, upload-time = "2025-09-11T10:29:12.597Z" } +sdist = { url = "https://files.pythonhosted.org/packages/40/bc/8b9ad3802cd8ac6583a4eb7de7e5d7db004e89cb7efe7008f9c8a537ee75/opentelemetry_semantic_conventions-0.59b0.tar.gz", hash = "sha256:7a6db3f30d70202d5bf9fa4b69bc866ca6a30437287de6c510fb594878aed6b0", size = 129861, upload-time = "2025-10-16T08:36:03.346Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/07/90/68152b7465f50285d3ce2481b3aec2f82822e3f52e5152eeeaf516bab841/opentelemetry_semantic_conventions-0.58b0-py3-none-any.whl", hash = "sha256:5564905ab1458b96684db1340232729fce3b5375a06e140e8904c78e4f815b28", size = 207954, upload-time = "2025-09-11T10:28:59.218Z" }, + { url = "https://files.pythonhosted.org/packages/24/7d/c88d7b15ba8fe5c6b8f93be50fc11795e9fc05386c44afaf6b76fe191f9b/opentelemetry_semantic_conventions-0.59b0-py3-none-any.whl", hash = "sha256:35d3b8833ef97d614136e253c1da9342b4c3c083bbaf29ce31d572a1c3825eed", size = 207954, upload-time = "2025-10-16T08:35:48.054Z" }, ] [[package]] @@ -1786,7 +1803,7 @@ wheels = [ [[package]] name = "posthog" -version = "6.7.6" +version = "6.7.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "backoff" }, @@ -1796,9 +1813,9 @@ dependencies = [ { name = "six" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e2/ce/11d6fa30ab517018796e1d675498992da585479e7079770ec8fa99a61561/posthog-6.7.6.tar.gz", hash = "sha256:ee5c5ad04b857d96d9b7a4f715e23916a2f206bfcf25e5a9d328a3d27664b0d3", size = 119129, upload-time = "2025-09-22T18:11:12.365Z" } +sdist = { url = "https://files.pythonhosted.org/packages/40/b1/a23c9d092de37e9ce39e27166f38f81b0bd7704022fe23f90734eb4b7ad4/posthog-6.7.8.tar.gz", hash = "sha256:999e65134571827061332f1f311df9b24730b386c6eabe0057bf768e514d87a8", size = 119085, upload-time = "2025-10-16T14:46:53.126Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/de/84/586422d8861b5391c8414360b10f603c0b7859bb09ad688e64430ed0df7b/posthog-6.7.6-py3-none-any.whl", hash = "sha256:b09a7e65a042ec416c28874b397d3accae412a80a8b0ef3fa686fbffc99e4d4b", size = 137348, upload-time = "2025-09-22T18:11:10.807Z" }, + { url = "https://files.pythonhosted.org/packages/1e/ce/5e5ede2f0b24db113544f9f7ce08d395a4107cbc66d77b8d05d9eaeaeada/posthog-6.7.8-py3-none-any.whl", hash = "sha256:842ccb518f925425f714bae29e4ac36a059a8948c45f6ed155543ca7386d554b", size = 137299, upload-time = "2025-10-16T14:46:51.547Z" }, ] [[package]] @@ -1855,16 +1872,17 @@ wheels = [ [[package]] name = "protobuf" -version = "6.32.1" +version = "6.33.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fa/a4/cc17347aa2897568beece2e674674359f911d6fe21b0b8d6268cd42727ac/protobuf-6.32.1.tar.gz", hash = "sha256:ee2469e4a021474ab9baafea6cd070e5bf27c7d29433504ddea1a4ee5850f68d", size = 440635, upload-time = "2025-09-11T21:38:42.935Z" } +sdist = { url = "https://files.pythonhosted.org/packages/19/ff/64a6c8f420818bb873713988ca5492cba3a7946be57e027ac63495157d97/protobuf-6.33.0.tar.gz", hash = "sha256:140303d5c8d2037730c548f8c7b93b20bb1dc301be280c378b82b8894589c954", size = 443463, upload-time = "2025-10-15T20:39:52.159Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/98/645183ea03ab3995d29086b8bf4f7562ebd3d10c9a4b14ee3f20d47cfe50/protobuf-6.32.1-cp310-abi3-win32.whl", hash = "sha256:a8a32a84bc9f2aad712041b8b366190f71dde248926da517bde9e832e4412085", size = 424411, upload-time = "2025-09-11T21:38:27.427Z" }, - { url = "https://files.pythonhosted.org/packages/8c/f3/6f58f841f6ebafe076cebeae33fc336e900619d34b1c93e4b5c97a81fdfa/protobuf-6.32.1-cp310-abi3-win_amd64.whl", hash = "sha256:b00a7d8c25fa471f16bc8153d0e53d6c9e827f0953f3c09aaa4331c718cae5e1", size = 435738, upload-time = "2025-09-11T21:38:30.959Z" }, - { url = "https://files.pythonhosted.org/packages/10/56/a8a3f4e7190837139e68c7002ec749190a163af3e330f65d90309145a210/protobuf-6.32.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:d8c7e6eb619ffdf105ee4ab76af5a68b60a9d0f66da3ea12d1640e6d8dab7281", size = 426454, upload-time = "2025-09-11T21:38:34.076Z" }, - { url = "https://files.pythonhosted.org/packages/3f/be/8dd0a927c559b37d7a6c8ab79034fd167dcc1f851595f2e641ad62be8643/protobuf-6.32.1-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:2f5b80a49e1eb7b86d85fcd23fe92df154b9730a725c3b38c4e43b9d77018bf4", size = 322874, upload-time = "2025-09-11T21:38:35.509Z" }, - { url = "https://files.pythonhosted.org/packages/5c/f6/88d77011b605ef979aace37b7703e4eefad066f7e84d935e5a696515c2dd/protobuf-6.32.1-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:b1864818300c297265c83a4982fd3169f97122c299f56a56e2445c3698d34710", size = 322013, upload-time = "2025-09-11T21:38:37.017Z" }, - { url = "https://files.pythonhosted.org/packages/97/b7/15cc7d93443d6c6a84626ae3258a91f4c6ac8c0edd5df35ea7658f71b79c/protobuf-6.32.1-py3-none-any.whl", hash = "sha256:2601b779fc7d32a866c6b4404f9d42a3f67c5b9f3f15b4db3cccabe06b95c346", size = 169289, upload-time = "2025-09-11T21:38:41.234Z" }, + { url = "https://files.pythonhosted.org/packages/7e/ee/52b3fa8feb6db4a833dfea4943e175ce645144532e8a90f72571ad85df4e/protobuf-6.33.0-cp310-abi3-win32.whl", hash = "sha256:d6101ded078042a8f17959eccd9236fb7a9ca20d3b0098bbcb91533a5680d035", size = 425593, upload-time = "2025-10-15T20:39:40.29Z" }, + { url = "https://files.pythonhosted.org/packages/7b/c6/7a465f1825872c55e0341ff4a80198743f73b69ce5d43ab18043699d1d81/protobuf-6.33.0-cp310-abi3-win_amd64.whl", hash = "sha256:9a031d10f703f03768f2743a1c403af050b6ae1f3480e9c140f39c45f81b13ee", size = 436882, upload-time = "2025-10-15T20:39:42.841Z" }, + { url = "https://files.pythonhosted.org/packages/e1/a9/b6eee662a6951b9c3640e8e452ab3e09f117d99fc10baa32d1581a0d4099/protobuf-6.33.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:905b07a65f1a4b72412314082c7dbfae91a9e8b68a0cc1577515f8df58ecf455", size = 427521, upload-time = "2025-10-15T20:39:43.803Z" }, + { url = "https://files.pythonhosted.org/packages/10/35/16d31e0f92c6d2f0e77c2a3ba93185130ea13053dd16200a57434c882f2b/protobuf-6.33.0-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:e0697ece353e6239b90ee43a9231318302ad8353c70e6e45499fa52396debf90", size = 324445, upload-time = "2025-10-15T20:39:44.932Z" }, + { url = "https://files.pythonhosted.org/packages/e6/eb/2a981a13e35cda8b75b5585aaffae2eb904f8f351bdd3870769692acbd8a/protobuf-6.33.0-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:e0a1715e4f27355afd9570f3ea369735afc853a6c3951a6afe1f80d8569ad298", size = 339159, upload-time = "2025-10-15T20:39:46.186Z" }, + { url = "https://files.pythonhosted.org/packages/21/51/0b1cbad62074439b867b4e04cc09b93f6699d78fd191bed2bbb44562e077/protobuf-6.33.0-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:35be49fd3f4fefa4e6e2aacc35e8b837d6703c37a2168a55ac21e9b1bc7559ef", size = 323172, upload-time = "2025-10-15T20:39:47.465Z" }, + { url = "https://files.pythonhosted.org/packages/07/d1/0a28c21707807c6aacd5dc9c3704b2aa1effbf37adebd8caeaf68b17a636/protobuf-6.33.0-py3-none-any.whl", hash = "sha256:25c9e1963c6734448ea2d308cfa610e692b801304ba0908d7bfa564ac5132995", size = 170477, upload-time = "2025-10-15T20:39:51.311Z" }, ] [[package]] @@ -1917,7 +1935,7 @@ wheels = [ [[package]] name = "pydantic" -version = "2.12.0" +version = "2.12.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, @@ -1925,38 +1943,38 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c3/da/b8a7ee04378a53f6fefefc0c5e05570a3ebfdfa0523a878bcd3b475683ee/pydantic-2.12.0.tar.gz", hash = "sha256:c1a077e6270dbfb37bfd8b498b3981e2bb18f68103720e51fa6c306a5a9af563", size = 814760, upload-time = "2025-10-07T15:58:03.467Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/1e/4f0a3233767010308f2fd6bd0814597e3f63f1dc98304a9112b8759df4ff/pydantic-2.12.3.tar.gz", hash = "sha256:1da1c82b0fc140bb0103bc1441ffe062154c8d38491189751ee00fd8ca65ce74", size = 819383, upload-time = "2025-10-17T15:04:21.222Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/9d/d5c855424e2e5b6b626fbc6ec514d8e655a600377ce283008b115abb7445/pydantic-2.12.0-py3-none-any.whl", hash = "sha256:f6a1da352d42790537e95e83a8bdfb91c7efbae63ffd0b86fa823899e807116f", size = 459730, upload-time = "2025-10-07T15:58:01.576Z" }, + { url = "https://files.pythonhosted.org/packages/a1/6b/83661fa77dcefa195ad5f8cd9af3d1a7450fd57cc883ad04d65446ac2029/pydantic-2.12.3-py3-none-any.whl", hash = "sha256:6986454a854bc3bc6e5443e1369e06a3a456af9d339eda45510f517d9ea5c6bf", size = 462431, upload-time = "2025-10-17T15:04:19.346Z" }, ] [[package]] name = "pydantic-core" -version = "2.41.1" +version = "2.41.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7d/14/12b4a0d2b0b10d8e1d9a24ad94e7bbb43335eaf29c0c4e57860e8a30734a/pydantic_core-2.41.1.tar.gz", hash = "sha256:1ad375859a6d8c356b7704ec0f547a58e82ee80bb41baa811ad710e124bc8f2f", size = 454870, upload-time = "2025-10-07T10:50:45.974Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ee/bc/5f520319ee1c9e25010412fac4154a72e0a40d0a19eb00281b1f200c0947/pydantic_core-2.41.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:db2f82c0ccbce8f021ad304ce35cbe02aa2f95f215cac388eed542b03b4d5eb4", size = 2099300, upload-time = "2025-10-06T21:10:30.463Z" }, - { url = "https://files.pythonhosted.org/packages/31/14/010cd64c5c3814fb6064786837ec12604be0dd46df3327cf8474e38abbbd/pydantic_core-2.41.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:47694a31c710ced9205d5f1e7e8af3ca57cbb8a503d98cb9e33e27c97a501601", size = 1910179, upload-time = "2025-10-06T21:10:31.782Z" }, - { url = "https://files.pythonhosted.org/packages/8e/2e/23fc2a8a93efad52df302fdade0a60f471ecc0c7aac889801ac24b4c07d6/pydantic_core-2.41.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e9decce94daf47baf9e9d392f5f2557e783085f7c5e522011545d9d6858e00", size = 1957225, upload-time = "2025-10-06T21:10:33.11Z" }, - { url = "https://files.pythonhosted.org/packages/b9/b6/6db08b2725b2432b9390844852e11d320281e5cea8a859c52c68001975fa/pydantic_core-2.41.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ab0adafdf2b89c8b84f847780a119437a0931eca469f7b44d356f2b426dd9741", size = 2053315, upload-time = "2025-10-06T21:10:34.87Z" }, - { url = "https://files.pythonhosted.org/packages/61/d9/4de44600f2d4514b44f3f3aeeda2e14931214b6b5bf52479339e801ce748/pydantic_core-2.41.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5da98cc81873f39fd56882e1569c4677940fbc12bce6213fad1ead784192d7c8", size = 2224298, upload-time = "2025-10-06T21:10:36.233Z" }, - { url = "https://files.pythonhosted.org/packages/7a/ae/dbe51187a7f35fc21b283c5250571a94e36373eb557c1cba9f29a9806dcf/pydantic_core-2.41.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:209910e88afb01fd0fd403947b809ba8dba0e08a095e1f703294fda0a8fdca51", size = 2351797, upload-time = "2025-10-06T21:10:37.601Z" }, - { url = "https://files.pythonhosted.org/packages/b5/a7/975585147457c2e9fb951c7c8dab56deeb6aa313f3aa72c2fc0df3f74a49/pydantic_core-2.41.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:365109d1165d78d98e33c5bfd815a9b5d7d070f578caefaabcc5771825b4ecb5", size = 2074921, upload-time = "2025-10-06T21:10:38.927Z" }, - { url = "https://files.pythonhosted.org/packages/62/37/ea94d1d0c01dec1b7d236c7cec9103baab0021f42500975de3d42522104b/pydantic_core-2.41.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:706abf21e60a2857acdb09502bc853ee5bce732955e7b723b10311114f033115", size = 2187767, upload-time = "2025-10-06T21:10:40.651Z" }, - { url = "https://files.pythonhosted.org/packages/d3/fe/694cf9fdd3a777a618c3afd210dba7b414cb8a72b1bd29b199c2e5765fee/pydantic_core-2.41.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bf0bd5417acf7f6a7ec3b53f2109f587be176cb35f9cf016da87e6017437a72d", size = 2136062, upload-time = "2025-10-06T21:10:42.09Z" }, - { url = "https://files.pythonhosted.org/packages/0f/ae/174aeabd89916fbd2988cc37b81a59e1186e952afd2a7ed92018c22f31ca/pydantic_core-2.41.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:2e71b1c6ceb9c78424ae9f63a07292fb769fb890a4e7efca5554c47f33a60ea5", size = 2317819, upload-time = "2025-10-06T21:10:43.974Z" }, - { url = "https://files.pythonhosted.org/packages/65/e8/e9aecafaebf53fc456314f72886068725d6fba66f11b013532dc21259343/pydantic_core-2.41.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:80745b9770b4a38c25015b517451c817799bfb9d6499b0d13d8227ec941cb513", size = 2312267, upload-time = "2025-10-06T21:10:45.34Z" }, - { url = "https://files.pythonhosted.org/packages/35/2f/1c2e71d2a052f9bb2f2df5a6a05464a0eb800f9e8d9dd800202fe31219e1/pydantic_core-2.41.1-cp312-cp312-win32.whl", hash = "sha256:83b64d70520e7890453f1aa21d66fda44e7b35f1cfea95adf7b4289a51e2b479", size = 1990927, upload-time = "2025-10-06T21:10:46.738Z" }, - { url = "https://files.pythonhosted.org/packages/b1/78/562998301ff2588b9c6dcc5cb21f52fa919d6e1decc75a35055feb973594/pydantic_core-2.41.1-cp312-cp312-win_amd64.whl", hash = "sha256:377defd66ee2003748ee93c52bcef2d14fde48fe28a0b156f88c3dbf9bc49a50", size = 2034703, upload-time = "2025-10-06T21:10:48.524Z" }, - { url = "https://files.pythonhosted.org/packages/b2/53/d95699ce5a5cdb44bb470bd818b848b9beadf51459fd4ea06667e8ede862/pydantic_core-2.41.1-cp312-cp312-win_arm64.whl", hash = "sha256:c95caff279d49c1d6cdfe2996e6c2ad712571d3b9caaa209a404426c326c4bde", size = 1972719, upload-time = "2025-10-06T21:10:50.256Z" }, - { url = "https://files.pythonhosted.org/packages/2b/3e/a51c5f5d37b9288ba30683d6e96f10fa8f1defad1623ff09f1020973b577/pydantic_core-2.41.1-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:b04fa9ed049461a7398138c604b00550bc89e3e1151d84b81ad6dc93e39c4c06", size = 2115344, upload-time = "2025-10-07T10:50:02.466Z" }, - { url = "https://files.pythonhosted.org/packages/5a/bd/389504c9e0600ef4502cd5238396b527afe6ef8981a6a15cd1814fc7b434/pydantic_core-2.41.1-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:b3b7d9cfbfdc43c80a16638c6dc2768e3956e73031fca64e8e1a3ae744d1faeb", size = 1927994, upload-time = "2025-10-07T10:50:04.379Z" }, - { url = "https://files.pythonhosted.org/packages/ff/9c/5111c6b128861cb792a4c082677e90dac4f2e090bb2e2fe06aa5b2d39027/pydantic_core-2.41.1-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eec83fc6abef04c7f9bec616e2d76ee9a6a4ae2a359b10c21d0f680e24a247ca", size = 1959394, upload-time = "2025-10-07T10:50:06.335Z" }, - { url = "https://files.pythonhosted.org/packages/14/3f/cfec8b9a0c48ce5d64409ec5e1903cb0b7363da38f14b41de2fcb3712700/pydantic_core-2.41.1-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6771a2d9f83c4038dfad5970a3eef215940682b2175e32bcc817bdc639019b28", size = 2147365, upload-time = "2025-10-07T10:50:07.978Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/df/18/d0944e8eaaa3efd0a91b0f1fc537d3be55ad35091b6a87638211ba691964/pydantic_core-2.41.4.tar.gz", hash = "sha256:70e47929a9d4a1905a67e4b687d5946026390568a8e952b92824118063cee4d5", size = 457557, upload-time = "2025-10-14T10:23:47.909Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/81/d3b3e95929c4369d30b2a66a91db63c8ed0a98381ae55a45da2cd1cc1288/pydantic_core-2.41.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ab06d77e053d660a6faaf04894446df7b0a7e7aba70c2797465a0a1af00fc887", size = 2099043, upload-time = "2025-10-14T10:20:28.561Z" }, + { url = "https://files.pythonhosted.org/packages/58/da/46fdac49e6717e3a94fc9201403e08d9d61aa7a770fab6190b8740749047/pydantic_core-2.41.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c53ff33e603a9c1179a9364b0a24694f183717b2e0da2b5ad43c316c956901b2", size = 1910699, upload-time = "2025-10-14T10:20:30.217Z" }, + { url = "https://files.pythonhosted.org/packages/1e/63/4d948f1b9dd8e991a5a98b77dd66c74641f5f2e5225fee37994b2e07d391/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:304c54176af2c143bd181d82e77c15c41cbacea8872a2225dd37e6544dce9999", size = 1952121, upload-time = "2025-10-14T10:20:32.246Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a7/e5fc60a6f781fc634ecaa9ecc3c20171d238794cef69ae0af79ac11b89d7/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:025ba34a4cf4fb32f917d5d188ab5e702223d3ba603be4d8aca2f82bede432a4", size = 2041590, upload-time = "2025-10-14T10:20:34.332Z" }, + { url = "https://files.pythonhosted.org/packages/70/69/dce747b1d21d59e85af433428978a1893c6f8a7068fa2bb4a927fba7a5ff/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9f5f30c402ed58f90c70e12eff65547d3ab74685ffe8283c719e6bead8ef53f", size = 2219869, upload-time = "2025-10-14T10:20:35.965Z" }, + { url = "https://files.pythonhosted.org/packages/83/6a/c070e30e295403bf29c4df1cb781317b6a9bac7cd07b8d3acc94d501a63c/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd96e5d15385d301733113bcaa324c8bcf111275b7675a9c6e88bfb19fc05e3b", size = 2345169, upload-time = "2025-10-14T10:20:37.627Z" }, + { url = "https://files.pythonhosted.org/packages/f0/83/06d001f8043c336baea7fd202a9ac7ad71f87e1c55d8112c50b745c40324/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98f348cbb44fae6e9653c1055db7e29de67ea6a9ca03a5fa2c2e11a47cff0e47", size = 2070165, upload-time = "2025-10-14T10:20:39.246Z" }, + { url = "https://files.pythonhosted.org/packages/14/0a/e567c2883588dd12bcbc110232d892cf385356f7c8a9910311ac997ab715/pydantic_core-2.41.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec22626a2d14620a83ca583c6f5a4080fa3155282718b6055c2ea48d3ef35970", size = 2189067, upload-time = "2025-10-14T10:20:41.015Z" }, + { url = "https://files.pythonhosted.org/packages/f4/1d/3d9fca34273ba03c9b1c5289f7618bc4bd09c3ad2289b5420481aa051a99/pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3a95d4590b1f1a43bf33ca6d647b990a88f4a3824a8c4572c708f0b45a5290ed", size = 2132997, upload-time = "2025-10-14T10:20:43.106Z" }, + { url = "https://files.pythonhosted.org/packages/52/70/d702ef7a6cd41a8afc61f3554922b3ed8d19dd54c3bd4bdbfe332e610827/pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:f9672ab4d398e1b602feadcffcdd3af44d5f5e6ddc15bc7d15d376d47e8e19f8", size = 2307187, upload-time = "2025-10-14T10:20:44.849Z" }, + { url = "https://files.pythonhosted.org/packages/68/4c/c06be6e27545d08b802127914156f38d10ca287a9e8489342793de8aae3c/pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:84d8854db5f55fead3b579f04bda9a36461dab0730c5d570e1526483e7bb8431", size = 2305204, upload-time = "2025-10-14T10:20:46.781Z" }, + { url = "https://files.pythonhosted.org/packages/b0/e5/35ae4919bcd9f18603419e23c5eaf32750224a89d41a8df1a3704b69f77e/pydantic_core-2.41.4-cp312-cp312-win32.whl", hash = "sha256:9be1c01adb2ecc4e464392c36d17f97e9110fbbc906bcbe1c943b5b87a74aabd", size = 1972536, upload-time = "2025-10-14T10:20:48.39Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c2/49c5bb6d2a49eb2ee3647a93e3dae7080c6409a8a7558b075027644e879c/pydantic_core-2.41.4-cp312-cp312-win_amd64.whl", hash = "sha256:d682cf1d22bab22a5be08539dca3d1593488a99998f9f412137bc323179067ff", size = 2031132, upload-time = "2025-10-14T10:20:50.421Z" }, + { url = "https://files.pythonhosted.org/packages/06/23/936343dbcba6eec93f73e95eb346810fc732f71ba27967b287b66f7b7097/pydantic_core-2.41.4-cp312-cp312-win_arm64.whl", hash = "sha256:833eebfd75a26d17470b58768c1834dfc90141b7afc6eb0429c21fc5a21dcfb8", size = 1969483, upload-time = "2025-10-14T10:20:52.35Z" }, + { url = "https://files.pythonhosted.org/packages/c4/48/ae937e5a831b7c0dc646b2ef788c27cd003894882415300ed21927c21efa/pydantic_core-2.41.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:4f5d640aeebb438517150fdeec097739614421900e4a08db4a3ef38898798537", size = 2112087, upload-time = "2025-10-14T10:22:56.818Z" }, + { url = "https://files.pythonhosted.org/packages/5e/db/6db8073e3d32dae017da7e0d16a9ecb897d0a4d92e00634916e486097961/pydantic_core-2.41.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:4a9ab037b71927babc6d9e7fc01aea9e66dc2a4a34dff06ef0724a4049629f94", size = 1920387, upload-time = "2025-10-14T10:22:59.342Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c1/dd3542d072fcc336030d66834872f0328727e3b8de289c662faa04aa270e/pydantic_core-2.41.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4dab9484ec605c3016df9ad4fd4f9a390bc5d816a3b10c6550f8424bb80b18c", size = 1951495, upload-time = "2025-10-14T10:23:02.089Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c6/db8d13a1f8ab3f1eb08c88bd00fd62d44311e3456d1e85c0e59e0a0376e7/pydantic_core-2.41.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8a5028425820731d8c6c098ab642d7b8b999758e24acae03ed38a66eca8335", size = 2139008, upload-time = "2025-10-14T10:23:04.539Z" }, ] [[package]] @@ -2234,7 +2252,7 @@ requires-dist = [ { name = "anthropic", specifier = ">=0.69.0" }, { name = "azure-identity", specifier = ">=1.24.0" }, { name = "boto3", specifier = ">=1.40.25" }, - { name = "deepeval", specifier = ">=3.6.6" }, + { name = "deepeval", specifier = ">=3.6.0" }, { name = "deepteam", specifier = ">=0.2.5" }, { name = "dspy", specifier = ">=3.0.3" }, { name = "fastapi", specifier = ">=0.116.1" }, @@ -2274,38 +2292,38 @@ wheels = [ [[package]] name = "referencing" -version = "0.36.2" +version = "0.37.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, { name = "rpds-py" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744, upload-time = "2025-01-25T08:48:16.138Z" } +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775, upload-time = "2025-01-25T08:48:14.241Z" }, + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, ] [[package]] name = "regex" -version = "2025.9.18" +version = "2025.10.22" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/49/d3/eaa0d28aba6ad1827ad1e716d9a93e1ba963ada61887498297d3da715133/regex-2025.9.18.tar.gz", hash = "sha256:c5ba23274c61c6fef447ba6a39333297d0c247f53059dba0bca415cac511edc4", size = 400917, upload-time = "2025-09-19T00:38:35.79Z" } +sdist = { url = "https://files.pythonhosted.org/packages/90/f2/97d95db85e11cc85f97581cfc8b4a0405c7fb6099003c23ffaaa0cb4f31d/regex-2025.10.22.tar.gz", hash = "sha256:cc50db098b9d678ace33176a3ab4099616726ae4680fee6ac292302e8950fc4c", size = 400985, upload-time = "2025-10-21T00:48:37.365Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b0/99/05859d87a66ae7098222d65748f11ef7f2dff51bfd7482a4e2256c90d72b/regex-2025.9.18-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:436e1b31d7efd4dcd52091d076482031c611dde58bf9c46ca6d0a26e33053a7e", size = 486335, upload-time = "2025-09-19T00:36:03.661Z" }, - { url = "https://files.pythonhosted.org/packages/97/7e/d43d4e8b978890932cf7b0957fce58c5b08c66f32698f695b0c2c24a48bf/regex-2025.9.18-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c190af81e5576b9c5fdc708f781a52ff20f8b96386c6e2e0557a78402b029f4a", size = 289720, upload-time = "2025-09-19T00:36:05.471Z" }, - { url = "https://files.pythonhosted.org/packages/bb/3b/ff80886089eb5dcf7e0d2040d9aaed539e25a94300403814bb24cc775058/regex-2025.9.18-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e4121f1ce2b2b5eec4b397cc1b277686e577e658d8f5870b7eb2d726bd2300ab", size = 287257, upload-time = "2025-09-19T00:36:07.072Z" }, - { url = "https://files.pythonhosted.org/packages/ee/66/243edf49dd8720cba8d5245dd4d6adcb03a1defab7238598c0c97cf549b8/regex-2025.9.18-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:300e25dbbf8299d87205e821a201057f2ef9aa3deb29caa01cd2cac669e508d5", size = 797463, upload-time = "2025-09-19T00:36:08.399Z" }, - { url = "https://files.pythonhosted.org/packages/df/71/c9d25a1142c70432e68bb03211d4a82299cd1c1fbc41db9409a394374ef5/regex-2025.9.18-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7b47fcf9f5316c0bdaf449e879407e1b9937a23c3b369135ca94ebc8d74b1742", size = 862670, upload-time = "2025-09-19T00:36:10.101Z" }, - { url = "https://files.pythonhosted.org/packages/f8/8f/329b1efc3a64375a294e3a92d43372bf1a351aa418e83c21f2f01cf6ec41/regex-2025.9.18-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:57a161bd3acaa4b513220b49949b07e252165e6b6dc910ee7617a37ff4f5b425", size = 910881, upload-time = "2025-09-19T00:36:12.223Z" }, - { url = "https://files.pythonhosted.org/packages/35/9e/a91b50332a9750519320ed30ec378b74c996f6befe282cfa6bb6cea7e9fd/regex-2025.9.18-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f130c3a7845ba42de42f380fff3c8aebe89a810747d91bcf56d40a069f15352", size = 802011, upload-time = "2025-09-19T00:36:13.901Z" }, - { url = "https://files.pythonhosted.org/packages/a4/1d/6be3b8d7856b6e0d7ee7f942f437d0a76e0d5622983abbb6d21e21ab9a17/regex-2025.9.18-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5f96fa342b6f54dcba928dd452e8d8cb9f0d63e711d1721cd765bb9f73bb048d", size = 786668, upload-time = "2025-09-19T00:36:15.391Z" }, - { url = "https://files.pythonhosted.org/packages/cb/ce/4a60e53df58bd157c5156a1736d3636f9910bdcc271d067b32b7fcd0c3a8/regex-2025.9.18-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0f0d676522d68c207828dcd01fb6f214f63f238c283d9f01d85fc664c7c85b56", size = 856578, upload-time = "2025-09-19T00:36:16.845Z" }, - { url = "https://files.pythonhosted.org/packages/86/e8/162c91bfe7217253afccde112868afb239f94703de6580fb235058d506a6/regex-2025.9.18-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:40532bff8a1a0621e7903ae57fce88feb2e8a9a9116d341701302c9302aef06e", size = 849017, upload-time = "2025-09-19T00:36:18.597Z" }, - { url = "https://files.pythonhosted.org/packages/35/34/42b165bc45289646ea0959a1bc7531733e90b47c56a72067adfe6b3251f6/regex-2025.9.18-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:039f11b618ce8d71a1c364fdee37da1012f5a3e79b1b2819a9f389cd82fd6282", size = 788150, upload-time = "2025-09-19T00:36:20.464Z" }, - { url = "https://files.pythonhosted.org/packages/79/5d/cdd13b1f3c53afa7191593a7ad2ee24092a5a46417725ffff7f64be8342d/regex-2025.9.18-cp312-cp312-win32.whl", hash = "sha256:e1dd06f981eb226edf87c55d523131ade7285137fbde837c34dc9d1bf309f459", size = 264536, upload-time = "2025-09-19T00:36:21.922Z" }, - { url = "https://files.pythonhosted.org/packages/e0/f5/4a7770c9a522e7d2dc1fa3ffc83ab2ab33b0b22b447e62cffef186805302/regex-2025.9.18-cp312-cp312-win_amd64.whl", hash = "sha256:3d86b5247bf25fa3715e385aa9ff272c307e0636ce0c9595f64568b41f0a9c77", size = 275501, upload-time = "2025-09-19T00:36:23.4Z" }, - { url = "https://files.pythonhosted.org/packages/df/05/9ce3e110e70d225ecbed455b966003a3afda5e58e8aec2964042363a18f4/regex-2025.9.18-cp312-cp312-win_arm64.whl", hash = "sha256:032720248cbeeae6444c269b78cb15664458b7bb9ed02401d3da59fe4d68c3a5", size = 268601, upload-time = "2025-09-19T00:36:25.092Z" }, + { url = "https://files.pythonhosted.org/packages/95/a8/3380a8cb20c255878a9f1165b33c4d6a31d8f5417650c22b73bdcaadd281/regex-2025.10.22-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8b66971471306def7e6baf18ead3f416347d56eb5e295f8a75014d13be92e9fd", size = 489185, upload-time = "2025-10-21T00:45:52.929Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1c/e1eb33fc1f3a7851cc0f53b588790e14edeeb618e80fd5fd7ea987f9957d/regex-2025.10.22-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8c93b179960f4f2f517fe47da9984848d8342a6903b4d24649f4ee9bd22ccd3c", size = 291124, upload-time = "2025-10-21T00:45:54.934Z" }, + { url = "https://files.pythonhosted.org/packages/1b/21/6cc0fe9d4ebd7d6e19c08e77f41082103d52c671eb7eb01cc032e9bccbd4/regex-2025.10.22-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9b4fa8d221b5db3226029978c8c3f66f2e4c6d871e94b726bcd357e746b7a63", size = 288796, upload-time = "2025-10-21T00:45:56.248Z" }, + { url = "https://files.pythonhosted.org/packages/23/b0/d74069acbcc60b54977e693dd673099352b024f7f037cec201b0d96b7d99/regex-2025.10.22-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2a0d4e5f63c8de13fbab94d4a25cc6b02f1007b84e2d4c74f48c242eacb06f1", size = 798441, upload-time = "2025-10-21T00:45:57.896Z" }, + { url = "https://files.pythonhosted.org/packages/2c/f3/69cd09c226ce0fc6a5cf48b5dea716c0139abed41d02fa81fa774e56e713/regex-2025.10.22-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d8df6c82c544eed8314667a1fb8f705a9a802a9d6368045354319588ff56708d", size = 864038, upload-time = "2025-10-21T00:46:00.298Z" }, + { url = "https://files.pythonhosted.org/packages/8e/b0/77bd0e6838f579cc5a02b9e18bc0a759d0ed85b9a8d4d44ad6d3478a40ec/regex-2025.10.22-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a114c2735369334a755a844abd15d5a12716635cc4677fb4e6d793ce369310f6", size = 912054, upload-time = "2025-10-21T00:46:02.358Z" }, + { url = "https://files.pythonhosted.org/packages/2d/41/c320c3408050eefa516d352d9e05fd4d6af5da7ec0daea56d1e68bb9096c/regex-2025.10.22-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5d53115edada199723b831a49c7e1585ddda7940fb2ba7a78d12bf22e92f23e2", size = 803374, upload-time = "2025-10-21T00:46:03.837Z" }, + { url = "https://files.pythonhosted.org/packages/88/ed/0942c27223ce6bff95087f4859991634d995d6e186807e038fd1c2c3759c/regex-2025.10.22-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b4a7d813fdffe99ae0ecc17c80f652c8946c05a6a090eb2560719d02dfdb4b0", size = 787714, upload-time = "2025-10-21T00:46:05.934Z" }, + { url = "https://files.pythonhosted.org/packages/1c/40/10e2657ed24966742efd68eeb566e26af1eea3925dfe761ce14260a69161/regex-2025.10.22-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:81fb24976e3f71d765edec8a3175abb10359918d8997ca6a756fd68dd3c051f6", size = 858392, upload-time = "2025-10-21T00:46:07.801Z" }, + { url = "https://files.pythonhosted.org/packages/f3/48/bd382281e2f3bcfc2f355b5283ef16d8175b6df4cb6ed532529b715baf07/regex-2025.10.22-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:d881e96a443528a83f46ab69714befeb35f4d0caf359c43a606b82cb717a5df9", size = 850482, upload-time = "2025-10-21T00:46:09.893Z" }, + { url = "https://files.pythonhosted.org/packages/2e/5c/fdc0ac5eb3f21a6f19158cce3150e57a65d9770709b8521e09fe9febe813/regex-2025.10.22-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:42abc81ee54e06bef4dbc8e7b8394a57882c718ed3c6aabfea47e429feb94ee9", size = 789633, upload-time = "2025-10-21T00:46:11.687Z" }, + { url = "https://files.pythonhosted.org/packages/a2/ef/c2e63968c9130a17d79431ba8aa98ada02962435436ef506fb4cef139760/regex-2025.10.22-cp312-cp312-win32.whl", hash = "sha256:db30ab87b3d745b7e95e69099e1c4bf544c3f3800b9376b935943e86f650705a", size = 266060, upload-time = "2025-10-21T00:46:13.577Z" }, + { url = "https://files.pythonhosted.org/packages/5d/9d/57bc04978add42a62391f8082e94ec3a8c3448d49e349ede8c2c66ca0a55/regex-2025.10.22-cp312-cp312-win_amd64.whl", hash = "sha256:64190fa0432ed254416898ff3b687648e025445bfa357988f20f1332f651f650", size = 276928, upload-time = "2025-10-21T00:46:15.18Z" }, + { url = "https://files.pythonhosted.org/packages/89/50/760700909a618de1c2405f3a0557a3ec9b4eba516a261aa85fe973d3a354/regex-2025.10.22-cp312-cp312-win_arm64.whl", hash = "sha256:cdfc74d0af9b0cb9bd442619489582b32efc348db651a44967ba5fb71b8d3dee", size = 270103, upload-time = "2025-10-21T00:46:16.903Z" }, ] [[package]] @@ -2402,28 +2420,28 @@ wheels = [ [[package]] name = "ruff" -version = "0.14.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/41/b9/9bd84453ed6dd04688de9b3f3a4146a1698e8faae2ceeccce4e14c67ae17/ruff-0.14.0.tar.gz", hash = "sha256:62ec8969b7510f77945df916de15da55311fade8d6050995ff7f680afe582c57", size = 5452071, upload-time = "2025-10-07T18:21:55.763Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/4e/79d463a5f80654e93fa653ebfb98e0becc3f0e7cf6219c9ddedf1e197072/ruff-0.14.0-py3-none-linux_armv6l.whl", hash = "sha256:58e15bffa7054299becf4bab8a1187062c6f8cafbe9f6e39e0d5aface455d6b3", size = 12494532, upload-time = "2025-10-07T18:21:00.373Z" }, - { url = "https://files.pythonhosted.org/packages/ee/40/e2392f445ed8e02aa6105d49db4bfff01957379064c30f4811c3bf38aece/ruff-0.14.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:838d1b065f4df676b7c9957992f2304e41ead7a50a568185efd404297d5701e8", size = 13160768, upload-time = "2025-10-07T18:21:04.73Z" }, - { url = "https://files.pythonhosted.org/packages/75/da/2a656ea7c6b9bd14c7209918268dd40e1e6cea65f4bb9880eaaa43b055cd/ruff-0.14.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:703799d059ba50f745605b04638fa7e9682cc3da084b2092feee63500ff3d9b8", size = 12363376, upload-time = "2025-10-07T18:21:07.833Z" }, - { url = "https://files.pythonhosted.org/packages/42/e2/1ffef5a1875add82416ff388fcb7ea8b22a53be67a638487937aea81af27/ruff-0.14.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ba9a8925e90f861502f7d974cc60e18ca29c72bb0ee8bfeabb6ade35a3abde7", size = 12608055, upload-time = "2025-10-07T18:21:10.72Z" }, - { url = "https://files.pythonhosted.org/packages/4a/32/986725199d7cee510d9f1dfdf95bf1efc5fa9dd714d0d85c1fb1f6be3bc3/ruff-0.14.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e41f785498bd200ffc276eb9e1570c019c1d907b07cfb081092c8ad51975bbe7", size = 12318544, upload-time = "2025-10-07T18:21:13.741Z" }, - { url = "https://files.pythonhosted.org/packages/9a/ed/4969cefd53315164c94eaf4da7cfba1f267dc275b0abdd593d11c90829a3/ruff-0.14.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30a58c087aef4584c193aebf2700f0fbcfc1e77b89c7385e3139956fa90434e2", size = 14001280, upload-time = "2025-10-07T18:21:16.411Z" }, - { url = "https://files.pythonhosted.org/packages/ab/ad/96c1fc9f8854c37681c9613d825925c7f24ca1acfc62a4eb3896b50bacd2/ruff-0.14.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f8d07350bc7af0a5ce8812b7d5c1a7293cf02476752f23fdfc500d24b79b783c", size = 15027286, upload-time = "2025-10-07T18:21:19.577Z" }, - { url = "https://files.pythonhosted.org/packages/b3/00/1426978f97df4fe331074baf69615f579dc4e7c37bb4c6f57c2aad80c87f/ruff-0.14.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eec3bbbf3a7d5482b5c1f42d5fc972774d71d107d447919fca620b0be3e3b75e", size = 14451506, upload-time = "2025-10-07T18:21:22.779Z" }, - { url = "https://files.pythonhosted.org/packages/58/d5/9c1cea6e493c0cf0647674cca26b579ea9d2a213b74b5c195fbeb9678e15/ruff-0.14.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:16b68e183a0e28e5c176d51004aaa40559e8f90065a10a559176713fcf435206", size = 13437384, upload-time = "2025-10-07T18:21:25.758Z" }, - { url = "https://files.pythonhosted.org/packages/29/b4/4cd6a4331e999fc05d9d77729c95503f99eae3ba1160469f2b64866964e3/ruff-0.14.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb732d17db2e945cfcbbc52af0143eda1da36ca8ae25083dd4f66f1542fdf82e", size = 13447976, upload-time = "2025-10-07T18:21:28.83Z" }, - { url = "https://files.pythonhosted.org/packages/3b/c0/ac42f546d07e4f49f62332576cb845d45c67cf5610d1851254e341d563b6/ruff-0.14.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:c958f66ab884b7873e72df38dcabee03d556a8f2ee1b8538ee1c2bbd619883dd", size = 13682850, upload-time = "2025-10-07T18:21:31.842Z" }, - { url = "https://files.pythonhosted.org/packages/5f/c4/4b0c9bcadd45b4c29fe1af9c5d1dc0ca87b4021665dfbe1c4688d407aa20/ruff-0.14.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7eb0499a2e01f6e0c285afc5bac43ab380cbfc17cd43a2e1dd10ec97d6f2c42d", size = 12449825, upload-time = "2025-10-07T18:21:35.074Z" }, - { url = "https://files.pythonhosted.org/packages/4b/a8/e2e76288e6c16540fa820d148d83e55f15e994d852485f221b9524514730/ruff-0.14.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4c63b2d99fafa05efca0ab198fd48fa6030d57e4423df3f18e03aa62518c565f", size = 12272599, upload-time = "2025-10-07T18:21:38.08Z" }, - { url = "https://files.pythonhosted.org/packages/18/14/e2815d8eff847391af632b22422b8207704222ff575dec8d044f9ab779b2/ruff-0.14.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:668fce701b7a222f3f5327f86909db2bbe99c30877c8001ff934c5413812ac02", size = 13193828, upload-time = "2025-10-07T18:21:41.216Z" }, - { url = "https://files.pythonhosted.org/packages/44/c6/61ccc2987cf0aecc588ff8f3212dea64840770e60d78f5606cd7dc34de32/ruff-0.14.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a86bf575e05cb68dcb34e4c7dfe1064d44d3f0c04bbc0491949092192b515296", size = 13628617, upload-time = "2025-10-07T18:21:44.04Z" }, - { url = "https://files.pythonhosted.org/packages/73/e6/03b882225a1b0627e75339b420883dc3c90707a8917d2284abef7a58d317/ruff-0.14.0-py3-none-win32.whl", hash = "sha256:7450a243d7125d1c032cb4b93d9625dea46c8c42b4f06c6b709baac168e10543", size = 12367872, upload-time = "2025-10-07T18:21:46.67Z" }, - { url = "https://files.pythonhosted.org/packages/41/77/56cf9cf01ea0bfcc662de72540812e5ba8e9563f33ef3d37ab2174892c47/ruff-0.14.0-py3-none-win_amd64.whl", hash = "sha256:ea95da28cd874c4d9c922b39381cbd69cb7e7b49c21b8152b014bd4f52acddc2", size = 13464628, upload-time = "2025-10-07T18:21:50.318Z" }, - { url = "https://files.pythonhosted.org/packages/c6/2a/65880dfd0e13f7f13a775998f34703674a4554906167dce02daf7865b954/ruff-0.14.0-py3-none-win_arm64.whl", hash = "sha256:f42c9495f5c13ff841b1da4cb3c2a42075409592825dada7c5885c2c844ac730", size = 12565142, upload-time = "2025-10-07T18:21:53.577Z" }, +version = "0.14.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/58/6ca66896635352812de66f71cdf9ff86b3a4f79071ca5730088c0cd0fc8d/ruff-0.14.1.tar.gz", hash = "sha256:1dd86253060c4772867c61791588627320abcb6ed1577a90ef432ee319729b69", size = 5513429, upload-time = "2025-10-16T18:05:41.766Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/39/9cc5ab181478d7a18adc1c1e051a84ee02bec94eb9bdfd35643d7c74ca31/ruff-0.14.1-py3-none-linux_armv6l.whl", hash = "sha256:083bfc1f30f4a391ae09c6f4f99d83074416b471775b59288956f5bc18e82f8b", size = 12445415, upload-time = "2025-10-16T18:04:48.227Z" }, + { url = "https://files.pythonhosted.org/packages/ef/2e/1226961855ccd697255988f5a2474890ac7c5863b080b15bd038df820818/ruff-0.14.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f6fa757cd717f791009f7669fefb09121cc5f7d9bd0ef211371fad68c2b8b224", size = 12784267, upload-time = "2025-10-16T18:04:52.515Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ea/fd9e95863124ed159cd0667ec98449ae461de94acda7101f1acb6066da00/ruff-0.14.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d6191903d39ac156921398e9c86b7354d15e3c93772e7dbf26c9fcae59ceccd5", size = 11781872, upload-time = "2025-10-16T18:04:55.396Z" }, + { url = "https://files.pythonhosted.org/packages/1e/5a/e890f7338ff537dba4589a5e02c51baa63020acfb7c8cbbaea4831562c96/ruff-0.14.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed04f0e04f7a4587244e5c9d7df50e6b5bf2705d75059f409a6421c593a35896", size = 12226558, upload-time = "2025-10-16T18:04:58.166Z" }, + { url = "https://files.pythonhosted.org/packages/a6/7a/8ab5c3377f5bf31e167b73651841217542bcc7aa1c19e83030835cc25204/ruff-0.14.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5c9e6cf6cd4acae0febbce29497accd3632fe2025c0c583c8b87e8dbdeae5f61", size = 12187898, upload-time = "2025-10-16T18:05:01.455Z" }, + { url = "https://files.pythonhosted.org/packages/48/8d/ba7c33aa55406955fc124e62c8259791c3d42e3075a71710fdff9375134f/ruff-0.14.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a6fa2458527794ecdfbe45f654e42c61f2503a230545a91af839653a0a93dbc6", size = 12939168, upload-time = "2025-10-16T18:05:04.397Z" }, + { url = "https://files.pythonhosted.org/packages/b4/c2/70783f612b50f66d083380e68cbd1696739d88e9b4f6164230375532c637/ruff-0.14.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:39f1c392244e338b21d42ab29b8a6392a722c5090032eb49bb4d6defcdb34345", size = 14386942, upload-time = "2025-10-16T18:05:07.102Z" }, + { url = "https://files.pythonhosted.org/packages/48/44/cd7abb9c776b66d332119d67f96acf15830d120f5b884598a36d9d3f4d83/ruff-0.14.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7382fa12a26cce1f95070ce450946bec357727aaa428983036362579eadcc5cf", size = 13990622, upload-time = "2025-10-16T18:05:09.882Z" }, + { url = "https://files.pythonhosted.org/packages/eb/56/4259b696db12ac152fe472764b4f78bbdd9b477afd9bc3a6d53c01300b37/ruff-0.14.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd0bf2be3ae8521e1093a487c4aa3b455882f139787770698530d28ed3fbb37c", size = 13431143, upload-time = "2025-10-16T18:05:13.46Z" }, + { url = "https://files.pythonhosted.org/packages/e0/35/266a80d0eb97bd224b3265b9437bd89dde0dcf4faf299db1212e81824e7e/ruff-0.14.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cabcaa9ccf8089fb4fdb78d17cc0e28241520f50f4c2e88cb6261ed083d85151", size = 13132844, upload-time = "2025-10-16T18:05:16.1Z" }, + { url = "https://files.pythonhosted.org/packages/65/6e/d31ce218acc11a8d91ef208e002a31acf315061a85132f94f3df7a252b18/ruff-0.14.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:747d583400f6125ec11a4c14d1c8474bf75d8b419ad22a111a537ec1a952d192", size = 13401241, upload-time = "2025-10-16T18:05:19.395Z" }, + { url = "https://files.pythonhosted.org/packages/9f/b5/dbc4221bf0b03774b3b2f0d47f39e848d30664157c15b965a14d890637d2/ruff-0.14.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5a6e74c0efd78515a1d13acbfe6c90f0f5bd822aa56b4a6d43a9ffb2ae6e56cd", size = 12132476, upload-time = "2025-10-16T18:05:22.163Z" }, + { url = "https://files.pythonhosted.org/packages/98/4b/ac99194e790ccd092d6a8b5f341f34b6e597d698e3077c032c502d75ea84/ruff-0.14.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0ea6a864d2fb41a4b6d5b456ed164302a0d96f4daac630aeba829abfb059d020", size = 12139749, upload-time = "2025-10-16T18:05:25.162Z" }, + { url = "https://files.pythonhosted.org/packages/47/26/7df917462c3bb5004e6fdfcc505a49e90bcd8a34c54a051953118c00b53a/ruff-0.14.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:0826b8764f94229604fa255918d1cc45e583e38c21c203248b0bfc9a0e930be5", size = 12544758, upload-time = "2025-10-16T18:05:28.018Z" }, + { url = "https://files.pythonhosted.org/packages/64/d0/81e7f0648e9764ad9b51dd4be5e5dac3fcfff9602428ccbae288a39c2c22/ruff-0.14.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cbc52160465913a1a3f424c81c62ac8096b6a491468e7d872cb9444a860bc33d", size = 13221811, upload-time = "2025-10-16T18:05:30.707Z" }, + { url = "https://files.pythonhosted.org/packages/c3/07/3c45562c67933cc35f6d5df4ca77dabbcd88fddaca0d6b8371693d29fd56/ruff-0.14.1-py3-none-win32.whl", hash = "sha256:e037ea374aaaff4103240ae79168c0945ae3d5ae8db190603de3b4012bd1def6", size = 12319467, upload-time = "2025-10-16T18:05:33.261Z" }, + { url = "https://files.pythonhosted.org/packages/02/88/0ee4ca507d4aa05f67e292d2e5eb0b3e358fbcfe527554a2eda9ac422d6b/ruff-0.14.1-py3-none-win_amd64.whl", hash = "sha256:59d599cdff9c7f925a017f6f2c256c908b094e55967f93f2821b1439928746a1", size = 13401123, upload-time = "2025-10-16T18:05:35.984Z" }, + { url = "https://files.pythonhosted.org/packages/b8/81/4b6387be7014858d924b843530e1b2a8e531846807516e9bea2ee0936bf7/ruff-0.14.1-py3-none-win_arm64.whl", hash = "sha256:e3b443c4c9f16ae850906b8d0a707b2a4c16f8d2f0a7fe65c475c5886665ce44", size = 12436636, upload-time = "2025-10-16T18:05:38.995Z" }, ] [[package]] @@ -2476,26 +2494,17 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fe/02/c5e3bc518655d714622bec87d83db9cdba1cd0619a4a04e2109751c4f47f/sentencepiece-0.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:daeb5e9e9fcad012324807856113708614d534f596d5008638eb9b40112cd9e4", size = 1033923, upload-time = "2025-08-12T06:59:51.952Z" }, ] -[[package]] -name = "setuptools" -version = "80.9.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" }, -] - [[package]] name = "sentry-sdk" -version = "2.40.0" +version = "2.42.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4f/b5/ce879ce3292e5ca41fa3ebf68f60645032eca813c9ed8f92dcf09804c0e3/sentry_sdk-2.40.0.tar.gz", hash = "sha256:b9c4672fb2cafabcc28586ab8fd0ceeff9b2352afcf2b936e13d5ba06d141b9f", size = 351703, upload-time = "2025-10-06T12:27:29.207Z" } +sdist = { url = "https://files.pythonhosted.org/packages/31/04/ec8c1dd9250847303d98516e917978cb1c7083024770d86d657d2ccb5a70/sentry_sdk-2.42.1.tar.gz", hash = "sha256:8598cc6edcfe74cb8074ba6a7c15338cdee93d63d3eb9b9943b4b568354ad5b6", size = 354839, upload-time = "2025-10-20T12:38:40.45Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/d1/a54bd3622c6e742e6a01bc3bac45966b7ba886e29827da6b8ca7ae234e21/sentry_sdk-2.40.0-py2.py3-none-any.whl", hash = "sha256:d5f6ae0f27ea73e7b09c70ad7d42242326eb44765e87a15d8c5aab96b80013e6", size = 374747, upload-time = "2025-10-06T12:27:27.051Z" }, + { url = "https://files.pythonhosted.org/packages/0f/cb/c21b96ff379923310b4fb2c06e8d560d801e24aeb300faa72a04776868fc/sentry_sdk-2.42.1-py2.py3-none-any.whl", hash = "sha256:f8716b50c927d3beb41bc88439dc6bcd872237b596df5b14613e2ade104aee02", size = 380952, upload-time = "2025-10-20T12:38:38.88Z" }, ] [[package]] @@ -2669,7 +2678,7 @@ wheels = [ [[package]] name = "torch" -version = "2.8.0" +version = "2.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock" }, @@ -2689,6 +2698,7 @@ dependencies = [ { name = "nvidia-cusparselt-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, { name = "nvidia-nccl-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, { name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvshmem-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, { name = "nvidia-nvtx-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, { name = "setuptools" }, { name = "sympy" }, @@ -2696,10 +2706,10 @@ dependencies = [ { name = "typing-extensions" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/49/0c/2fd4df0d83a495bb5e54dca4474c4ec5f9c62db185421563deeb5dabf609/torch-2.8.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:e2fab4153768d433f8ed9279c8133a114a034a61e77a3a104dcdf54388838705", size = 101906089, upload-time = "2025-08-06T14:53:52.631Z" }, - { url = "https://files.pythonhosted.org/packages/99/a8/6acf48d48838fb8fe480597d98a0668c2beb02ee4755cc136de92a0a956f/torch-2.8.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b2aca0939fb7e4d842561febbd4ffda67a8e958ff725c1c27e244e85e982173c", size = 887913624, upload-time = "2025-08-06T14:56:44.33Z" }, - { url = "https://files.pythonhosted.org/packages/af/8a/5c87f08e3abd825c7dfecef5a0f1d9aa5df5dd0e3fd1fa2f490a8e512402/torch-2.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:2f4ac52f0130275d7517b03a33d2493bab3693c83dcfadf4f81688ea82147d2e", size = 241326087, upload-time = "2025-08-06T14:53:46.503Z" }, - { url = "https://files.pythonhosted.org/packages/be/66/5c9a321b325aaecb92d4d1855421e3a055abd77903b7dab6575ca07796db/torch-2.8.0-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:619c2869db3ada2c0105487ba21b5008defcc472d23f8b80ed91ac4a380283b0", size = 73630478, upload-time = "2025-08-06T14:53:57.144Z" }, + { url = "https://files.pythonhosted.org/packages/d1/d3/3985739f3b8e88675127bf70f82b3a48ae083e39cda56305dbd90398fec0/torch-2.9.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:e5f7af1dc4c0a7c4a260c2534f41ddaf209714f7c89145e644c44712fbd6b642", size = 104107898, upload-time = "2025-10-15T15:46:20.883Z" }, + { url = "https://files.pythonhosted.org/packages/a5/4b/f4bb2e6c25d0272f798cd6d7a04ed315da76cec68c602d87040c7847287f/torch-2.9.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:01cff95ecd9a212ea2f141db28acccdceb6a4c54f64e6c51091146f5e2a772c6", size = 899738273, upload-time = "2025-10-15T15:50:04.188Z" }, + { url = "https://files.pythonhosted.org/packages/66/11/c1c5ba6691cda6279087c35bd626536e4fd29521fe740abf5008377a9a02/torch-2.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:4582b162f541651f0cb184d3e291c05c2f556c7117c64a9873e2ee158d40062b", size = 109280887, upload-time = "2025-10-15T15:46:26.228Z" }, + { url = "https://files.pythonhosted.org/packages/dd/5f/b85bd8c05312d71de9402bf5868d217c38827cfd09d8f8514e5be128a52b/torch-2.9.0-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:33f58e9a102a91259af289d50525c30323b5c9ae1d31322b6447c0814da68695", size = 74478983, upload-time = "2025-10-15T15:46:39.406Z" }, ] [[package]] @@ -2716,7 +2726,7 @@ wheels = [ [[package]] name = "transformers" -version = "4.57.0" +version = "4.57.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock" }, @@ -2730,25 +2740,22 @@ dependencies = [ { name = "tokenizers" }, { name = "tqdm" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f3/5c/a22c39dac2687f3fe2a6b97e2c1ae516e91cd4d3976a7a2b7c24ff2fae48/transformers-4.57.0.tar.gz", hash = "sha256:d045753f3d93f9216e693cdb168698dfd2e9d3aad1bb72579a5d60ebf1545a8b", size = 10142956, upload-time = "2025-10-03T17:03:47.177Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/68/a39307bcc4116a30b2106f2e689130a48de8bd8a1e635b5e1030e46fcd9e/transformers-4.57.1.tar.gz", hash = "sha256:f06c837959196c75039809636cd964b959f6604b75b8eeec6fdfc0440b89cc55", size = 10142511, upload-time = "2025-10-14T15:39:26.18Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/2b/4d2708ac1ff5cd708b6548f4c5812d0ae40d1c28591c4c1c762b6dbdef2d/transformers-4.57.0-py3-none-any.whl", hash = "sha256:9d7c6d098c026e40d897e017ed1f481ab803cbac041021dbc6ae6100e4949b55", size = 11990588, upload-time = "2025-10-03T17:03:43.629Z" }, + { url = "https://files.pythonhosted.org/packages/71/d3/c16c3b3cf7655a67db1144da94b021c200ac1303f82428f2beef6c2e72bb/transformers-4.57.1-py3-none-any.whl", hash = "sha256:b10d05da8fa67dc41644dbbf9bc45a44cb86ae33da6f9295f5fbf5b7890bd267", size = 11990925, upload-time = "2025-10-14T15:39:23.085Z" }, ] [[package]] name = "triton" -version = "3.4.0" +version = "3.5.0" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "setuptools" }, -] wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/66/b1eb52839f563623d185f0927eb3530ee4d5ffe9d377cdaf5346b306689e/triton-3.4.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:31c1d84a5c0ec2c0f8e8a072d7fd150cab84a9c239eaddc6706c081bfae4eb04", size = 155560068, upload-time = "2025-07-30T19:58:37.081Z" }, + { url = "https://files.pythonhosted.org/packages/f5/3a/e991574f3102147b642e49637e0281e9bb7c4ba254edb2bab78247c85e01/triton-3.5.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9e71db82261c4ffa3921cd050cd5faa18322d2d405c30eb56084afaff3b0833", size = 170476535, upload-time = "2025-10-13T16:38:05.18Z" }, ] [[package]] name = "typer" -version = "0.19.2" +version = "0.20.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, @@ -2756,9 +2763,9 @@ dependencies = [ { name = "shellingham" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/21/ca/950278884e2ca20547ff3eb109478c6baf6b8cf219318e6bc4f666fad8e8/typer-0.19.2.tar.gz", hash = "sha256:9ad824308ded0ad06cc716434705f691d4ee0bfd0fb081839d2e426860e7fdca", size = 104755, upload-time = "2025-09-23T09:47:48.256Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8f/28/7c85c8032b91dbe79725b6f17d2fffc595dff06a35c7a30a37bef73a1ab4/typer-0.20.0.tar.gz", hash = "sha256:1aaf6494031793e4876fb0bacfa6a912b551cf43c1e63c800df8b1a866720c37", size = 106492, upload-time = "2025-10-20T17:03:49.445Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/00/22/35617eee79080a5d071d0f14ad698d325ee6b3bf824fc0467c03b30e7fa8/typer-0.19.2-py3-none-any.whl", hash = "sha256:755e7e19670ffad8283db353267cb81ef252f595aa6834a0d1ca9312d9326cb9", size = 46748, upload-time = "2025-09-23T09:47:46.777Z" }, + { url = "https://files.pythonhosted.org/packages/78/64/7713ffe4b5983314e9d436a90d5bd4f63b6054e2aca783a3cfc44cb95bbf/typer-0.20.0-py3-none-any.whl", hash = "sha256:5b463df6793ec1dca6213a3cf4c0f03bc6e322ac5e16e13ddd622a889489784a", size = 47028, upload-time = "2025-10-20T17:03:47.617Z" }, ] [[package]] @@ -2815,15 +2822,15 @@ wheels = [ [[package]] name = "uvicorn" -version = "0.37.0" +version = "0.38.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "h11" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/71/57/1616c8274c3442d802621abf5deb230771c7a0fec9414cb6763900eb3868/uvicorn-0.37.0.tar.gz", hash = "sha256:4115c8add6d3fd536c8ee77f0e14a7fd2ebba939fed9b02583a97f80648f9e13", size = 80367, upload-time = "2025-09-23T13:33:47.486Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cb/ce/f06b84e2697fef4688ca63bdb2fdf113ca0a3be33f94488f2cadb690b0cf/uvicorn-0.38.0.tar.gz", hash = "sha256:fd97093bdd120a2609fc0d3afe931d4d4ad688b6e75f0f929fde1bc36fe0e91d", size = 80605, upload-time = "2025-10-18T13:46:44.63Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/85/cd/584a2ceb5532af99dd09e50919e3615ba99aa127e9850eafe5f31ddfdb9a/uvicorn-0.37.0-py3-none-any.whl", hash = "sha256:913b2b88672343739927ce381ff9e2ad62541f9f8289664fa1d1d3803fa2ce6c", size = 67976, upload-time = "2025-09-23T13:33:45.842Z" }, + { url = "https://files.pythonhosted.org/packages/ee/d9/d88e73ca598f4f6ff671fb5fde8a32925c2e08a637303a1d12883c7305fa/uvicorn-0.38.0-py3-none-any.whl", hash = "sha256:48c0afd214ceb59340075b4a052ea1ee91c16fbc2a9b1469cca0e54566977b02", size = 68109, upload-time = "2025-10-18T13:46:42.958Z" }, ] [[package]] @@ -2910,21 +2917,23 @@ wheels = [ [[package]] name = "wrapt" -version = "1.17.3" +version = "2.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547, upload-time = "2025-08-12T05:53:21.714Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/41/cad1aba93e752f1f9268c77270da3c469883d56e2798e7df6240dcb2287b/wrapt-1.17.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0", size = 53998, upload-time = "2025-08-12T05:51:47.138Z" }, - { url = "https://files.pythonhosted.org/packages/60/f8/096a7cc13097a1869fe44efe68dace40d2a16ecb853141394047f0780b96/wrapt-1.17.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba", size = 39020, upload-time = "2025-08-12T05:51:35.906Z" }, - { url = "https://files.pythonhosted.org/packages/33/df/bdf864b8997aab4febb96a9ae5c124f700a5abd9b5e13d2a3214ec4be705/wrapt-1.17.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd", size = 39098, upload-time = "2025-08-12T05:51:57.474Z" }, - { url = "https://files.pythonhosted.org/packages/9f/81/5d931d78d0eb732b95dc3ddaeeb71c8bb572fb01356e9133916cd729ecdd/wrapt-1.17.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828", size = 88036, upload-time = "2025-08-12T05:52:34.784Z" }, - { url = "https://files.pythonhosted.org/packages/ca/38/2e1785df03b3d72d34fc6252d91d9d12dc27a5c89caef3335a1bbb8908ca/wrapt-1.17.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9", size = 88156, upload-time = "2025-08-12T05:52:13.599Z" }, - { url = "https://files.pythonhosted.org/packages/b3/8b/48cdb60fe0603e34e05cffda0b2a4adab81fd43718e11111a4b0100fd7c1/wrapt-1.17.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396", size = 87102, upload-time = "2025-08-12T05:52:14.56Z" }, - { url = "https://files.pythonhosted.org/packages/3c/51/d81abca783b58f40a154f1b2c56db1d2d9e0d04fa2d4224e357529f57a57/wrapt-1.17.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc", size = 87732, upload-time = "2025-08-12T05:52:36.165Z" }, - { url = "https://files.pythonhosted.org/packages/9e/b1/43b286ca1392a006d5336412d41663eeef1ad57485f3e52c767376ba7e5a/wrapt-1.17.3-cp312-cp312-win32.whl", hash = "sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe", size = 36705, upload-time = "2025-08-12T05:53:07.123Z" }, - { url = "https://files.pythonhosted.org/packages/28/de/49493f962bd3c586ab4b88066e967aa2e0703d6ef2c43aa28cb83bf7b507/wrapt-1.17.3-cp312-cp312-win_amd64.whl", hash = "sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c", size = 38877, upload-time = "2025-08-12T05:53:05.436Z" }, - { url = "https://files.pythonhosted.org/packages/f1/48/0f7102fe9cb1e8a5a77f80d4f0956d62d97034bbe88d33e94699f99d181d/wrapt-1.17.3-cp312-cp312-win_arm64.whl", hash = "sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6", size = 36885, upload-time = "2025-08-12T05:52:54.367Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/49/19/5e5bcd855d808892fe02d49219f97a50f64cd6d8313d75df3494ee97b1a3/wrapt-2.0.0.tar.gz", hash = "sha256:35a542cc7a962331d0279735c30995b024e852cf40481e384fd63caaa391cbb9", size = 81722, upload-time = "2025-10-19T23:47:54.07Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/28/7f266b5bf50c3ad0c99c524d99faa0f7d6eecb045d950e7d2c9e1f0e1338/wrapt-2.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73c6f734aecb1a030d9a265c13a425897e1ea821b73249bb14471445467ca71c", size = 78078, upload-time = "2025-10-19T23:45:58.855Z" }, + { url = "https://files.pythonhosted.org/packages/06/0c/bbdcad7eb535fae9d6b0fcfa3995c364797cd8e2b423bba5559ab2d88dcf/wrapt-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b4a7f8023b8ce8a36370154733c747f8d65c8697cb977d8b6efeb89291fff23e", size = 61158, upload-time = "2025-10-19T23:46:00.096Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8a/bba3e7a4ebf4d1624103ee59d97b78a1fbb08fb5753ff5d1b69f5ef5e863/wrapt-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a1cb62f686c50e9dab5983c68f6c8e9cbf14a6007935e683662898a7d892fa69", size = 61646, upload-time = "2025-10-19T23:46:01.279Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0c/0f565294897a72493dbafe7b46229b5f09f3776795a894d6b737e98387de/wrapt-2.0.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:43dc0550ae15e33e6bb45a82a5e1b5495be2587fbaa996244b509921810ee49f", size = 121442, upload-time = "2025-10-19T23:46:04.287Z" }, + { url = "https://files.pythonhosted.org/packages/da/80/7f03501a8a078ad79b19b1a888f9192a9494e62ddf8985267902766a4f30/wrapt-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39c5b45b056d630545e40674d1f5e1b51864b3546f25ab6a4a331943de96262e", size = 123018, upload-time = "2025-10-19T23:46:06.052Z" }, + { url = "https://files.pythonhosted.org/packages/37/6b/ad0e1ff98359f13b4b0c2c52848e792841146fe79ac5f56899b9a028fc0d/wrapt-2.0.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:804e88f824b76240a1b670330637ccfd2d18b9efa3bb4f02eb20b2f64880b324", size = 117369, upload-time = "2025-10-19T23:46:02.53Z" }, + { url = "https://files.pythonhosted.org/packages/ac/6c/a90437bba8cb1ce2ed639af979515e09784678c2a7f4ffc79f2cf7de809e/wrapt-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c2c476aa3fc2b9899c3f7b20963fac4f952e7edb74a31fc92f7745389a2e3618", size = 121453, upload-time = "2025-10-19T23:46:07.747Z" }, + { url = "https://files.pythonhosted.org/packages/2c/a9/b3982f9bd15bd45857a23c48b7c36e47d05db4a4dcc5061c31f169238845/wrapt-2.0.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8d851e526891216f89fcb7a1820dad9bd503ba3468fb9635ee28e93c781aa98e", size = 116250, upload-time = "2025-10-19T23:46:09.385Z" }, + { url = "https://files.pythonhosted.org/packages/73/e2/b7a8b1afac9f791d8f5eac0d9726559f1d7ec4a2b5a6b4e67ac145b007a5/wrapt-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b95733c2360c4a8656ee93c7af78e84c0bd617da04a236d7a456c8faa34e7a2d", size = 120575, upload-time = "2025-10-19T23:46:11.882Z" }, + { url = "https://files.pythonhosted.org/packages/a2/0f/37920eeea96094f450ae35505d39f1135df951a2cdee0d4e01d4f843396a/wrapt-2.0.0-cp312-cp312-win32.whl", hash = "sha256:ea56817176834edf143df1109ae8fdaa087be82fdad3492648de0baa8ae82bf2", size = 58175, upload-time = "2025-10-19T23:46:15.678Z" }, + { url = "https://files.pythonhosted.org/packages/f0/db/b395f3b0c7f2c60d9219afacc54ceb699801ccf2d3d969ba556dc6d3af20/wrapt-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:3c7d3bee7be7a2665286103f4d1f15405c8074e6e1f89dac5774f9357c9a3809", size = 60415, upload-time = "2025-10-19T23:46:12.913Z" }, + { url = "https://files.pythonhosted.org/packages/86/22/33d660214548af47fc59d9eec8c0e0693bcedc5b3a0b52e8cbdd61f3b646/wrapt-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:680f707e1d26acbc60926659799b15659f077df5897a6791c7c598a5d4a211c4", size = 58911, upload-time = "2025-10-19T23:46:13.889Z" }, + { url = "https://files.pythonhosted.org/packages/00/5c/c34575f96a0a038579683c7f10fca943c15c7946037d1d254ab9db1536ec/wrapt-2.0.0-py3-none-any.whl", hash = "sha256:02482fb0df89857e35427dfb844319417e14fae05878f295ee43fa3bf3b15502", size = 43998, upload-time = "2025-10-19T23:47:52.858Z" }, ] [[package]]