From 6a5c411800c79ffda2cdfbcc2bfa17e71ab7deca Mon Sep 17 00:00:00 2001 From: hiondal Date: Sun, 15 Jun 2025 13:52:26 +0000 Subject: [PATCH] release --- .gitignore | 120 + restaurant/README.md | 415 ++ restaurant/app/main.py | 741 +++ restaurant/app/requirements.txt | 6 + restaurant/build-base.sh | 214 + restaurant/build.sh | 251 + restaurant/create-imagepullsecret.sh | 237 + restaurant/deployment/container/Dockerfile | 46 + .../deployment/container/Dockerfile-base | 37 + restaurant/deployment/manifest/configmap.yaml | 34 + .../deployment/manifest/deployment.yaml | 81 + restaurant/deployment/manifest/ingress.yaml | 38 + restaurant/deployment/manifest/secret.yaml | 10 + restaurant/deployment/manifest/service.yaml | 16 + restaurant/setup.sh | 212 + review/README.md | 480 ++ review/app/main.py | 1626 +++++++ review/app/requirements.txt | 9 + review/build-base.sh | 217 + review/build.sh | 257 + review/create-imagepullsecret.sh | 103 + review/deployment/container/Dockerfile | 72 + review/deployment/container/Dockerfile-base | 103 + review/deployment/manifests/configmap.yaml | 78 + review/deployment/manifests/deployment.yaml | 130 + review/deployment/manifests/ingress.yaml | 39 + review/deployment/manifests/secret.yaml | 21 + review/deployment/manifests/service.yaml | 17 + review/setup.sh | 280 ++ vector/README.md | 614 +++ vector/app/config/settings.py | 80 + vector/app/main.py | 917 ++++ vector/app/requirements.txt | 67 + vector/app/services/claude_service.py | 336 ++ vector/app/services/restaurant_service.py | 235 + vector/app/services/review_service.py | 467 ++ vector/app/services/vector_service.py | 728 +++ vector/app/utils/category_utils.py | 161 + vector/app/utils/data_utils.py | 194 + vector/build-base.sh | 282 ++ vector/build.sh | 337 ++ vector/create-imagepullsecret.sh | 236 + vector/deployment/container/Dockerfile | 100 + vector/deployment/container/Dockerfile-base | 138 + vector/deployment/manifest/configmap.yaml | 63 + vector/deployment/manifest/deployment.yaml | 164 + vector/deployment/manifest/ingress.yaml | 39 + vector/deployment/manifest/pvc.yaml | 18 + vector/deployment/manifest/secret.yaml | 11 + vector/deployment/manifest/service.yaml | 17 + vector/poetry.lock | 4274 +++++++++++++++++ vector/pyproject.toml | 41 + vector/setup.sh | 376 ++ 53 files changed, 15785 insertions(+) create mode 100644 .gitignore create mode 100644 restaurant/README.md create mode 100644 restaurant/app/main.py create mode 100644 restaurant/app/requirements.txt create mode 100755 restaurant/build-base.sh create mode 100755 restaurant/build.sh create mode 100755 restaurant/create-imagepullsecret.sh create mode 100644 restaurant/deployment/container/Dockerfile create mode 100644 restaurant/deployment/container/Dockerfile-base create mode 100644 restaurant/deployment/manifest/configmap.yaml create mode 100644 restaurant/deployment/manifest/deployment.yaml create mode 100644 restaurant/deployment/manifest/ingress.yaml create mode 100644 restaurant/deployment/manifest/secret.yaml create mode 100644 restaurant/deployment/manifest/service.yaml create mode 100755 restaurant/setup.sh create mode 100644 review/README.md create mode 100644 review/app/main.py create mode 100644 review/app/requirements.txt create mode 100755 review/build-base.sh create mode 100755 review/build.sh create mode 100755 review/create-imagepullsecret.sh create mode 100644 review/deployment/container/Dockerfile create mode 100644 review/deployment/container/Dockerfile-base create mode 100644 review/deployment/manifests/configmap.yaml create mode 100644 review/deployment/manifests/deployment.yaml create mode 100644 review/deployment/manifests/ingress.yaml create mode 100644 review/deployment/manifests/secret.yaml create mode 100644 review/deployment/manifests/service.yaml create mode 100755 review/setup.sh create mode 100644 vector/README.md create mode 100644 vector/app/config/settings.py create mode 100644 vector/app/main.py create mode 100644 vector/app/requirements.txt create mode 100644 vector/app/services/claude_service.py create mode 100644 vector/app/services/restaurant_service.py create mode 100644 vector/app/services/review_service.py create mode 100644 vector/app/services/vector_service.py create mode 100644 vector/app/utils/category_utils.py create mode 100644 vector/app/utils/data_utils.py create mode 100755 vector/build-base.sh create mode 100755 vector/build.sh create mode 100755 vector/create-imagepullsecret.sh create mode 100644 vector/deployment/container/Dockerfile create mode 100644 vector/deployment/container/Dockerfile-base create mode 100644 vector/deployment/manifest/configmap.yaml create mode 100644 vector/deployment/manifest/deployment.yaml create mode 100644 vector/deployment/manifest/ingress.yaml create mode 100644 vector/deployment/manifest/pvc.yaml create mode 100644 vector/deployment/manifest/secret.yaml create mode 100644 vector/deployment/manifest/service.yaml create mode 100644 vector/poetry.lock create mode 100644 vector/pyproject.toml create mode 100755 vector/setup.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4dbc92f --- /dev/null +++ b/.gitignore @@ -0,0 +1,120 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Virtual environments +venv/ +env/ +ENV/ +env.bak/ +venv.bak/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Environment variables +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Logs +*.log +logs/ + +# Vector DB and ML Models +vector_db/ +*.pkl +*.model +*.bin +models/ +embeddings/ +checkpoints/ + +# Data files +data/ +*.csv +*.json +*.xlsx +*.parquet + +# Temporary files +tmp/ +temp/ +*.tmp +*.temp + +# Docker +.dockerignore + +# Node.js (if applicable) +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Database +*.db +*.sqlite +*.sqlite3 + +# Backup files +*.bak +*.backup \ No newline at end of file diff --git a/restaurant/README.md b/restaurant/README.md new file mode 100644 index 0000000..4a3e869 --- /dev/null +++ b/restaurant/README.md @@ -0,0 +1,415 @@ +# 카카오 API 기반 음식점 수집 서비스 + +카카오 로컬 API를 활용하여 음식점 정보를 수집하고 관리하는 RESTful API 서비스입니다. + +## 📋 프로젝트 개요 + +### 주요 기능 +- 🔍 **카카오 로컬 API 연동**: 키워드 및 지역 기반 음식점 검색 +- 📊 **다중 페이지 수집**: 최대 45페이지까지 데이터 수집 지원 +- 💾 **자동 JSON 저장**: 수집된 데이터를 JSON 파일로 자동 저장 +- 🚀 **RESTful API 제공**: FastAPI 기반의 완전한 API 서비스 +- 📚 **Swagger UI 지원**: 자동 생성되는 API 문서 +- ☸️ **Kubernetes 배포**: 완전한 컨테이너 오케스트레이션 지원 +- 🔧 **환경변수 설정**: ConfigMap과 Secret을 통한 유연한 설정 관리 + +### 기술 스택 +- **Backend**: Python 3.11, FastAPI, aiohttp +- **API**: Kakao Local API +- **Container**: Docker, Multi-stage build +- **Orchestration**: Kubernetes (AKS) +- **Registry**: Azure Container Registry (ACR) +- **Documentation**: Swagger UI, ReDoc + +## 🏗️ 프로젝트 구조 + +``` +review-api/restaurant/ +├── app/ # 애플리케이션 소스 +│ ├── main.py # 메인 애플리케이션 +│ └── requirements.txt # Python 의존성 +├── deployment/ # 배포 관련 파일 +│ ├── container/ # 컨테이너 이미지 빌드 +│ │ ├── Dockerfile # 서비스 이미지 빌드 +│ │ └── Dockerfile-base # 베이스 이미지 빌드 +│ └── manifests/ # Kubernetes 매니페스트 +│ ├── configmap.yaml # 환경 설정 +│ ├── secret.yaml # 민감 정보 (API 키) +│ ├── deployment.yaml # 애플리케이션 배포 +│ ├── service.yaml # 서비스 노출 +│ └── ingress.yaml # 외부 접근 +├── build-base.sh # 베이스 이미지 빌드 스크립트 +├── build.sh # 서비스 이미지 빌드 스크립트 +├── create-imagepullsecret.sh # ACR 인증 설정 스크립트 +├── setup.sh # 로컬 환경 설정 스크립트 +└── README.md # 프로젝트 문서 +``` + +## 📋 사전 작업 + +### 카카오 API 설정 +카카오 developers 포탈에서 애플리케이션 등록이 필요합니다. + +1. **포탈 접속**: https://developers.kakao.com/console/app +2. **애플리케이션 등록**: + - 앱 이름: `RestaurantCollector` + - 회사명: `{회사명}` + - 카테고리: `식음료` +3. **카카오맵 활성화**: 등록한 애플리케이션에서 좌측 '카카오맵' 메뉴 클릭하여 활성화 + +## 🚀 빠른 시작 + +### 1. 로컬 개발 환경 설정 + +```bash +# 저장소 클론 (review-api/restaurant 디렉토리로 이동) +cd review-api/restaurant + +# 환경 설정 스크립트 실행 +chmod +x setup.sh +./setup.sh + +# 가상환경 활성화 +source venv/bin/activate + +# 애플리케이션 실행 +python app/main.py +``` + +### 2. 로컬 웹 브라우저 접속 + +```bash +# 애플리케이션이 정상 실행된 후 아래 URL로 접속 +``` + +- **메인 페이지**: http://localhost:18000 +- **Swagger UI**: http://localhost:18000/docs +- **ReDoc**: http://localhost:18000/redoc +- **헬스체크**: http://localhost:18000/health + +### 3. API 테스트 + +```bash +# 기본 음식점 수집 +curl -X POST "http://localhost:18000/collect" \ + -H "Content-Type: application/json" \ + -d '{ + "query": "치킨", + "region": "서울", + "size": 15, + "pages": 3, + "save_to_file": true + }' + +# 수집된 파일 목록 조회 +curl "http://localhost:18000/list-files" + +# 파일 다운로드 (예시) +curl "http://localhost:18000/download/restaurant.json" -o downloaded_restaurants.json +``` + +## 🐳 Docker 컨테이너 실행 + +### 베이스 이미지 빌드 + +```bash +# ACR에 베이스 이미지 빌드 및 푸시 +./build-base.sh latest acrdigitalgarage03 rg-digitalgarage-03 +``` + +### 서비스 이미지 빌드 + +```bash +# ACR에 서비스 이미지 빌드 및 푸시 +./build.sh latest acrdigitalgarage03 rg-digitalgarage-03 +``` + +### 로컬 Docker 실행 + +```bash +# 컨테이너 실행 +docker run -p 18000:8000 \ + -e KAKAO_API_KEY=5cdc24407edbf8544f3954cfaa4650c6 \ + -e PORT=8000 \ + restaurant-api:latest + +# 백그라운드 실행 +docker run -d -p 18000:8000 \ + --name restaurant-api \ + -e KAKAO_API_KEY=5cdc24407edbf8544f3954cfaa4650c6 \ + -e PORT=8000 \ + restaurant-api:latest +``` + +## ☸️ Kubernetes 배포 + +### 1. ACR Image Pull Secret 생성 + +```bash +# Image Pull Secret 생성 +./create-imagepullsecret.sh acrdigitalgarage03 rg-digitalgarage-03 +``` + +### 2. Kubernetes 리소스 배포 + +```bash +# ConfigMap 및 Secret 적용 +kubectl apply -f deployment/manifests/configmap.yaml +kubectl apply -f deployment/manifests/secret.yaml + +# 애플리케이션 배포 +kubectl apply -f deployment/manifests/deployment.yaml +kubectl apply -f deployment/manifests/service.yaml +kubectl apply -f deployment/manifests/ingress.yaml +``` + +### 3. 배포 상태 확인 + +```bash +# Pod 상태 확인 +kubectl get pods -l app=restaurant-api + +# 서비스 상태 확인 +kubectl get svc restaurant-api-service + +# Ingress 상태 확인 +kubectl get ingress restaurant-api-ingress + +# 로그 확인 +kubectl logs -l app=restaurant-api -f +``` + +### 4. 🌐 외부 브라우저에서 접속하기 + +#### Ingress 주소 확인 방법 + +```bash +# 1. Ingress 설정된 호스트 확인 +kubectl get ingress restaurant-api-ingress -o jsonpath='{.spec.rules[0].host}' + +# 2. Ingress External IP 확인 (LoadBalancer 타입인 경우) +kubectl get ingress restaurant-api-ingress + +# 3. Ingress Controller의 External IP 확인 +kubectl get svc -n ingress-nginx ingress-nginx-controller + +# 4. 현재 설정된 ingress 주소 확인 +INGRESS_HOST=$(kubectl get ingress restaurant-api-ingress -o jsonpath='{.spec.rules[0].host}') +echo "🌐 Restaurant API URL: http://${INGRESS_HOST}" +``` + +#### 브라우저 접속 주소 + +현재 설정된 주소로 접속하세요: + +```bash +# 현재 설정된 기본 주소 (환경에 따라 다를 수 있음) +INGRESS_URL="http://restaurant-api.20.249.191.180.nip.io" +echo "브라우저에서 접속: ${INGRESS_URL}" +``` + +**주요 접속 페이지:** +- **🏠 메인 페이지**: http://restaurant-api.20.249.191.180.nip.io +- **📖 Swagger UI**: http://restaurant-api.20.249.191.180.nip.io/docs +- **📄 ReDoc**: http://restaurant-api.20.249.191.180.nip.io/redoc +- **❤️ 헬스체크**: http://restaurant-api.20.249.191.180.nip.io/health + +#### 접속 테스트 + +```bash +# API 접속 테스트 +curl "http://restaurant-api.20.249.191.180.nip.io/health" + +# 설정 정보 확인 +curl "http://restaurant-api.20.249.191.180.nip.io/config" + +# Swagger UI 접속 확인 +curl -I "http://restaurant-api.20.249.191.180.nip.io/docs" +``` + +## ⚙️ 환경 설정 + +### 환경 변수 + +| 변수명 | 기본값 | 설명 | +|--------|--------|------| +| `KAKAO_API_KEY` | `5cdc24407edbf8544f3954cfaa4650c6` | 카카오 API 키 | +| `APP_TITLE` | `카카오 API 기반 음식점 수집 서비스` | 애플리케이션 제목 | +| `HOST` | `0.0.0.0` | 서버 호스트 | +| `PORT` | `8000` | 서버 포트 (컨테이너 내부) | +| `DEFAULT_QUERY` | `음식점` | 기본 검색 키워드 | +| `DEFAULT_REGION` | `서울` | 기본 검색 지역 | +| `DEFAULT_SIZE` | `15` | 페이지당 결과 수 | +| `MAX_PAGES` | `45` | 최대 페이지 수 | +| `DATA_DIR` | `/app/data` | 데이터 저장 디렉토리 | +| `REQUEST_DELAY` | `0.1` | API 요청 간 지연시간(초) | + +## 📊 API 엔드포인트 + +### 주요 엔드포인트 + +| Method | Endpoint | 설명 | +|--------|----------|------| +| `GET` | `/` | 메인 페이지 | +| `GET` | `/docs` | Swagger UI 문서 | +| `GET` | `/health` | 헬스체크 | +| `POST` | `/collect` | 음식점 정보 수집 | +| `POST` | `/collect-background` | 백그라운드 수집 | +| `GET` | `/list-files` | 저장된 파일 목록 | +| `GET` | `/download/{filename}` | 파일 다운로드 | +| `GET` | `/config` | 환경 설정 확인 | + +### 수집 API 예시 + +```json +POST /collect +{ + "query": "피자", + "region": "강남구", + "size": 15, + "pages": 5, + "save_to_file": true +} +``` + +**응답:** +```json +{ + "success": true, + "message": "음식점 정보 수집이 완료되었습니다. (총 67개)", + "metadata": { + "collection_date": "2024-06-12T10:30:00", + "query": "피자", + "region": "강남구", + "total_count": 67, + "pages_collected": 5, + "execution_time": 12.5 + }, + "restaurants": [...], + "file_saved": true, + "file_path": "/app/data/restaurants_피자_강남구_20240612_103000.json" +} +``` + +## 🔧 개발 및 확장 + +### 로컬 개발 + +```bash +# 개발 모드로 실행 (자동 재시작) +uvicorn app.main:app --host 0.0.0.0 --port 18000 --reload + +# 의존성 추가 +pip install 새패키지명 +pip freeze > app/requirements.txt + +# 코드 포맷팅 +black app/main.py +flake8 app/main.py +``` + +### Ingress 호스트 변경 + +현재 환경에 맞게 Ingress 호스트를 변경하려면: + +```bash +# 1. 현재 External IP 확인 +kubectl get svc -n ingress-nginx ingress-nginx-controller + +# 2. deployment/manifests/ingress.yaml 파일에서 host 수정 +# 예: restaurant-api.{YOUR_EXTERNAL_IP}.nip.io + +# 3. 변경사항 적용 +kubectl apply -f deployment/manifests/ingress.yaml + +# 4. 새로운 주소 확인 +kubectl get ingress restaurant-api-ingress +``` + +## 🐛 문제 해결 + +### 일반적인 문제 + +**1. API 키 인증 실패** +```bash +# API 키 확인 +curl -H "Authorization: KakaoAK YOUR_API_KEY" \ + "https://dapi.kakao.com/v2/local/search/keyword.json?query=테스트&size=1" + +# 환경변수 확인 +echo $KAKAO_API_KEY +``` + +**2. Kubernetes 배포 실패** +```bash +# Pod 로그 확인 +kubectl logs -l app=restaurant-api + +# ConfigMap 확인 +kubectl get configmap restaurant-api-config -o yaml + +# Secret 확인 +kubectl get secret restaurant-api-secret -o yaml +``` + +**3. Ingress 접속 실패** +```bash +# Ingress Controller 상태 확인 +kubectl get pods -n ingress-nginx + +# Ingress 규칙 확인 +kubectl describe ingress restaurant-api-ingress + +# Service 연결 확인 +kubectl get endpoints restaurant-api-service +``` + +**4. 포트 관련 문제** +- 로컬 개발: 18000번 포트 사용 +- Docker 컨테이너: 8000번 포트로 실행 (외부 18000번으로 매핑) +- Kubernetes: 8000번 포트로 실행 (Service에서 80번으로 노출, Ingress를 통해 외부 접근) + +## 🎯 성능 최적화 + +### 권장 설정 + +```yaml +# deployment.yaml에서 리소스 최적화 +resources: + requests: + memory: "512Mi" + cpu: "250m" + limits: + memory: "1Gi" + cpu: "500m" +``` + +### 모니터링 + +```bash +# 리소스 사용량 확인 +kubectl top pods -l app=restaurant-api + +# 메트릭 수집 (Prometheus 설정 시) +kubectl get --raw /metrics +``` + +## 📈 향후 확장 계획 + +- [ ] **전국 확대**: 지역별 음식점 수집 기능 확장 +- [ ] **데이터베이스 연동**: PostgreSQL, MongoDB 지원 +- [ ] **비동기 작업 큐**: Celery, Redis 활용 +- [ ] **데이터 분석**: 음식점 트렌드 분석 기능 +- [ ] **알림 시스템**: 새로운 음식점 알림 기능 +- [ ] **사용자 인증**: JWT 기반 인증 시스템 +- [ ] **API 레이트 리미팅**: 요청 제한 기능 +- [ ] **GraphQL 지원**: 유연한 쿼리 인터페이스 +- [ ] **실시간 모니터링**: Grafana, Prometheus 연동 + +--- + +## 📞 지원 및 문의 + +- **이슈 리포트**: GitHub Issues +- **기술 문의**: 개발팀 Slack +- **API 문서**: Swagger UI에서 상세 확인 \ No newline at end of file diff --git a/restaurant/app/main.py b/restaurant/app/main.py new file mode 100644 index 0000000..81c8861 --- /dev/null +++ b/restaurant/app/main.py @@ -0,0 +1,741 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +카카오 API 기반 음식점 수집 서비스 +review-api/restaurant/app/main.py +""" + +import os +import json +import asyncio +import aiohttp +import logging +import sys +from datetime import datetime +from typing import Optional, List, Dict, Any +from fastapi import FastAPI, HTTPException, BackgroundTasks, Query +from fastapi.responses import HTMLResponse, FileResponse +from pydantic import BaseModel, Field +import uvicorn + +# ============================================================================= +# .env 파일 로딩 (다른 import보다 먼저) +# ============================================================================= +from dotenv import load_dotenv + +# .env 파일에서 환경변수 로드 +load_dotenv() + +# ============================================================================= +# 로깅 설정 +# ============================================================================= +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[logging.StreamHandler(sys.stdout)] +) +logger = logging.getLogger(__name__) + +# ============================================================================= +# 환경 변수 설정 +# ============================================================================= +class Config: + """환경 변수 기반 설정 클래스""" + + # 애플리케이션 메타데이터 + APP_TITLE = os.getenv("APP_TITLE", "카카오 API 기반 음식점 수집 서비스") + APP_VERSION = os.getenv("APP_VERSION", "1.0.0") + APP_DESCRIPTION = os.getenv("APP_DESCRIPTION", "카카오 로컬 API를 활용한 음식점 정보 수집 시스템") + + # 서버 설정 + HOST = os.getenv("HOST", "0.0.0.0") + PORT = int(os.getenv("PORT", "8000")) + LOG_LEVEL = os.getenv("LOG_LEVEL", "info") + + # 카카오 API 설정 + KAKAO_API_KEY = os.getenv("KAKAO_API_KEY", "5cdc24407edbf8544f3954cfaa4650c6") + KAKAO_API_URL = "https://dapi.kakao.com/v2/local/search/keyword.json" + + # 검색 설정 + DEFAULT_QUERY = os.getenv("DEFAULT_QUERY", "음식점") + DEFAULT_REGION = os.getenv("DEFAULT_REGION", "서울") + DEFAULT_SIZE = int(os.getenv("DEFAULT_SIZE", "15")) + MAX_SIZE = int(os.getenv("MAX_SIZE", "15")) # 카카오 API 최대값 + MAX_PAGES = int(os.getenv("MAX_PAGES", "45")) # 카카오 API 최대 페이지 (45페이지 * 15 = 675개) + + # 파일 설정 + OUTPUT_FILE = os.getenv("OUTPUT_FILE", "restaurant.json") + DATA_DIR = os.getenv("DATA_DIR", "./data") + + # 요청 제한 설정 + REQUEST_DELAY = float(os.getenv("REQUEST_DELAY", "0.1")) # API 요청 간 지연시간(초) + REQUEST_TIMEOUT = int(os.getenv("REQUEST_TIMEOUT", "30")) + + # 헬스체크 설정 + HEALTH_CHECK_TIMEOUT = int(os.getenv("HEALTH_CHECK_TIMEOUT", "10")) + +config = Config() + +# 데이터 디렉토리 생성 +os.makedirs(config.DATA_DIR, exist_ok=True) + +# FastAPI 앱 초기화 +app = FastAPI( + title=config.APP_TITLE, + description=f""" + {config.APP_DESCRIPTION} + + **주요 기능:** + - 카카오 로컬 API를 활용한 음식점 정보 수집 + - 지역별, 키워드별 검색 지원 + - JSON 파일 자동 저장 기능 + - RESTful API 제공 + - Swagger UI 문서 제공 + + **API 키:** {config.KAKAO_API_KEY[:10]}... + **버전:** {config.APP_VERSION} + """, + version=config.APP_VERSION, + contact={ + "name": "관리자", + "email": "admin@example.com" + } +) + +# ============================================================================= +# Pydantic 모델 정의 +# ============================================================================= + +class RestaurantSearchRequest(BaseModel): + """음식점 검색 요청 모델""" + query: str = Field( + default=config.DEFAULT_QUERY, + description="검색 키워드 (예: 치킨, 피자, 한식)", + example="치킨" + ) + region: str = Field( + default=config.DEFAULT_REGION, + description="검색 지역 (예: 서울, 부산, 대구)", + example="서울" + ) + size: int = Field( + default=config.DEFAULT_SIZE, + description=f"페이지당 결과 수 (1-{config.MAX_SIZE})", + example=15, + ge=1, + le=config.MAX_SIZE + ) + pages: int = Field( + default=5, + description=f"검색할 페이지 수 (1-{config.MAX_PAGES})", + example=5, + ge=1, + le=config.MAX_PAGES + ) + save_to_file: bool = Field( + default=True, + description="결과를 JSON 파일로 저장할지 여부", + example=True + ) + +class RestaurantInfo(BaseModel): + """음식점 정보 모델""" + id: str = Field(description="카카오 장소 ID") + place_name: str = Field(description="장소명") + category_name: str = Field(description="카테고리명") + category_group_code: str = Field(description="카테고리 그룹 코드") + category_group_name: str = Field(description="카테고리 그룹명") + phone: str = Field(description="전화번호") + address_name: str = Field(description="전체 지번 주소") + road_address_name: str = Field(description="전체 도로명 주소") + place_url: str = Field(description="장소 상세페이지 URL") + distance: str = Field(description="중심좌표까지의 거리 (meter)") + x: str = Field(description="X 좌표값, 경위도인 경우 longitude") + y: str = Field(description="Y 좌표값, 경위도인 경우 latitude") + +class CollectionMetadata(BaseModel): + """수집 메타데이터""" + collection_date: str = Field(description="수집 날짜시간") + query: str = Field(description="검색 키워드") + region: str = Field(description="검색 지역") + total_count: int = Field(description="총 수집된 음식점 수") + pages_collected: int = Field(description="수집된 페이지 수") + api_key_used: str = Field(description="사용된 API 키 (마스킹)") + execution_time: float = Field(description="실행 시간(초)") + +class RestaurantSearchResponse(BaseModel): + """음식점 검색 응답 모델""" + success: bool = Field(description="검색 성공 여부") + message: str = Field(description="응답 메시지") + metadata: CollectionMetadata + restaurants: List[RestaurantInfo] + file_saved: bool = Field(description="파일 저장 여부") + file_path: Optional[str] = Field(description="저장된 파일 경로") + +class ErrorResponse(BaseModel): + """에러 응답 모델""" + success: bool = False + error: str + message: str + timestamp: str + +# ============================================================================= +# 카카오 API 클라이언트 +# ============================================================================= + +class KakaoRestaurantCollector: + """카카오 API를 활용한 음식점 수집기""" + + def __init__(self): + self.api_key = config.KAKAO_API_KEY + self.api_url = config.KAKAO_API_URL + self.session = None + logger.info("KakaoRestaurantCollector 초기화 완료") + + async def __aenter__(self): + """비동기 컨텍스트 매니저 진입""" + self.session = aiohttp.ClientSession( + timeout=aiohttp.ClientTimeout(total=config.REQUEST_TIMEOUT), + headers={ + 'Authorization': f'KakaoAK {self.api_key}', + 'Content-Type': 'application/json' + } + ) + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + """비동기 컨텍스트 매니저 종료""" + if self.session: + await self.session.close() + + async def search_restaurants(self, query: str, region: str, size: int = 15, pages: int = 5) -> Dict[str, Any]: + """음식점 검색 실행 (개선된 버전)""" + logger.info(f"음식점 검색 시작: query='{query}', region='{region}', size={size}, pages={pages}") + + start_time = datetime.now() + all_restaurants = [] + collected_pages = 0 + empty_page_count = 0 # 연속 빈 페이지 카운터 추가 + + try: + for page in range(1, pages + 1): + logger.info(f"페이지 {page}/{pages} 검색 중...") + + # API 요청 파라미터 - 더 구체적인 검색 + params = { + 'query': f"{query} {region}", + 'category_group_code': 'FD6', # 음식점 카테고리 + 'page': page, + 'size': size, + 'sort': 'accuracy' + } + + try: + async with self.session.get(self.api_url, params=params) as response: + if response.status == 200: + data = await response.json() + documents = data.get('documents', []) + + if not documents: + empty_page_count += 1 + logger.warning(f"페이지 {page}에서 결과 없음 (연속 빈 페이지: {empty_page_count})") + + # 연속 3페이지가 비어있으면 종료 (개선) + if empty_page_count >= 3: + logger.info(f"연속 {empty_page_count}페이지 빈 결과로 검색 종료") + break + + # 빈 페이지여도 계속 진행 + await asyncio.sleep(config.REQUEST_DELAY) + continue + + # 결과가 있으면 빈 페이지 카운터 리셋 + empty_page_count = 0 + + # 음식점 정보 추출 및 저장 + page_restaurants = [] + for doc in documents: + restaurant = self._extract_restaurant_info(doc) + if restaurant: + page_restaurants.append(restaurant) + + all_restaurants.extend(page_restaurants) + collected_pages += 1 + + logger.info(f"페이지 {page} 완료: {len(page_restaurants)}개 음식점 수집 (총: {len(all_restaurants)}개)") + + # API 요청 제한을 위한 지연 + if page < pages: + await asyncio.sleep(config.REQUEST_DELAY) + + elif response.status == 400: + error_data = await response.json() + logger.warning(f"API 요청 오류 (페이지 {page}): {error_data}") + + # 400 오류여도 계속 진행 (다른 페이지는 성공할 수 있음) + empty_page_count += 1 + if empty_page_count >= 5: + break + continue + + elif response.status == 401: + logger.error("API 키 인증 실패") + raise HTTPException(status_code=401, detail="카카오 API 키 인증 실패") + + elif response.status == 429: + logger.warning("API 요청 한도 초과 - 2초 대기 후 재시도") + await asyncio.sleep(2) + continue + + else: + logger.error(f"API 요청 실패: HTTP {response.status}") + empty_page_count += 1 + if empty_page_count >= 5: + break + continue + + except asyncio.TimeoutError: + logger.error(f"페이지 {page} 요청 타임아웃") + empty_page_count += 1 + if empty_page_count >= 5: + break + continue + except Exception as e: + logger.error(f"페이지 {page} 요청 중 오류: {e}") + empty_page_count += 1 + if empty_page_count >= 5: + break + continue + + execution_time = (datetime.now() - start_time).total_seconds() + + # 중복 제거 + + logger.info(f"중복 제거 시작: {len(all_restaurants)}개") + + # 중복 제거 통계 + stats = { + 'total': len(all_restaurants), + 'by_place_url': 0, + 'by_place_id': 0, + 'by_coordinates': 0, + 'duplicates': 0 + } + + unique_restaurants = {} + + for restaurant in all_restaurants: + place_url = restaurant.get('place_url', '').strip() + place_id = restaurant.get('id', '').strip() + place_name = restaurant.get('place_name', '').strip() + x = restaurant.get('x', '').strip() + y = restaurant.get('y', '').strip() + + # 고유 키 생성 (다중 전략) + unique_key = None + + if place_url: + unique_key = f"url:{place_url}" + stats['by_place_url'] += 1 + elif place_id: + unique_key = f"id:{place_id}" + stats['by_place_id'] += 1 + elif place_name and x and y: + unique_key = f"coord:{place_name}:{x}:{y}" + stats['by_coordinates'] += 1 + else: + # 마지막 수단: 이름만으로 + unique_key = f"name:{place_name}" + + if unique_key: + if unique_key not in unique_restaurants: + unique_restaurants[unique_key] = restaurant + else: + stats['duplicates'] += 1 + + final_restaurants = list(unique_restaurants.values()) + + logger.info(f"중복 제거 완료:") + logger.info(f" 총 입력: {stats['total']}개") + logger.info(f" URL 기준: {stats['by_place_url']}개") + logger.info(f" ID 기준: {stats['by_place_id']}개") + logger.info(f" 좌표 기준: {stats['by_coordinates']}개") + logger.info(f" 중복 제거: {stats['duplicates']}개") + logger.info(f" 최종 결과: {len(final_restaurants)}개") + logger.info(f" 중복률: {(stats['duplicates']/stats['total']*100):.1f}%") + + logger.info(f"검색 완료: 총 {len(final_restaurants)}개 음식점 수집 (중복 제거 후), 수집된 페이지: {collected_pages}") + + return { + 'restaurants': final_restaurants, + 'metadata': { + 'collection_date': start_time.isoformat(), + 'query': query, + 'region': region, + 'total_count': len(final_restaurants), + 'pages_collected': collected_pages, + 'pages_requested': pages, + 'api_key_used': f"{self.api_key[:10]}...", + 'execution_time': execution_time + } + } + + except Exception as e: + logger.error(f"음식점 검색 중 오류: {e}") + raise + + def _extract_restaurant_info(self, document: Dict) -> Dict[str, Any]: + """카카오 API 응답에서 음식점 정보 추출""" + try: + return { + 'id': document.get('id', ''), + 'place_name': document.get('place_name', ''), + 'category_name': document.get('category_name', ''), + 'category_group_code': document.get('category_group_code', ''), + 'category_group_name': document.get('category_group_name', ''), + 'phone': document.get('phone', ''), + 'address_name': document.get('address_name', ''), + 'road_address_name': document.get('road_address_name', ''), + 'place_url': document.get('place_url', ''), + 'distance': document.get('distance', ''), + 'x': document.get('x', ''), + 'y': document.get('y', '') + } + except Exception as e: + logger.warning(f"음식점 정보 추출 실패: {e}") + return None + +# ============================================================================= +# 파일 관리 유틸리티 +# ============================================================================= + +def save_restaurants_to_file(data: Dict[str, Any], filename: str = None) -> str: + """음식점 데이터를 JSON 파일로 저장""" + if filename is None: + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + filename = f"restaurants_{timestamp}.json" + + file_path = os.path.join(config.DATA_DIR, filename) + + try: + with open(file_path, 'w', encoding='utf-8') as f: + json.dump(data, f, ensure_ascii=False, indent=2) + + logger.info(f"음식점 데이터 저장 완료: {file_path}") + return file_path + + except Exception as e: + logger.error(f"파일 저장 실패: {e}") + raise + +# ============================================================================= +# API 엔드포인트 +# ============================================================================= + +@app.get("/", response_class=HTMLResponse, include_in_schema=False) +async def root(): + """메인 페이지""" + return f""" + + + {config.APP_TITLE} + + + +
+

🍽️ {config.APP_TITLE}

+

카카오 로컬 API를 활용한 음식점 정보 수집 시스템

+

버전: {config.APP_VERSION}

+
+ +
+

📋 서비스 정보

+ +
+ +
+

⚙️ 환경 설정

+ +
+ +

📚 API 문서

+ Swagger UI 문서 + ReDoc 문서 + 헬스 체크 + +

🛠️ 사용 방법

+

POST /collect - 음식점 정보 수집

+
+{{
+  "query": "치킨",
+  "region": "서울",
+  "size": 15,
+  "pages": 5,
+  "save_to_file": true
+}}
+            
+ +

GET /download/{filename} - 저장된 파일 다운로드

+ + + """ + +@app.post( + "/collect", + response_model=RestaurantSearchResponse, + summary="음식점 정보 수집", + description=""" + 카카오 로컬 API를 사용하여 지정된 조건의 음식점 정보를 수집합니다. + + **주요 기능:** + - 키워드 및 지역 기반 음식점 검색 + - 페이지네이션 지원 (최대 45페이지) + - JSON 파일 자동 저장 + - 중복 음식점 제거 + + **응답 시간:** 페이지 수에 따라 5초-60초 소요 + """, + responses={ + 200: {"description": "수집 성공", "model": RestaurantSearchResponse}, + 400: {"description": "잘못된 요청", "model": ErrorResponse}, + 401: {"description": "API 키 인증 실패", "model": ErrorResponse}, + 500: {"description": "서버 오류", "model": ErrorResponse} + } +) +async def collect_restaurants(request: RestaurantSearchRequest): + """음식점 정보 수집 API""" + logger.info(f"음식점 수집 요청: {request}") + + try: + async with KakaoRestaurantCollector() as collector: + # 음식점 검색 실행 + result = await collector.search_restaurants( + query=request.query, + region=request.region, + size=request.size, + pages=request.pages + ) + + # 파일 저장 + file_saved = False + file_path = None + + if request.save_to_file: + try: + # 파일명 생성 (쿼리와 지역 포함) + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + safe_query = "".join(c for c in request.query if c.isalnum() or c in (' ', '_')).strip() + safe_region = "".join(c for c in request.region if c.isalnum() or c in (' ', '_')).strip() + filename = f"restaurants_{safe_query}_{safe_region}_{timestamp}.json" + + file_path = save_restaurants_to_file(result, filename) + file_saved = True + + # 기본 파일명으로도 저장 (최신 결과) + default_path = save_restaurants_to_file(result, config.OUTPUT_FILE) + logger.info(f"기본 파일로도 저장: {default_path}") + + except Exception as save_error: + logger.error(f"파일 저장 실패: {save_error}") + file_saved = False + + # 응답 데이터 구성 + response_data = RestaurantSearchResponse( + success=True, + message=f"음식점 정보 수집이 완료되었습니다. (총 {len(result['restaurants'])}개)", + metadata=CollectionMetadata(**result['metadata']), + restaurants=[RestaurantInfo(**r) for r in result['restaurants']], + file_saved=file_saved, + file_path=file_path + ) + + logger.info(f"수집 완료: {len(result['restaurants'])}개 음식점") + return response_data + + except HTTPException: + raise + except Exception as e: + logger.error(f"음식점 수집 실패: {str(e)}") + raise HTTPException( + status_code=500, + detail={ + "success": False, + "error": "COLLECTION_FAILED", + "message": f"음식점 수집 중 오류가 발생했습니다: {str(e)}", + "timestamp": datetime.now().isoformat() + } + ) + +@app.get("/list-files", summary="저장된 파일 목록", description="저장된 음식점 데이터 파일 목록을 반환합니다.") +async def list_files(): + """저장된 파일 목록 조회""" + try: + files = [] + if os.path.exists(config.DATA_DIR): + for filename in os.listdir(config.DATA_DIR): + if filename.endswith('.json'): + file_path = os.path.join(config.DATA_DIR, filename) + file_stat = os.stat(file_path) + + files.append({ + 'filename': filename, + 'size': file_stat.st_size, + 'created': datetime.fromtimestamp(file_stat.st_ctime).isoformat(), + 'modified': datetime.fromtimestamp(file_stat.st_mtime).isoformat(), + 'download_url': f"/download/{filename}" + }) + + files.sort(key=lambda x: x['modified'], reverse=True) + + return { + 'success': True, + 'total_files': len(files), + 'files': files, + 'data_directory': config.DATA_DIR + } + + except Exception as e: + logger.error(f"파일 목록 조회 실패: {e}") + raise HTTPException(status_code=500, detail=f"파일 목록 조회 실패: {str(e)}") + +@app.get("/download/{filename}", summary="파일 다운로드", description="저장된 음식점 데이터 파일을 다운로드합니다.") +async def download_file(filename: str): + """파일 다운로드""" + file_path = os.path.join(config.DATA_DIR, filename) + + if not os.path.exists(file_path): + raise HTTPException(status_code=404, detail="파일을 찾을 수 없습니다.") + + if not filename.endswith('.json'): + raise HTTPException(status_code=400, detail="JSON 파일만 다운로드 가능합니다.") + + return FileResponse( + path=file_path, + filename=filename, + media_type='application/json' + ) + +@app.get("/health", summary="헬스 체크", description="API 서버 상태를 확인합니다.") +async def health_check(): + """헬스 체크""" + return { + "status": "healthy", + "timestamp": datetime.now().isoformat(), + "version": config.APP_VERSION, + "api_key_configured": bool(config.KAKAO_API_KEY), + "data_directory_exists": os.path.exists(config.DATA_DIR), + "message": f"{config.APP_TITLE}이 정상 작동 중입니다." + } + +@app.get("/config", summary="환경 설정 확인", description="현재 적용된 환경 변수 설정을 확인합니다.") +async def get_config(): + """환경 설정 확인""" + return { + "app_info": { + "title": config.APP_TITLE, + "version": config.APP_VERSION, + "description": config.APP_DESCRIPTION + }, + "server_config": { + "host": config.HOST, + "port": config.PORT, + "log_level": config.LOG_LEVEL + }, + "api_config": { + "kakao_api_url": config.KAKAO_API_URL, + "api_key_configured": bool(config.KAKAO_API_KEY), + "api_key_preview": f"{config.KAKAO_API_KEY[:10]}..." if config.KAKAO_API_KEY else None + }, + "search_defaults": { + "default_query": config.DEFAULT_QUERY, + "default_region": config.DEFAULT_REGION, + "default_size": config.DEFAULT_SIZE, + "max_size": config.MAX_SIZE, + "max_pages": config.MAX_PAGES + }, + "file_config": { + "output_file": config.OUTPUT_FILE, + "data_dir": config.DATA_DIR, + "data_dir_exists": os.path.exists(config.DATA_DIR) + }, + "request_config": { + "request_delay": config.REQUEST_DELAY, + "request_timeout": config.REQUEST_TIMEOUT, + "health_check_timeout": config.HEALTH_CHECK_TIMEOUT + }, + "timestamp": datetime.now().isoformat() + } + +# 백그라운드 작업을 위한 예제 (향후 확장용) +@app.post("/collect-background", summary="백그라운드 수집", description="음식점 정보를 백그라운드에서 수집합니다.") +async def collect_restaurants_background(background_tasks: BackgroundTasks, request: RestaurantSearchRequest): + """백그라운드 음식점 수집""" + + async def background_collect(): + try: + logger.info("백그라운드 수집 시작") + async with KakaoRestaurantCollector() as collector: + result = await collector.search_restaurants( + query=request.query, + region=request.region, + size=request.size, + pages=request.pages + ) + + if request.save_to_file: + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + filename = f"restaurants_bg_{timestamp}.json" + save_restaurants_to_file(result, filename) + + logger.info(f"백그라운드 수집 완료: {len(result['restaurants'])}개") + + except Exception as e: + logger.error(f"백그라운드 수집 실패: {e}") + + background_tasks.add_task(background_collect) + + return { + "success": True, + "message": "백그라운드 수집이 시작되었습니다.", + "timestamp": datetime.now().isoformat() + } + +if __name__ == "__main__": + print("🍽️ " + "="*60) + print(f" {config.APP_TITLE} 서버 시작") + print("="*64) + print(f"📊 설정 정보:") + print(f" - API 키: {config.KAKAO_API_KEY[:10]}...") + print(f" - 기본 검색어: {config.DEFAULT_QUERY}") + print(f" - 기본 지역: {config.DEFAULT_REGION}") + print(f" - 데이터 저장 경로: {config.DATA_DIR}") + print() + print(f"📚 문서:") + print(f" - Swagger UI: http://{config.HOST}:{config.PORT}/docs") + print(f" - ReDoc: http://{config.HOST}:{config.PORT}/redoc") + print(f" - 메인 페이지: http://{config.HOST}:{config.PORT}/") + print() + + uvicorn.run( + app, + host=config.HOST, + port=config.PORT, + log_level=config.LOG_LEVEL + ) + diff --git a/restaurant/app/requirements.txt b/restaurant/app/requirements.txt new file mode 100644 index 0000000..0b63a4e --- /dev/null +++ b/restaurant/app/requirements.txt @@ -0,0 +1,6 @@ +fastapi==0.104.1 +uvicorn[standard]==0.24.0 +aiohttp==3.9.1 +pydantic==2.5.0 +python-multipart==0.0.6 +python-dotenv==1.0.0 diff --git a/restaurant/build-base.sh b/restaurant/build-base.sh new file mode 100755 index 0000000..d5643e2 --- /dev/null +++ b/restaurant/build-base.sh @@ -0,0 +1,214 @@ +#!/bin/bash +# build-base.sh - Base Image 빌드 스크립트 + +set -e + +# 변수 설정 +BASE_IMAGE_NAME="restaurant-api-base" +BASE_IMAGE_TAG="${1:-latest}" +ACR_NAME="${2:-acrdigitalgarage03}" +RESOURCE_GROUP="${3:-rg-digitalgarage-03}" + +# ACR URL 자동 구성 +if [ -n "${ACR_NAME}" ]; then + REGISTRY="${ACR_NAME}.azurecr.io" + FULL_BASE_IMAGE_NAME="${REGISTRY}/${BASE_IMAGE_NAME}:${BASE_IMAGE_TAG}" +else + FULL_BASE_IMAGE_NAME="${BASE_IMAGE_NAME}:${BASE_IMAGE_TAG}" +fi + +# 고정된 Dockerfile 경로 +BASE_DOCKERFILE_PATH="deployment/container/Dockerfile-base" +BUILD_CONTEXT="." + +echo "=====================================================" +echo " 카카오 API 음식점 수집 서비스 Base Image 빌드" +echo "=====================================================" +echo "Base 이미지명: ${FULL_BASE_IMAGE_NAME}" +if [ -n "${ACR_NAME}" ]; then + echo "ACR 이름: ${ACR_NAME}" + echo "리소스 그룹: ${RESOURCE_GROUP}" +fi +echo "빌드 시작: $(date)" +echo "" + +# 사용법 표시 함수 +show_usage() { + echo "사용법:" + echo " $0 [BASE_IMAGE_TAG] [ACR_NAME] [RESOURCE_GROUP]" + echo "" + echo "파라미터:" + echo " BASE_IMAGE_TAG: Base 이미지 태그 (기본값: latest)" + echo " ACR_NAME : Azure Container Registry 이름" + echo " RESOURCE_GROUP: Azure 리소스 그룹" + echo "" + echo "예시:" + echo " $0 v1.0.0 # 로컬 빌드만" + echo " $0 v1.0.0 acrdigitalgarage01 rg-digitalgarage-03 # ACR 빌드 + 푸시" +} + +# ACR 로그인 함수 +acr_login() { + local acr_name="$1" + local resource_group="$2" + + echo "🔐 Azure Container Registry 로그인 중..." + + if ! command -v az &> /dev/null; then + echo "❌ Azure CLI (az)가 설치되지 않았습니다." + exit 1 + fi + + if ! command -v jq &> /dev/null; then + echo "❌ jq가 설치되지 않았습니다." + exit 1 + fi + + if ! az account show &> /dev/null; then + echo "❌ Azure에 로그인되지 않았습니다." + echo "로그인 명령: az login" + exit 1 + fi + + local credential_json + credential_json=$(az acr credential show --name "${acr_name}" --resource-group "${resource_group}" 2>/dev/null) + + if [ $? -ne 0 ]; then + echo "❌ ACR credential 조회 실패" + exit 1 + fi + + local username + local password + + username=$(echo "${credential_json}" | jq -r '.username') + password=$(echo "${credential_json}" | jq -r '.passwords[0].value') + + if [ -z "${username}" ] || [ -z "${password}" ] || [ "${username}" == "null" ] || [ "${password}" == "null" ]; then + echo "❌ ACR credential 파싱 실패" + exit 1 + fi + + echo "🔐 Docker 로그인 실행 중..." + echo "${password}" | docker login "${REGISTRY}" -u "${username}" --password-stdin + + if [ $? -eq 0 ]; then + echo "✅ ACR 로그인 성공!" + return 0 + else + echo "❌ ACR 로그인 실패" + exit 1 + fi +} + +# 파라미터 검증 +if [ "$1" == "--help" ] || [ "$1" == "-h" ]; then + show_usage + exit 0 +fi + +if [ -n "${ACR_NAME}" ] && [ -z "${RESOURCE_GROUP}" ]; then + echo "❌ ACR_NAME이 제공된 경우 RESOURCE_GROUP도 필요합니다." + echo "" + show_usage + exit 1 +fi + +# 필수 파일 확인 +echo "📁 필수 파일 확인 중..." + +if [ ! -f "${BASE_DOCKERFILE_PATH}" ]; then + echo "❌ ${BASE_DOCKERFILE_PATH} 파일을 찾을 수 없습니다." + exit 1 +fi + +echo "✅ Base Dockerfile 확인 완료" +echo "📄 Base Dockerfile: ${BASE_DOCKERFILE_PATH}" + +# ACR 로그인 수행 +if [ -n "${ACR_NAME}" ] && [ -n "${RESOURCE_GROUP}" ]; then + echo "" + acr_login "${ACR_NAME}" "${RESOURCE_GROUP}" + echo "" +fi + +# Docker 빌드 +echo "🔨 Base Image 빌드 시작..." +echo "명령어: docker build -t \"${FULL_BASE_IMAGE_NAME}\" -f \"${BASE_DOCKERFILE_PATH}\" \"${BUILD_CONTEXT}\"" + +docker build -t "${FULL_BASE_IMAGE_NAME}" -f "${BASE_DOCKERFILE_PATH}" "${BUILD_CONTEXT}" + +if [ $? -eq 0 ]; then + echo "✅ Base Image 빌드 완료!" + echo "이미지명: ${FULL_BASE_IMAGE_NAME}" + + # 이미지 정보 표시 + echo "" + echo "📊 Base Image 정보:" + docker images "${FULL_BASE_IMAGE_NAME}" --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}\t{{.CreatedAt}}" + + # latest 태그 추가 생성 + if [ "${BASE_IMAGE_TAG}" != "latest" ] && [ -n "${REGISTRY}" ]; then + echo "" + echo "🏷️ latest 태그 생성 중..." + docker tag "${FULL_BASE_IMAGE_NAME}" "${REGISTRY}/${BASE_IMAGE_NAME}:latest" + echo "✅ latest 태그 생성 완료: ${REGISTRY}/${BASE_IMAGE_NAME}:latest" + fi + + # ACR 푸시 + if [ -n "${ACR_NAME}" ]; then + echo "" + echo "🚀 ACR에 Base Image 푸시 중..." + + echo "📤 푸시 중: ${FULL_BASE_IMAGE_NAME}" + docker push "${FULL_BASE_IMAGE_NAME}" + + if [ $? -eq 0 ]; then + echo "✅ Base Image 푸시 성공" + + if [ "${BASE_IMAGE_TAG}" != "latest" ]; then + echo "📤 푸시 중: ${REGISTRY}/${BASE_IMAGE_NAME}:latest" + docker push "${REGISTRY}/${BASE_IMAGE_NAME}:latest" + + if [ $? -eq 0 ]; then + echo "✅ latest 태그 푸시 성공" + fi + fi + else + echo "❌ Base Image 푸시 실패" + exit 1 + fi + fi + + echo "" + echo "🎉 Base Image 빌드 완료!" + echo "" + echo "📋 완료된 작업:" + echo " ✅ Base Image 빌드: ${FULL_BASE_IMAGE_NAME}" + if [ "${BASE_IMAGE_TAG}" != "latest" ] && [ -n "${REGISTRY}" ]; then + echo " ✅ latest 태그: ${REGISTRY}/${BASE_IMAGE_NAME}:latest" + fi + if [ -n "${ACR_NAME}" ]; then + echo " ✅ ACR 푸시 완료" + fi + + echo "" + echo "🧪 테스트 명령어:" + echo " docker run --rm ${FULL_BASE_IMAGE_NAME} python --version" + + echo "" + echo "📝 다음 단계:" + echo " 이제 Service Image를 빌드하세요:" + if [ -n "${ACR_NAME}" ]; then + echo " ./build.sh v1.0.0 ${ACR_NAME} ${RESOURCE_GROUP}" + else + echo " ./build.sh v1.0.0" + fi + +else + echo "❌ Base Image 빌드 실패!" + exit 1 +fi + +echo "" +echo "🏁 Base Image 빌드 프로세스 완료 - $(date)" diff --git a/restaurant/build.sh b/restaurant/build.sh new file mode 100755 index 0000000..1caf3b2 --- /dev/null +++ b/restaurant/build.sh @@ -0,0 +1,251 @@ +#!/bin/bash +# build.sh - Service Image 빌드 스크립트 (Base Image 활용) + +set -e + +# 변수 설정 +IMAGE_NAME="restaurant-api" +IMAGE_TAG="${1:-latest}" +ACR_NAME="${2:-acrdigitalgarage03}" +RESOURCE_GROUP="${3:-rg-digitalgarage-03}" +BASE_IMAGE_TAG="${4:-latest}" + +# ACR URL 자동 구성 +if [ -n "${ACR_NAME}" ]; then + REGISTRY="${ACR_NAME}.azurecr.io" + FULL_IMAGE_NAME="${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}" + BASE_IMAGE="${REGISTRY}/restaurant-api-base:${BASE_IMAGE_TAG}" +else + FULL_IMAGE_NAME="${IMAGE_NAME}:${IMAGE_TAG}" + BASE_IMAGE="restaurant-api-base:${BASE_IMAGE_TAG}" +fi + +# 고정된 Dockerfile 경로 +DOCKERFILE_PATH="deployment/container/Dockerfile" +BUILD_CONTEXT="." + +echo "=====================================================" +echo " 카카오 API 음식점 수집 서비스 Service Image 빌드" +echo "=====================================================" +echo "Service 이미지명: ${FULL_IMAGE_NAME}" +echo "Base 이미지: ${BASE_IMAGE}" +if [ -n "${ACR_NAME}" ]; then + echo "ACR 이름: ${ACR_NAME}" + echo "리소스 그룹: ${RESOURCE_GROUP}" +fi +echo "빌드 시작: $(date)" +echo "" + +# 사용법 표시 함수 +show_usage() { + echo "사용법:" + echo " $0 [IMAGE_TAG] [ACR_NAME] [RESOURCE_GROUP] [BASE_IMAGE_TAG]" + echo "" + echo "파라미터:" + echo " IMAGE_TAG : Service 이미지 태그 (기본값: latest)" + echo " ACR_NAME : Azure Container Registry 이름" + echo " RESOURCE_GROUP: Azure 리소스 그룹" + echo " BASE_IMAGE_TAG: Base 이미지 태그 (기본값: latest)" + echo "" + echo "예시:" + echo " $0 v1.0.0 # 로컬 빌드만" + echo " $0 v1.0.0 acrdigitalgarage01 rg-digitalgarage-03 # ACR 빌드 + 푸시" + echo " $0 v1.0.0 acrdigitalgarage01 rg-digitalgarage-03 v2.0.0 # 특정 Base Image 사용" + echo "" + echo "전제조건:" + echo " Base Image가 먼저 빌드되어 있어야 합니다:" + echo " ./build-base.sh ${BASE_IMAGE_TAG} [ACR_NAME] [RESOURCE_GROUP]" +} + +# ACR 로그인 함수 +acr_login() { + local acr_name="$1" + local resource_group="$2" + + echo "🔐 Azure Container Registry 로그인 중..." + + if ! command -v az &> /dev/null; then + echo "❌ Azure CLI (az)가 설치되지 않았습니다." + exit 1 + fi + + if ! command -v jq &> /dev/null; then + echo "❌ jq가 설치되지 않았습니다." + exit 1 + fi + + if ! az account show &> /dev/null; then + echo "❌ Azure에 로그인되지 않았습니다." + echo "로그인 명령: az login" + exit 1 + fi + + local credential_json + credential_json=$(az acr credential show --name "${acr_name}" --resource-group "${resource_group}" 2>/dev/null) + + if [ $? -ne 0 ]; then + echo "❌ ACR credential 조회 실패" + exit 1 + fi + + local username + local password + + username=$(echo "${credential_json}" | jq -r '.username') + password=$(echo "${credential_json}" | jq -r '.passwords[0].value') + + if [ -z "${username}" ] || [ -z "${password}" ] || [ "${username}" == "null" ] || [ "${password}" == "null" ]; then + echo "❌ ACR credential 파싱 실패" + exit 1 + fi + + echo "🔐 Docker 로그인 실행 중..." + echo "${password}" | docker login "${REGISTRY}" -u "${username}" --password-stdin + + if [ $? -eq 0 ]; then + echo "✅ ACR 로그인 성공!" + return 0 + else + echo "❌ ACR 로그인 실패" + exit 1 + fi +} + +# 파라미터 검증 +if [ "$1" == "--help" ] || [ "$1" == "-h" ]; then + show_usage + exit 0 +fi + +if [ -n "${ACR_NAME}" ] && [ -z "${RESOURCE_GROUP}" ]; then + echo "❌ ACR_NAME이 제공된 경우 RESOURCE_GROUP도 필요합니다." + echo "" + show_usage + exit 1 +fi + +# 필수 파일 확인 +echo "📁 필수 파일 확인 중..." + +if [ ! -f "app/main.py" ]; then + echo "❌ app/main.py 파일을 찾을 수 없습니다." + exit 1 +fi + +if [ ! -f "app/requirements.txt" ]; then + echo "❌ app/requirements.txt 파일을 찾을 수 없습니다." + exit 1 +fi + +if [ ! -f "${DOCKERFILE_PATH}" ]; then + echo "❌ ${DOCKERFILE_PATH} 파일을 찾을 수 없습니다." + exit 1 +fi + +echo "✅ 모든 필수 파일이 확인되었습니다." +echo "📄 Dockerfile: ${DOCKERFILE_PATH}" +echo "🏗️ 빌드 컨텍스트: ${BUILD_CONTEXT}" + +# Base Image 존재 확인 +echo "" +echo "🔍 Base Image 확인 중..." +if docker image inspect "${BASE_IMAGE}" >/dev/null 2>&1; then + echo "✅ Base Image 확인됨: ${BASE_IMAGE}" +else + echo "❌ Base Image를 찾을 수 없습니다: ${BASE_IMAGE}" + echo "" + echo "Base Image를 먼저 빌드하세요:" + if [ -n "${ACR_NAME}" ]; then + echo " ./build-base.sh ${BASE_IMAGE_TAG} ${ACR_NAME} ${RESOURCE_GROUP}" + else + echo " ./build-base.sh ${BASE_IMAGE_TAG}" + fi + exit 1 +fi + +# ACR 로그인 수행 +if [ -n "${ACR_NAME}" ] && [ -n "${RESOURCE_GROUP}" ]; then + echo "" + acr_login "${ACR_NAME}" "${RESOURCE_GROUP}" + echo "" +fi + +# Docker 빌드 +echo "🔨 Service Image 빌드 시작... (빠른 빌드 예상)" +echo "명령어: docker build --build-arg BASE_IMAGE=\"${BASE_IMAGE}\" -t \"${FULL_IMAGE_NAME}\" -f \"${DOCKERFILE_PATH}\" \"${BUILD_CONTEXT}\"" + +docker build --build-arg BASE_IMAGE="${BASE_IMAGE}" -t "${FULL_IMAGE_NAME}" -f "${DOCKERFILE_PATH}" "${BUILD_CONTEXT}" + +if [ $? -eq 0 ]; then + echo "✅ Service Image 빌드 완료!" + echo "이미지명: ${FULL_IMAGE_NAME}" + + # 이미지 정보 표시 + echo "" + echo "📊 Service Image 정보:" + docker images "${FULL_IMAGE_NAME}" --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}\t{{.CreatedAt}}" + + # latest 태그 추가 생성 + if [ "${IMAGE_TAG}" != "latest" ] && [ -n "${REGISTRY}" ]; then + echo "" + echo "🏷️ latest 태그 생성 중..." + docker tag "${FULL_IMAGE_NAME}" "${REGISTRY}/${IMAGE_NAME}:latest" + echo "✅ latest 태그 생성 완료: ${REGISTRY}/${IMAGE_NAME}:latest" + fi + + # ACR 푸시 + if [ -n "${ACR_NAME}" ]; then + echo "" + echo "🚀 ACR에 Service Image 푸시 중..." + + echo "📤 푸시 중: ${FULL_IMAGE_NAME}" + docker push "${FULL_IMAGE_NAME}" + + if [ $? -eq 0 ]; then + echo "✅ Service Image 푸시 성공" + + if [ "${IMAGE_TAG}" != "latest" ]; then + echo "📤 푸시 중: ${REGISTRY}/${IMAGE_NAME}:latest" + docker push "${REGISTRY}/${IMAGE_NAME}:latest" + + if [ $? -eq 0 ]; then + echo "✅ latest 태그 푸시 성공" + fi + fi + else + echo "❌ Service Image 푸시 실패" + exit 1 + fi + fi + + echo "" + echo "🎉 Service Image 빌드 완료!" + echo "" + echo "📋 완료된 작업:" + echo " ✅ Service Image 빌드: ${FULL_IMAGE_NAME}" + echo " ✅ 사용된 Base Image: ${BASE_IMAGE}" + if [ "${IMAGE_TAG}" != "latest" ] && [ -n "${REGISTRY}" ]; then + echo " ✅ latest 태그: ${REGISTRY}/${IMAGE_NAME}:latest" + fi + if [ -n "${ACR_NAME}" ]; then + echo " ✅ ACR 푸시 완료" + fi + + echo "" + echo "🧪 테스트 명령어:" + echo " docker run -p 8000:8000 ${FULL_IMAGE_NAME}" + echo " curl http://localhost:8000/health" + + if [ -n "${ACR_NAME}" ]; then + echo "" + echo "🔍 ACR 이미지 확인:" + echo " az acr repository show-tags --name ${ACR_NAME} --repository ${IMAGE_NAME}" + fi + +else + echo "❌ Service Image 빌드 실패!" + exit 1 +fi + +echo "" +echo "🏁 Service Image 빌드 프로세스 완료 - $(date)" diff --git a/restaurant/create-imagepullsecret.sh b/restaurant/create-imagepullsecret.sh new file mode 100755 index 0000000..56ef6cd --- /dev/null +++ b/restaurant/create-imagepullsecret.sh @@ -0,0 +1,237 @@ +#!/bin/bash +# create-imagepullsecret.sh - ACR Image Pull Secret 생성 스크립트 + +set -e + +# 변수 설정 +ACR_NAME="${1:-acrdigitalgarage03}" +RESOURCE_GROUP="${2:-rg-digitalgarage-03}" +SECRET_NAME="${3:-acr-secret}" + +echo "=====================================================" +echo " ACR Image Pull Secret 생성 (Restaurant API)" +echo "=====================================================" + +# 사용법 표시 함수 +show_usage() { + echo "사용법:" + echo " $0 [ACR_NAME] [RESOURCE_GROUP] [SECRET_NAME]" + echo "" + echo "파라미터:" + echo " ACR_NAME : Azure Container Registry 이름 (필수)" + echo " RESOURCE_GROUP: Azure 리소스 그룹 (필수)" + echo " SECRET_NAME : Secret 이름 (기본값: acr-secret)" + echo "" + echo "예시:" + echo " $0 acrdigitalgarage01 rg-digitalgarage-03" + echo " $0 acrdigitalgarage01 rg-digitalgarage-03 acr-prod-secret" + echo "" +} + +# 파라미터 검증 +if [ "$1" == "--help" ] || [ "$1" == "-h" ]; then + show_usage + exit 0 +fi + +if [ -z "${ACR_NAME}" ] || [ -z "${RESOURCE_GROUP}" ]; then + echo "❌ ACR_NAME과 RESOURCE_GROUP는 필수 파라미터입니다." + echo "" + show_usage + exit 1 +fi + +# 필수 도구 확인 +echo "🔧 필수 도구 확인 중..." + +if ! command -v az &> /dev/null; then + echo "❌ Azure CLI (az)가 설치되지 않았습니다." + echo "설치 방법: https://docs.microsoft.com/en-us/cli/azure/install-azure-cli" + exit 1 +fi + +if ! command -v kubectl &> /dev/null; then + echo "❌ kubectl이 설치되지 않았습니다." + echo "설치 방법: https://kubernetes.io/docs/tasks/tools/" + exit 1 +fi + +if ! command -v jq &> /dev/null; then + echo "❌ jq가 설치되지 않았습니다." + echo "설치 방법: sudo apt-get install jq" + exit 1 +fi + +echo "✅ 필수 도구 확인 완료" + +# Azure 로그인 확인 +echo "" +echo "🔐 Azure 로그인 상태 확인 중..." + +if ! az account show &> /dev/null; then + echo "❌ Azure에 로그인되지 않았습니다." + echo "로그인 명령: az login" + exit 1 +fi + +CURRENT_SUBSCRIPTION=$(az account show --query name -o tsv) +echo "✅ Azure 로그인 확인됨" +echo " 현재 구독: ${CURRENT_SUBSCRIPTION}" + +# Kubernetes 클러스터 연결 확인 +echo "" +echo "☸️ Kubernetes 클러스터 연결 확인 중..." + +if ! kubectl cluster-info &> /dev/null; then + echo "❌ Kubernetes 클러스터에 연결되지 않았습니다." + echo "클러스터 연결 방법:" + echo " az aks get-credentials --resource-group ${RESOURCE_GROUP} --name " + exit 1 +fi + +CURRENT_CONTEXT=$(kubectl config current-context) +echo "✅ Kubernetes 클러스터 연결 확인됨" +echo " 현재 컨텍스트: ${CURRENT_CONTEXT}" + +# ACR 정보 설정 +REGISTRY_URL="${ACR_NAME}.azurecr.io" + +echo "" +echo "📋 설정 정보:" +echo " ACR 이름: ${ACR_NAME}" +echo " 레지스트리 URL: ${REGISTRY_URL}" +echo " 리소스 그룹: ${RESOURCE_GROUP}" +echo " Secret 이름: ${SECRET_NAME}" + +# ACR 존재 확인 +echo "" +echo "🏪 ACR 존재 확인 중..." + +if ! az acr show --name "${ACR_NAME}" --resource-group "${RESOURCE_GROUP}" &> /dev/null; then + echo "❌ ACR을 찾을 수 없습니다." + echo "확인 사항:" + echo " - ACR 이름: ${ACR_NAME}" + echo " - 리소스 그룹: ${RESOURCE_GROUP}" + echo " - ACR이 해당 리소스 그룹에 존재하는지 확인" + exit 1 +fi + +echo "✅ ACR 존재 확인됨: ${ACR_NAME}" + +# ACR credential 조회 +echo "" +echo "🔑 ACR credential 조회 중..." + +credential_json=$(az acr credential show --name "${ACR_NAME}" --resource-group "${RESOURCE_GROUP}" 2>/dev/null) + +if [ $? -ne 0 ]; then + echo "❌ ACR credential 조회 실패" + echo "확인 사항:" + echo " - ACR 이름: ${ACR_NAME}" + echo " - 리소스 그룹: ${RESOURCE_GROUP}" + echo " - ACR에 대한 권한이 있는지 확인" + exit 1 +fi + +# JSON에서 username과 password 추출 +username=$(echo "${credential_json}" | jq -r '.username') +password=$(echo "${credential_json}" | jq -r '.passwords[0].value') + +if [ -z "${username}" ] || [ -z "${password}" ] || [ "${username}" == "null" ] || [ "${password}" == "null" ]; then + echo "❌ ACR credential 파싱 실패" + echo "credential JSON:" + echo "${credential_json}" + exit 1 +fi + +echo "✅ ACR credential 조회 성공" +echo " 사용자명: ${username}" +echo " 비밀번호: ${password:0:10}..." + +# 기존 Secret 확인 및 삭제 +echo "" +echo "🔍 기존 Secret 확인 중..." + +if kubectl get secret "${SECRET_NAME}" &> /dev/null; then + echo "🗑️ 기존 Secret 삭제 중..." + kubectl delete secret "${SECRET_NAME}" + echo "✅ 기존 Secret 삭제 완료" +else + echo "✅ 기존 Secret 없음" +fi + +# Image Pull Secret 생성 +echo "" +echo "🔐 Image Pull Secret 생성 중..." + +kubectl create secret docker-registry "${SECRET_NAME}" \ + --docker-server="${REGISTRY_URL}" \ + --docker-username="${username}" \ + --docker-password="${password}" + +if [ $? -eq 0 ]; then + echo "✅ Image Pull Secret 생성 성공!" +else + echo "❌ Image Pull Secret 생성 실패" + exit 1 +fi + +# Secret 정보 확인 +echo "" +echo "📊 생성된 Secret 정보:" +kubectl describe secret "${SECRET_NAME}" + +echo "" +echo "🧪 Secret 테스트 중..." + +# Secret이 올바르게 생성되었는지 확인 +SECRET_TYPE=$(kubectl get secret "${SECRET_NAME}" -o jsonpath='{.type}') +if [ "${SECRET_TYPE}" = "kubernetes.io/dockerconfigjson" ]; then + echo "✅ Secret 타입 확인됨: ${SECRET_TYPE}" +else + echo "❌ Secret 타입 불일치: ${SECRET_TYPE}" +fi + +# Registry URL 확인 +SECRET_REGISTRY=$(kubectl get secret "${SECRET_NAME}" -o jsonpath='{.data.\.dockerconfigjson}' | base64 -d | jq -r ".auths | keys[0]") +if [ "${SECRET_REGISTRY}" = "${REGISTRY_URL}" ]; then + echo "✅ Registry URL 확인됨: ${SECRET_REGISTRY}" +else + echo "❌ Registry URL 불일치: ${SECRET_REGISTRY}" +fi + +echo "" +echo "🎉 ACR Image Pull Secret 생성 완료!" +echo "" +echo "📋 사용 방법:" +echo "" +echo "1. Deployment에서 imagePullSecrets 사용:" +echo " spec:" +echo " template:" +echo " spec:" +echo " imagePullSecrets:" +echo " - name: ${SECRET_NAME}" +echo "" +echo "2. ServiceAccount에 Secret 연결:" +echo " kubectl patch serviceaccount default -p '{\"imagePullSecrets\": [{\"name\": \"${SECRET_NAME}\"}]}'" +echo "" +echo "3. Secret 확인:" +echo " kubectl get secret ${SECRET_NAME}" +echo " kubectl describe secret ${SECRET_NAME}" +echo "" +echo "4. Secret 삭제 (필요시):" +echo " kubectl delete secret ${SECRET_NAME}" +echo "" +echo "🔧 다음 단계:" +echo "1. Deployment 매니페스트에 imagePullSecrets 추가" +echo "2. kubectl apply -f deployment/manifests/deployment.yaml" +echo "3. Pod 상태 확인: kubectl get pods" +echo "" +echo "💡 문제 해결:" +echo "- ErrImagePull 오류 시: Secret 이름과 레지스트리 URL 확인" +echo "- 권한 오류 시: ACR에 대한 적절한 권한 확인" +echo "- 네트워크 오류 시: 클러스터에서 ACR로의 네트워크 연결 확인" + +echo "" +echo "✅ Image Pull Secret 설정이 완료되었습니다!" + diff --git a/restaurant/deployment/container/Dockerfile b/restaurant/deployment/container/Dockerfile new file mode 100644 index 0000000..5eb4255 --- /dev/null +++ b/restaurant/deployment/container/Dockerfile @@ -0,0 +1,46 @@ +# deployment/container/Dockerfile +# Restaurant Collection Service Image +ARG BASE_IMAGE=restaurant-api-base:latest +FROM ${BASE_IMAGE} + +# 메타데이터 +LABEL maintainer="admin@example.com" +LABEL version="1.0.0" +LABEL description="카카오 API 기반 음식점 수집 서비스" + +# root로 전환 (패키지 설치용) +USER root + +# 환경 변수 설정 +ENV HOME=/home/appuser \ + PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 + +# Python 의존성 파일 복사 및 설치 +COPY app/requirements.txt /app/ +RUN pip install --no-cache-dir -r /app/requirements.txt + +# 애플리케이션 소스 복사 +COPY app/main.py /app/ + +# 데이터 디렉토리 생성 및 권한 설정 +RUN mkdir -p /app/data \ + && chown -R appuser:appuser /app \ + && chmod -R 755 /app + +# 비root 사용자로 전환 +USER appuser + +# 작업 디렉토리 설정 +WORKDIR /app + +# 포트 노출 +EXPOSE 18000 + +# 헬스체크 +HEALTHCHECK --interval=30s --timeout=15s --start-period=30s --retries=3 \ + CMD curl -f http://localhost:18000/health || exit 1 + +# 애플리케이션 실행 +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "18000", "--log-level", "info"] + diff --git a/restaurant/deployment/container/Dockerfile-base b/restaurant/deployment/container/Dockerfile-base new file mode 100644 index 0000000..d7c0ee4 --- /dev/null +++ b/restaurant/deployment/container/Dockerfile-base @@ -0,0 +1,37 @@ +# deployment/container/Dockerfile-base +FROM python:3.11-slim + +# 메타데이터 +LABEL maintainer="admin@example.com" +LABEL description="카카오 API 기반 음식점 수집 서비스 - Base Image" +LABEL version="base-1.0.0" + +# 환경 변수 설정 +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + DEBIAN_FRONTEND=noninteractive + +# 필수 패키지 설치 +RUN apt-get update && apt-get install -y \ + curl \ + wget \ + ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +# 비root 사용자 생성 +RUN groupadd -r appuser && useradd -r -g appuser -d /home/appuser -s /bin/bash appuser \ + && mkdir -p /home/appuser \ + && chown -R appuser:appuser /home/appuser + +# 작업 디렉토리 생성 +WORKDIR /app +RUN chown appuser:appuser /app + +# pip 업그레이드 +RUN pip install --no-cache-dir --upgrade pip + +# 포트 노출 +EXPOSE 8000 + +# 기본 명령어 (오버라이드 가능) +CMD ["python", "--version"] diff --git a/restaurant/deployment/manifest/configmap.yaml b/restaurant/deployment/manifest/configmap.yaml new file mode 100644 index 0000000..a8d8015 --- /dev/null +++ b/restaurant/deployment/manifest/configmap.yaml @@ -0,0 +1,34 @@ +# deployment/manifests/configmap.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: restaurant-api-config +data: + # 애플리케이션 설정 + APP_TITLE: "카카오 API 기반 음식점 수집 서비스" + APP_VERSION: "1.0.0" + APP_DESCRIPTION: "카카오 로컬 API를 활용한 음식점 정보 수집 시스템" + + # 서버 설정 + HOST: "0.0.0.0" + PORT: "18000" + LOG_LEVEL: "info" + + # 카카오 API 설정 + KAKAO_API_URL: "https://dapi.kakao.com/v2/local/search/keyword.json" + + # 검색 기본값 + DEFAULT_QUERY: "음식점" + DEFAULT_REGION: "서울" + DEFAULT_SIZE: "15" + MAX_SIZE: "15" + MAX_PAGES: "45" + + # 파일 설정 + OUTPUT_FILE: "restaurant.json" + DATA_DIR: "/app/data" + + # 요청 제한 설정 + REQUEST_DELAY: "0.1" + REQUEST_TIMEOUT: "30" + HEALTH_CHECK_TIMEOUT: "10" diff --git a/restaurant/deployment/manifest/deployment.yaml b/restaurant/deployment/manifest/deployment.yaml new file mode 100644 index 0000000..21d119e --- /dev/null +++ b/restaurant/deployment/manifest/deployment.yaml @@ -0,0 +1,81 @@ +# deployment/manifests/deployment.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: restaurant-api + labels: + app: restaurant-api + version: v1 +spec: + replicas: 1 + selector: + matchLabels: + app: restaurant-api + template: + metadata: + labels: + app: restaurant-api + version: v1 + spec: + imagePullSecrets: + - name: acr-secret + containers: + - name: api + image: acrdigitalgarage03.azurecr.io/restaurant-api:latest + imagePullPolicy: Always + ports: + - containerPort: 18000 + name: http + + # ConfigMap 환경 변수 + envFrom: + - configMapRef: + name: restaurant-api-config + + # Secret 환경 변수 + env: + - name: KAKAO_API_KEY + valueFrom: + secretKeyRef: + name: restaurant-api-secret + key: KAKAO_API_KEY + + # 리소스 제한 + resources: + requests: + memory: "512Mi" + cpu: "250m" + limits: + memory: "1Gi" + cpu: "500m" + + # 헬스 체크 + livenessProbe: + httpGet: + path: /health + port: 18000 + initialDelaySeconds: 30 + periodSeconds: 30 + timeoutSeconds: 10 + failureThreshold: 3 + + readinessProbe: + httpGet: + path: /health + port: 18000 + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + + # 볼륨 마운트 (데이터 저장용) + volumeMounts: + - name: data-volume + mountPath: /app/data + + # 볼륨 정의 + volumes: + - name: data-volume + emptyDir: {} + + restartPolicy: Always diff --git a/restaurant/deployment/manifest/ingress.yaml b/restaurant/deployment/manifest/ingress.yaml new file mode 100644 index 0000000..11765ed --- /dev/null +++ b/restaurant/deployment/manifest/ingress.yaml @@ -0,0 +1,38 @@ +# deployment/manifests/ingress.yaml +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: restaurant-api-ingress + annotations: + nginx.ingress.kubernetes.io/rewrite-target: / + nginx.ingress.kubernetes.io/ssl-redirect: "false" + nginx.ingress.kubernetes.io/proxy-body-size: "10m" + # 타임아웃 설정 (API 수집 시간 고려) + nginx.ingress.kubernetes.io/proxy-read-timeout: "300" + nginx.ingress.kubernetes.io/proxy-send-timeout: "300" + nginx.ingress.kubernetes.io/client-body-timeout: "300" + nginx.ingress.kubernetes.io/proxy-connect-timeout: "60" + # CORS 설정 + nginx.ingress.kubernetes.io/enable-cors: "true" + nginx.ingress.kubernetes.io/cors-allow-origin: "*" + nginx.ingress.kubernetes.io/cors-allow-methods: "GET, POST, OPTIONS" + nginx.ingress.kubernetes.io/cors-allow-headers: "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization" +spec: + ingressClassName: nginx + rules: + # 환경에 맞게 호스트명 수정 필요 + - host: restaurant-api.20.249.191.180.nip.io + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: restaurant-api-service + port: + number: 80 + # TLS 설정 (HTTPS 필요시 주석 해제) + # tls: + # - hosts: + # - restaurant-api.example.com + # secretName: restaurant-api-tls diff --git a/restaurant/deployment/manifest/secret.yaml b/restaurant/deployment/manifest/secret.yaml new file mode 100644 index 0000000..b8faf24 --- /dev/null +++ b/restaurant/deployment/manifest/secret.yaml @@ -0,0 +1,10 @@ +# deployment/manifests/secret.yaml +apiVersion: v1 +kind: Secret +metadata: + name: restaurant-api-secret +type: Opaque +data: + # 카카오 API 키 (Base64 인코딩 필요) + # echo -n "5cdc24407edbf8544f3954cfaa4650c6" | base64 + KAKAO_API_KEY: NWNkYzI0NDA3ZWRiZjg1NDRmMzk1NGNmYWE0NjUwYzY= diff --git a/restaurant/deployment/manifest/service.yaml b/restaurant/deployment/manifest/service.yaml new file mode 100644 index 0000000..017694d --- /dev/null +++ b/restaurant/deployment/manifest/service.yaml @@ -0,0 +1,16 @@ +# deployment/manifests/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: restaurant-api-service + labels: + app: restaurant-api +spec: + type: ClusterIP + ports: + - port: 80 + targetPort: 18000 + protocol: TCP + name: http + selector: + app: restaurant-api diff --git a/restaurant/setup.sh b/restaurant/setup.sh new file mode 100755 index 0000000..6f4250e --- /dev/null +++ b/restaurant/setup.sh @@ -0,0 +1,212 @@ +#!/bin/bash +# setup.sh - Restaurant API 환경 설정 스크립트 + +set -e + +echo "🍽️ Restaurant API 환경 설정 시작..." + +# 시스템 정보 확인 +echo "📊 시스템 정보:" +echo " - OS: $(lsb_release -d | cut -f2)" +echo " - Python: $(python3 --version 2>/dev/null || echo 'Python3 미설치')" +echo " - pip: $(pip3 --version 2>/dev/null || echo 'pip3 미설치')" + +# 필수 패키지 설치 +echo "" +echo "📦 필수 패키지 설치 중..." +sudo apt update +sudo apt install -y python3-pip python3-venv curl wget jq + +# Python 버전 확인 +PYTHON_VERSION=$(python3 -c "import sys; print('.'.join(map(str, sys.version_info[:2])))") +echo "📍 Python 버전: ${PYTHON_VERSION}" + +if [ "$(echo "${PYTHON_VERSION} < 3.8" | bc)" -eq 1 ]; then + echo "⚠️ Python 3.8 이상이 권장됩니다." +fi + +# Python 가상환경 설정 +echo "" +echo "🐍 Python 가상환경 설정 중..." +if [ ! -d "venv" ]; then + python3 -m venv venv + echo "✅ 가상환경 생성 완료" +else + echo "✅ 기존 가상환경 발견" +fi + +# 가상환경 활성화 및 라이브러리 설치 +echo "📚 Python 라이브러리 설치 중..." +source venv/bin/activate + +# pip 업그레이드 +pip install --upgrade pip + +# 필요한 라이브러리 설치 +if [ -f "app/requirements.txt" ]; then + pip install -r app/requirements.txt + echo "✅ requirements.txt에서 라이브러리 설치 완료" +else + echo "⚠️ app/requirements.txt 파일을 찾을 수 없습니다." + echo "🔧 기본 라이브러리들을 직접 설치합니다..." + pip install fastapi uvicorn aiohttp pydantic python-multipart python-dotenv +fi + +# 데이터 디렉토리 생성 +echo "" +echo "📁 데이터 디렉토리 설정 중..." +mkdir -p data +chmod 755 data +echo "✅ 데이터 디렉토리 생성: $(pwd)/data" + +# 환경변수 파일 생성 (예시) +echo "" +echo "⚙️ 환경변수 파일 생성 중..." +cat > .env << EOF +# 카카오 API 설정 +KAKAO_API_KEY=5cdc24407edbf8544f3954cfaa4650c6 + +# 서버 설정 +HOST=0.0.0.0 +PORT=18000 +LOG_LEVEL=info + +# 기본 검색 설정 +DEFAULT_QUERY=음식점 +DEFAULT_REGION=서울 +DEFAULT_SIZE=15 +MAX_PAGES=10 + +# 파일 설정 +OUTPUT_FILE=restaurant.json +DATA_DIR=./data + +# 요청 설정 +REQUEST_DELAY=0.1 +REQUEST_TIMEOUT=30 +EOF + echo "✅ .env 파일 생성 완료" + +# 네트워크 연결 테스트 +echo "" +echo "🌐 네트워크 연결 테스트 중..." + +# 카카오 API 연결 테스트 +if curl -s --connect-timeout 5 "https://dapi.kakao.com" > /dev/null; then + echo "✅ 카카오 API 서버 연결 가능" +else + echo "❌ 카카오 API 서버 연결 실패" + echo " 인터넷 연결 상태를 확인해주세요." +fi + +# API 키 유효성 간단 테스트 +echo "" +echo "🔑 API 키 유효성 테스트 중..." +API_KEY="5cdc24407edbf8544f3954cfaa4650c6" +TEST_RESPONSE=$(curl -s -w "%{http_code}" -o /dev/null \ + -H "Authorization: KakaoAK ${API_KEY}" \ + --data-urlencode "query=카카오프렌즈&size=1" \ + "https://dapi.kakao.com/v2/local/search/keyword.json") + +if [ "${TEST_RESPONSE}" = "200" ]; then + echo "✅ API 키 유효성 확인 완료" +elif [ "${TEST_RESPONSE}" = "401" ]; then + echo "❌ API 키 인증 실패" + echo " .env 파일의 KAKAO_API_KEY를 확인해주세요." +elif [ "${TEST_RESPONSE}" = "000" ]; then + echo "⚠️ 네트워크 연결 문제로 API 키 테스트 불가" +else + echo "⚠️ API 키 테스트 실패 (HTTP ${TEST_RESPONSE})" +fi + +# .env 파일 로딩 테스트 +echo "" +echo "🔧 .env 파일 로딩 테스트 중..." +python3 -c " +from dotenv import load_dotenv +import os +load_dotenv() +port = os.getenv('PORT', '8000') +data_dir = os.getenv('DATA_DIR', './data') +print(f'✅ .env 파일 로딩 성공') +print(f' - PORT: {port}') +print(f' - DATA_DIR: {data_dir}') +" 2>/dev/null && echo "✅ python-dotenv 동작 확인" || echo "❌ python-dotenv 설치 필요: pip install python-dotenv" + +# Docker 설치 확인 (선택사항) +echo "" +echo "🐳 Docker 설치 확인 중..." +if command -v docker &> /dev/null; then + DOCKER_VERSION=$(docker --version) + echo "✅ Docker 설치됨: ${DOCKER_VERSION}" + + # Docker 권한 확인 + if docker ps &> /dev/null; then + echo "✅ Docker 권한 확인됨" + else + echo "⚠️ Docker 권한 없음. 다음 명령어로 권한을 추가하세요:" + echo " sudo usermod -aG docker $USER" + echo " newgrp docker" + fi +else + echo "⚠️ Docker가 설치되지 않음 (컨테이너 배포 시 필요)" + echo " 설치 방법: https://docs.docker.com/engine/install/ubuntu/" +fi + +# 방화벽 설정 확인 +echo "" +echo "🔥 방화벽 설정 확인 중..." +if command -v ufw &> /dev/null; then + UFW_STATUS=$(sudo ufw status | head -n1 | awk '{print $2}') + if [ "${UFW_STATUS}" = "active" ]; then + echo "🔥 UFW 방화벽 활성화됨" + if sudo ufw status | grep -q "18000"; then + echo "✅ 포트 18000 방화벽 규칙 확인됨" + else + echo "⚠️ 포트 18000 방화벽 규칙 없음" + echo " 다음 명령어로 포트를 열어주세요:" + echo " sudo ufw allow 18000" + fi + else + echo "✅ UFW 방화벽 비활성화됨" + fi +else + echo "✅ UFW 방화벽 미설치" +fi + +echo "" +echo "🎉 환경 설정 완료!" +echo "" +echo "📋 다음 단계:" +echo "1. 가상환경 활성화:" +echo " source venv/bin/activate" +echo "" +echo "2. 애플리케이션 실행:" +echo " python app/main.py" +echo " 또는" +echo " uvicorn app.main:app --host 0.0.0.0 --port 18000 --reload" +echo "" +echo "3. 웹 브라우저에서 접속:" +echo " http://localhost:18000 (메인 페이지)" +echo " http://localhost:18000/docs (Swagger UI)" +echo " http://localhost:18000/health (헬스체크)" +echo "" +echo "4. API 테스트:" +echo " curl -X POST \"http://localhost:18000/collect\" \\" +echo " -H \"Content-Type: application/json\" \\" +echo " -d '{\"query\":\"치킨\",\"region\":\"서울\",\"pages\":2}'" +echo "" +echo "💡 문제 발생 시:" +echo " - 로그 확인: tail -f 로그파일" +echo " - 환경변수 확인: cat .env" +echo " - .env 로딩 확인: python -c \"from dotenv import load_dotenv; load_dotenv(); import os; print(os.getenv('PORT'))\"" +echo " - 네트워크 확인: curl https://dapi.kakao.com" +echo "" +echo "🔧 설정 파일 위치:" +echo " - 환경변수: $(pwd)/.env" +echo " - 데이터 저장: $(pwd)/data/" +echo " - 애플리케이션: $(pwd)/app/" + +deactivate 2>/dev/null || true +echo "" +echo "✅ 환경 설정이 완료되었습니다!" \ No newline at end of file diff --git a/review/README.md b/review/README.md new file mode 100644 index 0000000..efaca99 --- /dev/null +++ b/review/README.md @@ -0,0 +1,480 @@ +# 카카오맵 리뷰 분석 서비스 (교육용) + +AI 기반 리뷰 분석을 통해 소상공인의 고객 피드백 관리를 지원하는 RESTful API 서비스입니다. + +## ⚠️ 중요 법적 경고사항 + +**이 서비스는 교육 목적으로만 제작되었습니다.** + +### 법적 위험 +- 카카오 이용약관 위반 → 계정 정지, 법적 조치 +- 개인정보보호법(PIPA) 위반 → 과태료 최대 3억원 +- 저작권 및 데이터베이스권 침해 → 손해배상 +- 업무방해죄 → 5년 이하 징역 또는 1천500만원 이하 벌금 + +### 합법적 대안 +- **카카오 공식 API 활용** ([developers.kakao.com](https://developers.kakao.com)) +- 점주 대상 자체 가게 관리 서비스 개발 +- 사용자 동의 기반 데이터 수집 앱 +- 카카오와 정식 파트너십 체결 + +**실제 서비스 사용을 금지합니다!** + +--- + +## 📋 프로젝트 개요 + +### 주요 기능 +- 🔍 **카카오맵 리뷰 분석**: HTML 파싱 및 Selenium 기반 동적 수집 +- 🤖 **AI 기반 피드백**: Claude API 연동을 통한 맞춤형 개선 방안 제시 +- 📊 **감정 분석**: 긍정/부정/중립 감정 분류 및 통계 +- 💾 **Vector DB 연동**: 유사 케이스 검색 및 템플릿 활용 +- 🚀 **RESTful API 제공**: FastAPI 기반의 완전한 API 서비스 +- 📚 **Swagger UI 지원**: 자동 생성되는 API 문서 +- ☸️ **Kubernetes 배포**: 완전한 컨테이너 오케스트레이션 지원 +- 🔧 **환경변수 설정**: ConfigMap과 Secret을 통한 유연한 설정 관리 + +### 기술 스택 +- **Backend**: Python 3.11, FastAPI, aiohttp +- **Web Scraping**: Selenium, BeautifulSoup4, Chrome WebDriver +- **AI/ML**: Claude AI API, Vector DB (Chroma) +- **Database**: PostgreSQL (향후), Vector DB +- **Container**: Docker, Multi-stage build +- **Orchestration**: Kubernetes (AKS) +- **Registry**: Azure Container Registry (ACR) +- **Documentation**: Swagger UI, ReDoc + +## 🏗️ 프로젝트 구조 + +``` +review-api/review/ +├── app/ # 애플리케이션 소스 +│ ├── main.py # 메인 애플리케이션 +│ └── requirements.txt # Python 의존성 +├── deployment/ # 배포 관련 파일 +│ ├── container/ # 컨테이너 이미지 빌드 +│ │ ├── Dockerfile # 서비스 이미지 빌드 +│ │ └── Dockerfile-base # 베이스 이미지 빌드 +│ └── manifests/ # Kubernetes 매니페스트 +│ ├── configmap.yaml # 환경 설정 +│ ├── secret.yaml # 민감 정보 (API 키) +│ ├── deployment.yaml # 애플리케이션 배포 +│ ├── service.yaml # 서비스 노출 +│ └── ingress.yaml # 외부 접근 +├── build-base.sh # 베이스 이미지 빌드 스크립트 +├── build.sh # 서비스 이미지 빌드 스크립트 +├── create-imagepullsecret.sh # ACR 인증 설정 스크립트 +├── setup.sh # 로컬 환경 설정 스크립트 +└── README.md # 프로젝트 문서 +``` + +## 🚀 빠른 시작 + +### 1. 로컬 개발 환경 설정 + +```bash +# 저장소 클론 (review-api/review 디렉토리로 이동) +cd review-api/review + +# 환경 설정 스크립트 실행 +chmod +x setup.sh +./setup.sh + +# 가상환경 활성화 +source venv/bin/activate + +# 애플리케이션 실행 +python app/main.py +``` + +### 2. 로컬 웹 브라우저 접속 + +```bash +# 애플리케이션이 정상 실행된 후 아래 URL로 접속 +``` + +- **메인 페이지**: http://localhost:19000 +- **Swagger UI**: http://localhost:19000/docs +- **ReDoc**: http://localhost:19000/redoc +- **헬스체크**: http://localhost:19000/health +- **진단 정보**: http://localhost:19000/diagnostic +- **법적 경고**: http://localhost:19000/legal-warning + +### 3. API 테스트 (교육용) + +```bash +# 기본 리뷰 분석 (교육용 - 실제 사용 금지) +curl -X POST "http://localhost:19000/analyze" \ + -H "Content-Type: application/json" \ + -d '{ + "store_id": "501745730", + "days_limit": 7, + "max_time": 300 + }' + +# 환경 설정 확인 +curl "http://localhost:19000/config" + +# 법적 경고사항 확인 +curl "http://localhost:19000/legal-warning" +``` + +## 🐳 Docker 컨테이너 실행 + +### 베이스 이미지 빌드 + +```bash +# ACR에 베이스 이미지 빌드 및 푸시 +./build-base.sh latest acrdigitalgarage03 rg-digitalgarage-03 +``` + +### 서비스 이미지 빌드 + +```bash +# ACR에 서비스 이미지 빌드 및 푸시 +./build.sh latest acrdigitalgarage03 rg-digitalgarage-03 +``` + +### 로컬 Docker 실행 + +```bash +# 컨테이너 실행 +docker run -p 19000:19000 \ + -e APP_TITLE="카카오맵 리뷰 분석 API" \ + -e PORT=19000 \ + kakao-review-api:latest + +# 백그라운드 실행 +docker run -d -p 19000:19000 \ + --name kakao-review-api \ + -e PORT=19000 \ + kakao-review-api:latest +``` + +## ☸️ Kubernetes 배포 + +### 1. ACR Image Pull Secret 생성 + +```bash +# Image Pull Secret 생성 +./create-imagepullsecret.sh acrdigitalgarage03 rg-digitalgarage-03 +``` + +### 2. Kubernetes 리소스 배포 + +```bash +# ConfigMap 및 Secret 적용 +kubectl apply -f deployment/manifests/configmap.yaml +kubectl apply -f deployment/manifests/secret.yaml + +# 애플리케이션 배포 +kubectl apply -f deployment/manifests/deployment.yaml +kubectl apply -f deployment/manifests/service.yaml +kubectl apply -f deployment/manifests/ingress.yaml +``` + +### 3. 배포 상태 확인 + +```bash +# Pod 상태 확인 +kubectl get pods -l app=kakao-review-api + +# 서비스 상태 확인 +kubectl get svc kakao-review-api-service + +# Ingress 상태 확인 +kubectl get ingress kakao-review-api-ingress + +# 로그 확인 +kubectl logs -l app=kakao-review-api -f +``` + +### 4. 🌐 외부 브라우저에서 접속하기 + +#### Ingress 주소 확인 방법 + +```bash +# 1. Ingress 설정된 호스트 확인 +kubectl get ingress kakao-review-api-ingress -o jsonpath='{.spec.rules[0].host}' + +# 2. Ingress External IP 확인 (LoadBalancer 타입인 경우) +kubectl get ingress kakao-review-api-ingress + +# 3. Ingress Controller의 External IP 확인 +kubectl get svc -n ingress-nginx ingress-nginx-controller + +# 4. 현재 설정된 ingress 주소 확인 +INGRESS_HOST=$(kubectl get ingress kakao-review-api-ingress -o jsonpath='{.spec.rules[0].host}') +echo "🌐 Review API URL: http://${INGRESS_HOST}" +``` + +#### 브라우저 접속 주소 + +현재 설정된 주소로 접속하세요: + +```bash +# 현재 설정된 기본 주소 (환경에 따라 다를 수 있음) +INGRESS_URL="http://kakao-review-api.20.249.191.180.nip.io" +echo "브라우저에서 접속: ${INGRESS_URL}" +``` + +**주요 접속 페이지:** +- **🏠 메인 페이지**: http://kakao-review-api.20.249.191.180.nip.io +- **📖 Swagger UI**: http://kakao-review-api.20.249.191.180.nip.io/docs +- **📄 ReDoc**: http://kakao-review-api.20.249.191.180.nip.io/redoc +- **❤️ 헬스체크**: http://kakao-review-api.20.249.191.180.nip.io/health +- **🔧 진단 정보**: http://kakao-review-api.20.249.191.180.nip.io/diagnostic +- **⚠️ 법적 경고**: http://kakao-review-api.20.249.191.180.nip.io/legal-warning + +#### 접속 테스트 + +```bash +# API 접속 테스트 +curl "http://kakao-review-api.20.249.191.180.nip.io/health" + +# 설정 정보 확인 +curl "http://kakao-review-api.20.249.191.180.nip.io/config" + +# Swagger UI 접속 확인 +curl -I "http://kakao-review-api.20.249.191.180.nip.io/docs" + +# 법적 경고 확인 +curl "http://kakao-review-api.20.249.191.180.nip.io/legal-warning" +``` + +## ⚙️ 환경 설정 + +### 환경 변수 + +| 변수명 | 기본값 | 설명 | +|--------|--------|------| +| `APP_TITLE` | `카카오맵 리뷰 분석 API` | 애플리케이션 제목 | +| `APP_VERSION` | `1.0.1` | 애플리케이션 버전 | +| `HOST` | `0.0.0.0` | 서버 호스트 | +| `PORT` | `19000` | 서버 포트 | +| `DEFAULT_MAX_TIME` | `300` | 기본 최대 스크롤 시간(초) | +| `DEFAULT_DAYS_LIMIT` | `60` | 기본 날짜 제한(일) | +| `MAX_DAYS_LIMIT` | `365` | 최대 날짜 제한(일) | +| `CHROME_OPTIONS` | `--headless...` | Chrome 브라우저 옵션 | +| `LEGAL_WARNING_ENABLED` | `true` | 법적 경고 표시 여부 | +| `CONTACT_EMAIL` | `admin@example.com` | 연락처 이메일 | + +### Chrome 및 Selenium 설정 + +현재 서비스는 Chrome WebDriver를 사용합니다: + +```bash +# Chrome 설치 확인 +google-chrome --version + +# ChromeDriver 설치 확인 +chromedriver --version + +# Selenium 테스트 +python -c "from selenium import webdriver; print('Selenium 설치됨')" +``` + +## 📊 API 엔드포인트 + +### 주요 엔드포인트 + +| Method | Endpoint | 설명 | +|--------|----------|------| +| `GET` | `/` | 메인 페이지 | +| `GET` | `/docs` | Swagger UI 문서 | +| `GET` | `/health` | 헬스체크 | +| `GET` | `/diagnostic` | 시스템 진단 정보 | +| `POST` | `/analyze` | 리뷰 분석 수행 (교육용) | +| `GET` | `/config` | 환경 설정 확인 | +| `GET` | `/legal-warning` | 법적 경고사항 | + +### 리뷰 분석 API 예시 (교육용) + +```json +POST /analyze +{ + "store_id": "501745730", + "days_limit": 7, + "max_time": 300 +} +``` + +**응답:** +```json +{ + "success": true, + "message": "분석이 성공적으로 완료되었습니다.", + "store_info": { + "id": "501745730", + "name": "맛있는 식당", + "category": "한식", + "rating": "4.2", + "review_count": "1,234" + }, + "reviews": [...], + "analysis_date": "2024-06-12T10:30:00", + "total_reviews": 25, + "execution_time": 45.2 +} +``` + +## 🔧 개발 및 확장 + +### 로컬 개발 + +```bash +# 개발 모드로 실행 (자동 재시작) +uvicorn app.main:app --host 0.0.0.0 --port 19000 --reload + +# 의존성 추가 +pip install 새패키지명 +pip freeze > app/requirements.txt + +# 코드 포맷팅 +black app/main.py +flake8 app/main.py +``` + +### Ingress 호스트 변경 + +현재 환경에 맞게 Ingress 호스트를 변경하려면: + +```bash +# 1. 현재 External IP 확인 +kubectl get svc -n ingress-nginx ingress-nginx-controller + +# 2. deployment/manifests/ingress.yaml 파일에서 host 수정 +# 예: kakao-review-api.{YOUR_EXTERNAL_IP}.nip.io + +# 3. 변경사항 적용 +kubectl apply -f deployment/manifests/ingress.yaml + +# 4. 새로운 주소 확인 +kubectl get ingress kakao-review-api-ingress +``` + +## 🐛 문제 해결 + +### 일반적인 문제 + +**1. Chrome WebDriver 관련 문제** +```bash +# Chrome 설치 상태 확인 +docker run --rm kakao-review-api:latest google-chrome --version + +# ChromeDriver 버전 확인 +docker run --rm kakao-review-api:latest chromedriver --version + +# 헤드리스 모드 테스트 +curl -X POST "http://localhost:19000/analyze" \ + -H "Content-Type: application/json" \ + -d '{"store_id": "test", "days_limit": 1, "max_time": 30}' +``` + +**2. Kubernetes 배포 실패** +```bash +# Pod 로그 확인 (Chrome 에러 확인) +kubectl logs -l app=kakao-review-api + +# ConfigMap 확인 +kubectl get configmap kakao-review-api-config -o yaml + +# 리소스 사용량 확인 (Chrome이 메모리를 많이 사용함) +kubectl top pods -l app=kakao-review-api +``` + +**3. Ingress 접속 실패** +```bash +# Ingress Controller 상태 확인 +kubectl get pods -n ingress-nginx + +# Ingress 규칙 확인 +kubectl describe ingress kakao-review-api-ingress + +# Service 연결 확인 +kubectl get endpoints kakao-review-api-service + +# 타임아웃 설정 확인 (Chrome 분석은 시간이 오래 걸림) +kubectl get ingress kakao-review-api-ingress -o yaml | grep timeout +``` + +**4. 포트 관련 문제** +- 로컬 개발: 19000번 포트 사용 +- Docker 컨테이너: 19000번 포트로 실행 +- Kubernetes: 19000번 포트로 실행 (Service에서 80번으로 노출, Ingress를 통해 외부 접근) + +**5. 법적 문제 회피** +```bash +# 법적 경고 항상 확인 +curl "http://kakao-review-api.20.249.191.180.nip.io/legal-warning" + +# 교육용임을 명시하는 환경변수 설정 +export LEGAL_WARNING_ENABLED=true +export EDUCATIONAL_USE_ONLY=true +``` + +## 🎯 성능 최적화 + +### Chrome 최적화 설정 + +```yaml +# deployment.yaml에서 Chrome 최적화를 위한 리소스 설정 +resources: + requests: + memory: "1Gi" # Chrome은 메모리를 많이 사용 + cpu: "500m" + limits: + memory: "2Gi" # Chrome 분석을 위한 충분한 메모리 + cpu: "1000m" +``` + +### 타임아웃 설정 + +```yaml +# ingress.yaml에서 긴 분석 시간을 고려한 타임아웃 +nginx.ingress.kubernetes.io/proxy-read-timeout: "1800" # 30분 +nginx.ingress.kubernetes.io/proxy-send-timeout: "1800" # 30분 +``` + +## 📈 향후 확장 계획 + +- [ ] **Claude AI 연동**: 리뷰 분석 및 개선 방안 자동 생성 +- [ ] **Vector DB 구축**: 유사 케이스 검색 및 템플릿 관리 +- [ ] **PostgreSQL 연동**: 분석 결과 영구 저장 +- [ ] **다중 플랫폼 지원**: 네이버, 구글 등 추가 플랫폼 (합법적 방법으로) +- [ ] **실시간 분석**: WebSocket 기반 실시간 피드백 +- [ ] **사용자 인증**: JWT 기반 인증 시스템 +- [ ] **대시보드**: 분석 결과 시각화 +- [ ] **알림 시스템**: 새로운 리뷰 알림 기능 +- [ ] **감정 분석 고도화**: 더 정확한 감정 분류 +- [ ] **비즈니스 통찰**: AI 기반 개선 방안 제시 + +## ⚖️ 법적 준수 사항 + +### 필수 확인 사항 + +1. **교육용 목적으로만 사용** +2. **실제 서비스 환경에서 사용 금지** +3. **카카오 이용약관 준수** +4. **개인정보보호법 준수** +5. **저작권 침해 금지** + +### 권장 대안 + +1. **카카오 공식 API 사용** +2. **자체 플랫폼 개발** +3. **사용자 동의 기반 수집** +4. **정식 파트너십 체결** + +--- + +## 📞 지원 및 문의 + +- **이슈 리포트**: GitHub Issues +- **기술 문의**: 개발팀 Slack +- **법적 문의**: 법무팀 +- **API 문서**: Swagger UI에서 상세 확인 + +--- + +**⚠️ 재고지: 이 서비스는 교육용으로만 제작되었으며, 실제 운영 환경에서의 사용을 엄격히 금지합니다.** diff --git a/review/app/main.py b/review/app/main.py new file mode 100644 index 0000000..29969c4 --- /dev/null +++ b/review/app/main.py @@ -0,0 +1,1626 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +카카오맵 리뷰 분석 API (로깅 강화 버전) +ConfigMap과 Secret을 활용한 Kubernetes 배포용 + +app/main.py +""" + +import os +from fastapi import FastAPI, HTTPException, BackgroundTasks +from fastapi.responses import HTMLResponse +from pydantic import BaseModel, Field +from typing import Optional, List, Dict, Any +import asyncio +import threading +import json +from datetime import datetime, timedelta +import logging +import sys +import subprocess +import platform + +# 기존 분석기 import +from bs4 import BeautifulSoup +import re +import time + +# ============================================================================= +# .env 파일 로딩 (다른 import보다 먼저) +# ============================================================================= +from dotenv import load_dotenv + +# .env 파일에서 환경변수 로드 +load_dotenv() + +# ============================================================================= +# 로깅 설정 (가장 먼저) +# ============================================================================= +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[ + logging.StreamHandler(sys.stdout) + ] +) +logger = logging.getLogger(__name__) + +# ============================================================================= +# 시스템 정보 로깅 +# ============================================================================= +logger.info("="*60) +logger.info("카카오맵 리뷰 분석 API 시작") +logger.info("="*60) +logger.info(f"Python 버전: {sys.version}") +logger.info(f"플랫폼: {platform.platform()}") +logger.info(f"아키텍처: {platform.architecture()}") +logger.info(f"현재 작업 디렉토리: {os.getcwd()}") + +# Python 경로 확인 +logger.info("Python 경로 정보:") +for i, path in enumerate(sys.path): + logger.info(f" [{i}] {path}") + +# ============================================================================= +# 패키지 설치 상태 확인 +# ============================================================================= +def check_package_installation(): + """설치된 패키지 확인""" + logger.info("패키지 설치 상태 확인 중...") + + required_packages = [ + 'selenium', 'webdriver-manager', 'beautifulsoup4', + 'fastapi', 'uvicorn', 'pydantic' + ] + + for package in required_packages: + try: + result = subprocess.run([sys.executable, '-m', 'pip', 'show', package], + capture_output=True, text=True) + if result.returncode == 0: + version_line = [line for line in result.stdout.split('\n') if line.startswith('Version:')] + version = version_line[0].split(':')[1].strip() if version_line else 'Unknown' + logger.info(f"✅ {package}: {version}") + else: + logger.error(f"❌ {package}: 설치되지 않음") + except Exception as e: + logger.error(f"❌ {package} 확인 실패: {e}") + +check_package_installation() + +# ============================================================================= +# Selenium 관련 import (세분화된 로깅) +# ============================================================================= +logger.info("Selenium 관련 모듈 import 시작...") + +SELENIUM_AVAILABLE = False +SELENIUM_IMPORT_ERRORS = [] + +# 각 모듈을 개별적으로 import하여 어디서 실패하는지 확인 +selenium_modules = [ + ('selenium', 'selenium'), + ('selenium.webdriver', 'webdriver'), + ('selenium.webdriver.chrome.options', 'Options'), + ('selenium.webdriver.common.by', 'By'), + ('selenium.webdriver.support.ui', 'WebDriverWait'), + ('selenium.webdriver.support', 'expected_conditions as EC'), + ('webdriver_manager.chrome', 'ChromeDriverManager'), + ('selenium.webdriver.chrome.service', 'Service') +] + +imported_modules = {} + +for module_path, import_name in selenium_modules: + try: + logger.info(f" 📦 {module_path}.{import_name} import 시도...") + if import_name == 'selenium': + import selenium + imported_modules['selenium'] = selenium + logger.info(f" ✅ selenium 버전: {selenium.__version__}") + elif import_name == 'webdriver': + from selenium import webdriver + imported_modules['webdriver'] = webdriver + logger.info(f" ✅ webdriver import 성공") + elif import_name == 'Options': + from selenium.webdriver.chrome.options import Options + imported_modules['Options'] = Options + logger.info(f" ✅ Options import 성공") + elif import_name == 'By': + from selenium.webdriver.common.by import By + imported_modules['By'] = By + logger.info(f" ✅ By import 성공") + elif import_name == 'WebDriverWait': + from selenium.webdriver.support.ui import WebDriverWait + imported_modules['WebDriverWait'] = WebDriverWait + logger.info(f" ✅ WebDriverWait import 성공") + elif import_name == 'expected_conditions as EC': + from selenium.webdriver.support import expected_conditions as EC + imported_modules['EC'] = EC + logger.info(f" ✅ expected_conditions import 성공") + elif import_name == 'ChromeDriverManager': + from webdriver_manager.chrome import ChromeDriverManager + imported_modules['ChromeDriverManager'] = ChromeDriverManager + logger.info(f" ✅ ChromeDriverManager import 성공") + elif import_name == 'Service': + from selenium.webdriver.chrome.service import Service + imported_modules['Service'] = Service + logger.info(f" ✅ Service import 성공") + + except ImportError as e: + error_msg = f"{module_path}.{import_name} import 실패: {e}" + logger.error(f" ❌ {error_msg}") + SELENIUM_IMPORT_ERRORS.append(error_msg) + except Exception as e: + error_msg = f"{module_path}.{import_name} import 중 예외 발생: {e}" + logger.error(f" ❌ {error_msg}") + SELENIUM_IMPORT_ERRORS.append(error_msg) + +# 모든 모듈이 성공적으로 import되었는지 확인 +required_modules = ['webdriver', 'Options', 'By', 'WebDriverWait', 'EC', 'ChromeDriverManager', 'Service'] +missing_modules = [mod for mod in required_modules if mod not in imported_modules] + +if missing_modules: + logger.error(f"❌ 누락된 모듈들: {missing_modules}") + SELENIUM_AVAILABLE = False +else: + logger.info("✅ 모든 Selenium 모듈 import 성공!") + SELENIUM_AVAILABLE = True + + # 전역 변수로 설정 + webdriver = imported_modules['webdriver'] + Options = imported_modules['Options'] + By = imported_modules['By'] + WebDriverWait = imported_modules['WebDriverWait'] + EC = imported_modules['EC'] + ChromeDriverManager = imported_modules['ChromeDriverManager'] + Service = imported_modules['Service'] + +logger.info(f"Selenium 사용 가능 여부: {SELENIUM_AVAILABLE}") +if SELENIUM_IMPORT_ERRORS: + logger.error("Import 오류 목록:") + for error in SELENIUM_IMPORT_ERRORS: + logger.error(f" - {error}") + +# ============================================================================= +# Chrome 설치 상태 확인 +# ============================================================================= +def check_chrome_installation(): + """Chrome 브라우저 설치 상태 확인""" + logger.info("Chrome 브라우저 설치 상태 확인 중...") + + chrome_paths = [ + '/usr/bin/google-chrome', + '/usr/bin/google-chrome-stable', + '/usr/bin/chromium-browser', + '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', + 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe', + 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe' + ] + + chrome_found = False + for path in chrome_paths: + if os.path.exists(path): + logger.info(f" ✅ Chrome 발견: {path}") + chrome_found = True + + # 버전 확인 시도 + try: + result = subprocess.run([path, '--version'], capture_output=True, text=True, timeout=5) + if result.returncode == 0: + logger.info(f" 📋 Chrome 버전: {result.stdout.strip()}") + except Exception as e: + logger.warning(f" ⚠️ Chrome 버전 확인 실패: {e}") + break + + if not chrome_found: + logger.error(" ❌ Chrome 브라우저가 설치되지 않음") + + # 시스템 명령어로 확인 시도 + try: + result = subprocess.run(['which', 'google-chrome'], capture_output=True, text=True) + if result.returncode == 0: + logger.info(f" ✅ PATH에서 Chrome 발견: {result.stdout.strip()}") + chrome_found = True + except: + pass + + try: + result = subprocess.run(['google-chrome', '--version'], capture_output=True, text=True, timeout=5) + if result.returncode == 0: + logger.info(f" ✅ 명령어로 Chrome 실행 가능: {result.stdout.strip()}") + chrome_found = True + except Exception as e: + logger.error(f" ❌ Chrome 명령어 실행 실패: {e}") + + return chrome_found + +chrome_available = check_chrome_installation() + +# ============================================================================= +# 환경 변수 설정 +# ============================================================================= +class Config: + """환경 변수 기반 설정 클래스""" + + # 애플리케이션 메타데이터 + APP_TITLE = os.getenv("APP_TITLE", "카카오맵 리뷰 분석 API") + APP_VERSION = os.getenv("APP_VERSION", "1.0.0") + APP_DESCRIPTION = os.getenv("APP_DESCRIPTION", "교육 목적 전용 - 실제 서비스 사용 금지") + + # 서버 설정 + HOST = os.getenv("HOST", "0.0.0.0") + PORT = int(os.getenv("PORT", "8000")) + WORKERS = int(os.getenv("WORKERS", "1")) + LOG_LEVEL = os.getenv("LOG_LEVEL", "info") + + # API 기본값 + DEFAULT_MAX_TIME = int(os.getenv("DEFAULT_MAX_TIME", "300")) + DEFAULT_DAYS_LIMIT = int(os.getenv("DEFAULT_DAYS_LIMIT", "7")) + MAX_DAYS_LIMIT = int(os.getenv("MAX_DAYS_LIMIT", "365")) + MIN_MAX_TIME = int(os.getenv("MIN_MAX_TIME", "60")) + MAX_MAX_TIME = int(os.getenv("MAX_MAX_TIME", "1800")) + + # Chrome 옵션 (ConfigMap에서 멀티라인 문자열로 받음) + CHROME_OPTIONS_RAW = os.getenv("CHROME_OPTIONS", """ + --headless + --no-sandbox + --disable-dev-shm-usage + --disable-gpu + --window-size=1920,1080 + --disable-extensions + --disable-plugins + --disable-usb-keyboard-detect + --no-first-run + --no-default-browser-check + --disable-logging + --log-level=3 + """) + + @property + def CHROME_OPTIONS_LIST(self): + """Chrome 옵션을 리스트로 파싱""" + return [opt.strip() for opt in self.CHROME_OPTIONS_RAW.strip().split('\n') if opt.strip()] + + # 스크롤링 설정 + SCROLL_CHECK_INTERVAL = int(os.getenv("SCROLL_CHECK_INTERVAL", "3")) + SCROLL_NO_CHANGE_LIMIT = int(os.getenv("SCROLL_NO_CHANGE_LIMIT", "8")) + SCROLL_WAIT_TIME_SHORT = float(os.getenv("SCROLL_WAIT_TIME_SHORT", "1.5")) + SCROLL_WAIT_TIME_LONG = float(os.getenv("SCROLL_WAIT_TIME_LONG", "2.0")) + + # 법적 경고 + LEGAL_WARNING_ENABLED = os.getenv("LEGAL_WARNING_ENABLED", "true").lower() == "true" + CONTACT_EMAIL = os.getenv("CONTACT_EMAIL", "admin@example.com") + + # 건강 체크 + HEALTH_CHECK_TIMEOUT = int(os.getenv("HEALTH_CHECK_TIMEOUT", "5")) + + # Secret 값들 (현재는 사용하지 않지만 향후 확장용) + EXTERNAL_API_KEY = os.getenv("EXTERNAL_API_KEY", "") + DB_USERNAME = os.getenv("DB_USERNAME", "") + DB_PASSWORD = os.getenv("DB_PASSWORD", "") + JWT_SECRET = os.getenv("JWT_SECRET", "") + +# 설정 인스턴스 +config = Config() + +logger.info("환경 설정 로드 완료:") +logger.info(f" - Chrome 옵션 개수: {len(config.CHROME_OPTIONS_LIST)}") +logger.info(f" - 기본 최대 시간: {config.DEFAULT_MAX_TIME}초") +logger.info(f" - 로그 레벨: {config.LOG_LEVEL}") + +# FastAPI 앱 초기화 (환경변수 사용) +app = FastAPI( + title=config.APP_TITLE, + description=f""" + **⚠️ 중요 법적 경고사항 ⚠️** + + {config.APP_DESCRIPTION} + + **진단 정보:** + - Selenium 사용 가능: {SELENIUM_AVAILABLE} + - Chrome 설치됨: {chrome_available} + - Import 오류 개수: {len(SELENIUM_IMPORT_ERRORS)} + + **법적 위험:** + - 카카오 이용약관 위반 → 계정 정지, 법적 조치 + - 개인정보보호법(PIPA) 위반 → 과태료 최대 3억원 + - 저작권 및 데이터베이스권 침해 → 손해배상 + - 업무방해죄 → 5년 이하 징역 또는 1천500만원 이하 벌금 + + **합법적 대안:** + - 카카오 공식 API 활용 (developers.kakao.com) + - 점주 대상 자체 가게 관리 서비스 개발 + - 사용자 동의 기반 데이터 수집 앱 + - 카카오와 정식 파트너십 체결 + + **이 API를 실제 서비스에 사용하지 마세요!** + + **환경:** 로컬 개발 + **버전:** {config.APP_VERSION} + **연락처:** {config.CONTACT_EMAIL} + """, + version=config.APP_VERSION, + contact={ + "name": "관리자", + "email": config.CONTACT_EMAIL + }, + license_info={ + "name": "Educational Use Only", + "url": "https://developers.kakao.com" + } +) + +# Pydantic 모델 정의 (기존과 동일) +class ReviewAnalysisRequest(BaseModel): + """리뷰 분석 요청 모델""" + store_id: str = Field( + ..., + description="카카오맵 가게 ID (예: 501745730)", + example="501745730", + min_length=1, + max_length=20 + ) + days_limit: Optional[int] = Field( + None, + description=f"며칠 이후의 리뷰만 수집할지 (None이면 모든 날짜, 최대 {config.MAX_DAYS_LIMIT}일)", + example=config.DEFAULT_DAYS_LIMIT, + ge=1, + le=config.MAX_DAYS_LIMIT + ) + max_time: int = Field( + config.DEFAULT_MAX_TIME, + description=f"최대 스크롤 시간(초, {config.MIN_MAX_TIME}-{config.MAX_MAX_TIME}초)", + example=config.DEFAULT_MAX_TIME, + ge=config.MIN_MAX_TIME, + le=config.MAX_MAX_TIME + ) + +class ReviewerStats(BaseModel): + """리뷰어 통계 정보""" + reviews: Optional[int] = None + average_rating: Optional[float] = None + followers: Optional[int] = None + +class ReviewData(BaseModel): + """개별 리뷰 데이터""" + reviewer_name: str + reviewer_level: str + reviewer_stats: ReviewerStats + rating: int + date: str + content: str + badges: List[str] + likes: int + photo_count: int + has_photos: bool + +class StoreInfo(BaseModel): + """가게 정보 (수정됨: id 필드 추가)""" + id: str = Field(description="가게 ID") + name: str + category: str + rating: str + review_count: str + status: str + address: str + +class DateFilter(BaseModel): + """날짜 필터 정보""" + cutoff_date: Optional[str] = None + filtered: bool + +class ReviewAnalysisResponse(BaseModel): + """리뷰 분석 응답 모델""" + success: bool = Field(description="분석 성공 여부") + message: str = Field(description="응답 메시지") + store_info: Optional[StoreInfo] = None + reviews: List[ReviewData] = [] + analysis_date: str = Field(description="분석 수행 날짜시간") + total_reviews: int = Field(description="수집된 총 리뷰 수") + analysis_method: str = "selenium" + date_filter: DateFilter + execution_time: float = Field(description="실행 시간(초)") + +class ErrorResponse(BaseModel): + """에러 응답 모델""" + success: bool = False + error: str + message: str + timestamp: str + +# 분석기 클래스 (로깅 강화) +class KakaoReviewAnalyzerAPI: + """로깅 강화된 카카오맵 리뷰 분석기""" + + def __init__(self): + self.store_info = {} + self.reviews = [] + self.cutoff_date = None + self.config = config + logger.info("KakaoReviewAnalyzerAPI 인스턴스 생성 완료") + + def analyze_by_store_id(self, store_id: str, days_limit: Optional[int] = None, max_scroll_time: int = None): + """Selenium을 사용한 동적 리뷰 분석 (ChromeDriver 경로 수정)""" + + logger.info("="*60) + logger.info("analyze_by_store_id 함수 시작") + logger.info("="*60) + + if max_scroll_time is None: + max_scroll_time = self.config.DEFAULT_MAX_TIME + + logger.info(f"입력 파라미터:") + logger.info(f" - store_id: {store_id}") + logger.info(f" - days_limit: {days_limit}") + logger.info(f" - max_scroll_time: {max_scroll_time}") + + # 날짜 제한 설정 + if days_limit is not None: + self.cutoff_date = datetime.now() - timedelta(days=days_limit) + logger.info(f"수집 기준일 설정: {self.cutoff_date.strftime('%Y.%m.%d')} 이후 리뷰만 수집") + else: + self.cutoff_date = None + logger.info("날짜 제한 없음: 모든 날짜의 리뷰 수집") + + target_url = f"https://place.map.kakao.com/{store_id}#comment" + logger.info(f"대상 URL 생성: {target_url}") + + driver = None + temp_profile = None + step_counter = 0 + + try: + # Step 1: Chrome 옵션 설정 + step_counter += 1 + logger.info(f"[단계 {step_counter}] Chrome 옵션 설정 중...") + + if not SELENIUM_AVAILABLE: + raise Exception(f"Selenium을 사용할 수 없음. Import 오류: {SELENIUM_IMPORT_ERRORS}") + + options = Options() + logger.info(f" Chrome Options 객체 생성 완료") + + for i, option in enumerate(self.config.CHROME_OPTIONS_LIST): + logger.info(f" 옵션 [{i+1}] 추가: {option}") + options.add_argument(option) + + # User Agent 추가 + user_agent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' + logger.info(f" User Agent 설정: {user_agent}") + options.add_argument(f'--user-agent={user_agent}') + + # 추가 최적화 옵션들 + experimental_options = ['enable-logging'] + logger.info(f" 실험적 옵션 제외: {experimental_options}") + options.add_experimental_option('excludeSwitches', experimental_options) + options.add_experimental_option('useAutomationExtension', False) + + logger.info(f" Chrome 옵션 설정 완료 (총 {len(self.config.CHROME_OPTIONS_LIST)+1}개)") + + # Step 2: WebDriver 초기화 (ChromeDriverManager 사용 중단) + step_counter += 1 + logger.info(f"[단계 {step_counter}] Chrome WebDriver 초기화 중...") + + # 🔧 시스템에 설치된 ChromeDriver 사용 + chromedriver_paths = [ + "/usr/local/bin/chromedriver", # Dockerfile에서 설치한 경로 + "/usr/bin/chromedriver", + "/opt/chromedriver/chromedriver", + "./chromedriver" + ] + + driver_path = None + for path in chromedriver_paths: + if os.path.exists(path) and os.access(path, os.X_OK): + logger.info(f" ChromeDriver 발견: {path}") + # 버전 확인 + try: + import subprocess + result = subprocess.run([path, "--version"], capture_output=True, text=True, timeout=5) + if result.returncode == 0: + logger.info(f" ChromeDriver 버전: {result.stdout.strip()}") + driver_path = path + break + except Exception as version_error: + logger.warning(f" {path} 버전 확인 실패: {version_error}") + continue + + if not driver_path: + # 마지막 수단: ChromeDriverManager 사용하되 올바른 실행 파일 찾기 + logger.info(" 시스템 ChromeDriver 없음 - ChromeDriverManager 사용...") + try: + # 🔧 ChromeDriverManager 개선 + os.environ['WDM_LOCAL'] = '/tmp/.wdm' + downloaded_path = ChromeDriverManager(cache_valid_range=7).install() + logger.info(f" 다운로드된 경로: {downloaded_path}") + + # 다운로드된 디렉토리에서 실제 실행 파일 찾기 + download_dir = os.path.dirname(downloaded_path) + possible_paths = [ + os.path.join(download_dir, "chromedriver"), + os.path.join(download_dir, "chromedriver-linux64", "chromedriver"), + downloaded_path + ] + + for candidate_path in possible_paths: + if os.path.exists(candidate_path) and os.access(candidate_path, os.X_OK): + # 파일이 실제 실행 파일인지 확인 (텍스트 파일이 아닌지) + try: + with open(candidate_path, 'rb') as f: + header = f.read(4) + # ELF 헤더 확인 (리눅스 실행 파일) + if header.startswith(b'\x7fELF'): + logger.info(f" 실행 가능한 ChromeDriver 발견: {candidate_path}") + driver_path = candidate_path + break + except Exception as file_check_error: + logger.warning(f" 파일 확인 실패: {candidate_path} - {file_check_error}") + continue + + if not driver_path: + raise Exception(f"다운로드된 디렉토리에서 실행 가능한 chromedriver를 찾을 수 없음: {download_dir}") + + except Exception as cdm_error: + logger.error(f" ChromeDriverManager 실패: {cdm_error}") + raise Exception(f"ChromeDriver를 찾을 수 없음. 시스템에 chromedriver를 설치하거나 Docker 이미지를 다시 빌드하세요.") + + # Service 객체 생성 + logger.info(f" ChromeDriver 경로 사용: {driver_path}") + service = Service(driver_path) + + # 🔧 Chrome 실행 환경 설정 + logger.info(" Chrome 실행 환경 설정...") + + # 임시 프로필 디렉토리 생성 + import tempfile + temp_profile = tempfile.mkdtemp(prefix='chrome_profile_') + logger.info(f" 임시 Chrome 프로필 생성: {temp_profile}") + + # Chrome 옵션에 프로필 경로 및 추가 안정성 옵션 추가 + options.add_argument(f'--user-data-dir={temp_profile}') + options.add_argument('--disable-web-security') + options.add_argument('--allow-running-insecure-content') + options.add_argument('--disable-blink-features=AutomationControlled') + options.add_experimental_option("excludeSwitches", ["enable-automation"]) + options.add_experimental_option('useAutomationExtension', False) + + logger.info(" 추가 안정성 옵션 적용 완료") + + # WebDriver 생성 + logger.info(" Chrome WebDriver 인스턴스 생성 중...") + try: + driver = webdriver.Chrome(service=service, options=options) + logger.info(" Chrome WebDriver 객체 생성 완료") + + # 🔧 즉시 연결 상태 확인 + logger.info(" Chrome 연결 상태 확인...") + driver.set_page_load_timeout(30) + driver.set_script_timeout(30) + driver.implicitly_wait(10) + + # 기본 페이지로 이동하여 연결 테스트 + logger.info(" 연결 테스트용 빈 페이지 로드...") + driver.get("data:text/html,Test

Connection Test

") + + # 페이지 제목 확인으로 연결 검증 + test_title = driver.title + logger.info(f" 연결 테스트 성공 - 페이지 제목: {test_title}") + + if not test_title: + raise Exception("Chrome 연결은 되었으나 페이지 로드 실패") + + logger.info(" Chrome WebDriver 연결 확인 완료") + + # Chrome 정보 확인 + try: + capabilities = driver.capabilities + chrome_version = capabilities.get('browserVersion', 'Unknown') + driver_version = capabilities.get('chrome', {}).get('chromedriverVersion', 'Unknown') + logger.info(f" Chrome 버전: {chrome_version}") + logger.info(f" ChromeDriver 버전: {driver_version}") + + # 창 크기 확인 + window_size = driver.get_window_size() + logger.info(f" 현재 창 크기: {window_size}") + except Exception as info_error: + logger.warning(f" Chrome 정보 확인 실패: {info_error}") + + except Exception as driver_error: + logger.error(f" Chrome WebDriver 생성/연결 실패: {driver_error}") + + # 리소스 정리 + if driver: + try: + driver.quit() + except: + pass + + if temp_profile and os.path.exists(temp_profile): + try: + import shutil + shutil.rmtree(temp_profile) + logger.info(" 임시 프로필 디렉토리 정리 완료") + except Exception as cleanup_error: + logger.warning(f" 임시 프로필 정리 실패: {cleanup_error}") + + error_details = { + "error_type": "WEBDRIVER_CONNECTION_FAILED", + "chrome_options_count": len(self.config.CHROME_OPTIONS_LIST), + "driver_path": driver_path, + "temp_profile": temp_profile, + "system_info": { + "checked_paths": chromedriver_paths, + "chrome_exists": os.path.exists("/usr/bin/google-chrome"), + "wdm_cache": os.path.exists("/tmp/.wdm") + } + } + raise Exception(f"Chrome WebDriver 연결 실패: {driver_error}\n상세 정보: {error_details}") + + # Step 3: 페이지 로드 + step_counter += 1 + logger.info(f"[단계 {step_counter}] 페이지 로드 시작...") + logger.info(f" 요청 URL: {target_url}") + + start_time = time.time() + try: + driver.get(target_url) + load_time = time.time() - start_time + logger.info(f" 페이지 로드 완료 (소요시간: {load_time:.2f}초)") + + # 페이지 정보 확인 + current_url = driver.current_url + page_title = driver.title + logger.info(f" 현재 URL: {current_url}") + logger.info(f" 페이지 제목: {page_title}") + + if "404" in page_title or "오류" in page_title: + raise Exception(f"유효하지 않은 페이지: {page_title}") + + # Body 로드 대기 + logger.info(" 페이지 body 로드 대기 중...") + WebDriverWait(driver, 15).until( + EC.presence_of_element_located((By.TAG_NAME, "body")) + ) + logger.info(" 페이지 body 로드 완료") + + except Exception as page_error: + logger.error(f" 페이지 로드 실패: {page_error}") + current_url = driver.current_url if driver else "N/A" + raise Exception(f"페이지 로드 실패 - 요청 URL: {target_url}, 현재 URL: {current_url}, 오류: {page_error}") + + # Step 4: 후기 탭 클릭 (이하 동일) + step_counter += 1 + logger.info(f"[단계 {step_counter}] 후기 탭 클릭 시도...") + + review_tab_selectors = [ + "//a[@href='#comment']", + "//a[contains(text(), '후기')]", + "//button[contains(text(), '후기')]", + "//*[@id='comment']", + "//a[contains(@class, 'comment')]" + ] + + review_tab_found = False + for i, selector in enumerate(review_tab_selectors): + try: + logger.info(f" 선택자 [{i+1}] 시도: {selector}") + review_tab = WebDriverWait(driver, 3).until( + EC.element_to_be_clickable((By.XPATH, selector)) + ) + logger.info(f" 후기 탭 발견: {selector}") + driver.execute_script("arguments[0].click();", review_tab) + time.sleep(3) + logger.info(f" 후기 탭 클릭 성공") + review_tab_found = True + break + except Exception as e: + logger.info(f" 선택자 [{i+1}] 실패: {e}") + continue + + if not review_tab_found: + logger.warning(" 후기 탭을 찾을 수 없음 - 기본 페이지에서 진행") + + # Step 5: 리뷰 목록 로드 확인 + step_counter += 1 + logger.info(f"[단계 {step_counter}] 리뷰 목록 로드 확인...") + + review_list_selectors = [ + ".list_review", + "[class*='review']", + "[class*='comment']", + ".review_list", + ".comment_list" + ] + + review_list_found = False + for i, selector in enumerate(review_list_selectors): + try: + logger.info(f" 리뷰 목록 선택자 [{i+1}] 시도: {selector}") + elements = driver.find_elements(By.CSS_SELECTOR, selector) + if elements: + logger.info(f" 리뷰 목록 발견: {selector} (요소 {len(elements)}개)") + review_list_found = True + break + except Exception as e: + logger.info(f" 리뷰 목록 선택자 [{i+1}] 실패: {e}") + + if not review_list_found: + logger.warning(" 리뷰 목록을 찾을 수 없음 - HTML에서 직접 추출 시도") + + # Step 6: 스크롤링 + step_counter += 1 + logger.info(f"[단계 {step_counter}] 스크롤링 시작...") + logger.info(f" 최대 스크롤 시간: {max_scroll_time}초") + + try: + self._smart_infinite_scroll(driver, max_scroll_time) + logger.info(" 스크롤링 완료") + except Exception as scroll_error: + logger.error(f" 스크롤링 중 오류: {scroll_error}") + logger.info(" 스크롤링 실패 - 현재까지 로드된 내용으로 분석 진행") + + # Step 7: HTML 분석 + step_counter += 1 + logger.info(f"[단계 {step_counter}] HTML 내용 분석...") + + html_content = driver.page_source + html_length = len(html_content) + logger.info(f" HTML 내용 추출 완료: {html_length:,}자") + + if html_length < 1000: + raise Exception(f"HTML 내용이 너무 짧음: {html_length}자") + + # HTML 분석 수행 + logger.info(" HTML 파싱 및 데이터 추출 시작...") + store_info, reviews = self.analyze_html_content(html_content, store_id) + + # 결과 검증 + if not store_info: + logger.warning(" 가게 정보 추출 실패 - 기본값 설정") + store_info = { + 'id': store_id, + 'name': f'Store_{store_id}', + 'category': '', + 'rating': '', + 'review_count': '', + 'status': '', + 'address': '' + } + else: + logger.info(f" 가게 정보 추출 성공: {store_info.get('name', 'N/A')}") + + if not reviews: + logger.warning(" 리뷰 데이터 추출 실패") + reviews = [] + else: + logger.info(f" 리뷰 데이터 추출 성공: {len(reviews)}개") + + logger.info("="*60) + logger.info("analyze_by_store_id 함수 완료") + logger.info(f"최종 결과: 가게 정보={store_info.get('name', 'N/A')}, 리뷰={len(reviews)}개") + logger.info("="*60) + + return store_info, reviews + + except Exception as e: + logger.error("="*60) + logger.error(f"[단계 {step_counter}] 오류 발생!") + logger.error(f"오류 내용: {str(e)}") + logger.error("="*60) + + # 상세 디버그 정보 수집 + debug_info = { + "failed_step": step_counter, + "store_id": store_id, + "target_url": target_url, + "max_scroll_time": max_scroll_time, + "days_limit": days_limit, + "selenium_available": SELENIUM_AVAILABLE, + "chrome_available": chrome_available, + "chrome_options_count": len(self.config.CHROME_OPTIONS_LIST), + "import_errors": SELENIUM_IMPORT_ERRORS, + "driver_status": "initialized" if driver else "not_initialized", + "temp_profile": temp_profile + } + + if driver: + try: + debug_info.update({ + "current_url": driver.current_url, + "page_title": driver.title, + "window_size": driver.get_window_size() + }) + except: + debug_info["driver_info"] = "driver_not_accessible" + + logger.error("디버그 정보:") + for key, value in debug_info.items(): + logger.error(f" {key}: {value}") + + raise Exception(f"리뷰 분석 실패 (단계 {step_counter}): {str(e)}") + + finally: + # 🔧 리소스 정리 강화 + logger.info("리소스 정리 중...") + + # WebDriver 정리 + if driver: + try: + logger.info(" Chrome WebDriver 종료 중...") + driver.quit() + logger.info(" Chrome WebDriver 종료 완료") + except Exception as cleanup_error: + logger.warning(f" WebDriver 정리 중 오류: {cleanup_error}") + else: + logger.info(" WebDriver가 초기화되지 않았음 - 정리 작업 불필요") + + # 임시 프로필 디렉토리 정리 + if temp_profile and os.path.exists(temp_profile): + try: + import shutil + shutil.rmtree(temp_profile) + logger.info(f" 임시 프로필 디렉토리 정리 완료: {temp_profile}") + except Exception as cleanup_error: + logger.warning(f" 임시 프로필 정리 실패: {cleanup_error}") + + logger.info("리소스 정리 완료") + + def _smart_infinite_scroll(self, driver, max_time): + """환경변수 기반 스마트 무한 스크롤링 (로깅 강화)""" + logger.info("스크롤링 세부 설정:") + logger.info(f" - 체크 간격: {self.config.SCROLL_CHECK_INTERVAL}회마다") + logger.info(f" - 변화 없음 제한: {self.config.SCROLL_NO_CHANGE_LIMIT}회") + logger.info(f" - 대기 시간 (짧음): {self.config.SCROLL_WAIT_TIME_SHORT}초") + logger.info(f" - 대기 시간 (김): {self.config.SCROLL_WAIT_TIME_LONG}초") + + start_time = time.time() + scroll_count = 0 + no_change_count = 0 + last_review_count = 0 + + while True: + scroll_count += 1 + elapsed_time = time.time() - start_time + + # 시간 제한 확인 + if elapsed_time > max_time: + logger.info(f"스크롤링 종료: 시간 제한 도달 ({max_time}초)") + break + + try: + # 현재 리뷰 개수 확인 + review_elements = driver.find_elements(By.CSS_SELECTOR, "ul.list_review li") + current_review_count = len(review_elements) + + # 10회마다 진행 상황 로깅 + if scroll_count % 10 == 0: + logger.info(f"스크롤링 진행: {scroll_count}회 | 리뷰 {current_review_count}개 | {elapsed_time:.1f}초 경과") + + # 날짜 기준 확인 + if self.cutoff_date and scroll_count % self.config.SCROLL_CHECK_INTERVAL == 0: + logger.info(f"날짜 기준 확인 중... (스크롤 {scroll_count}회)") + date_cutoff_reached, oldest_date = self._check_date_cutoff_realtime(driver) + if date_cutoff_reached: + logger.info(f"스크롤링 종료: 날짜 기준 도달 ({oldest_date})") + break + + # 리뷰 개수 변화 확인 + if current_review_count == last_review_count: + no_change_count += 1 + if no_change_count >= self.config.SCROLL_NO_CHANGE_LIMIT: + logger.info(f"스크롤링 종료: 더 이상 새로운 리뷰 없음 (총 {current_review_count}개)") + break + else: + no_change_count = 0 + last_review_count = current_review_count + + # 스크롤 실행 + driver.execute_script("window.scrollTo(0, document.body.scrollHeight);") + + # 대기 시간 조정 + wait_time = (self.config.SCROLL_WAIT_TIME_SHORT if current_review_count < 30 + else self.config.SCROLL_WAIT_TIME_LONG) + time.sleep(wait_time) + + except Exception as e: + logger.error(f"스크롤 {scroll_count}회 실행 실패: {e}") + break + + # 최종 결과 + final_count = len(driver.find_elements(By.CSS_SELECTOR, "ul.list_review li")) + logger.info(f"스크롤링 최종 결과:") + logger.info(f" - 총 스크롤 횟수: {scroll_count}회") + logger.info(f" - 최종 리뷰 개수: {final_count}개") + logger.info(f" - 총 소요 시간: {elapsed_time:.1f}초") + + def _check_date_cutoff_realtime(self, driver): + """실시간 날짜 기준 확인 (로깅 강화)""" + if not self.cutoff_date: + return False, None + + try: + date_elements = driver.find_elements(By.CSS_SELECTOR, "ul.list_review li .txt_date") + recent_dates = date_elements[-10:] if len(date_elements) > 10 else date_elements + + logger.debug(f"날짜 확인: 최근 {len(recent_dates)}개 날짜 요소 검사") + + for i, date_elem in enumerate(recent_dates): + try: + date_text = date_elem.text.strip() + if date_text.endswith('.'): + date_text = date_text[:-1] + + if not date_text: + continue + + review_date = datetime.strptime(date_text, '%Y.%m.%d') + logger.debug(f" 날짜 [{i+1}]: {date_text} vs 기준일 {self.cutoff_date.strftime('%Y.%m.%d')}") + + if review_date < self.cutoff_date: + logger.info(f"날짜 기준 충족: {date_text} < {self.cutoff_date.strftime('%Y.%m.%d')}") + return True, date_text + + except (ValueError, Exception) as e: + logger.debug(f" 날짜 파싱 실패 [{i+1}]: {date_text} - {e}") + continue + + except Exception as e: + logger.warning(f"날짜 확인 중 오류: {e}") + + return False, None + + def analyze_html_content(self, html_content, store_id): + """HTML 내용 분석 (수정됨: store_id 파라미터 추가)""" + logger.info("HTML 내용 분석 시작...") + + try: + soup = BeautifulSoup(html_content, 'html.parser') + logger.info("BeautifulSoup 파싱 완료") + + # 가게 정보 추출 + logger.info("가게 정보 추출 중...") + self.store_info = self._extract_store_info(soup, store_id) + logger.info(f"가게 정보 추출 완료: {self.store_info}") + + # 리뷰 추출 + logger.info("리뷰 목록 추출 중...") + self.reviews = self._extract_reviews(soup) + logger.info(f"리뷰 추출 완료: {len(self.reviews)}개") + + return self.store_info, self.reviews + + except Exception as e: + logger.error(f"HTML 분석 중 오류: {e}") + raise + + def _extract_store_info(self, soup, store_id): + """가게 정보 추출 (수정됨: id 필드 추가)""" + store_info = { + 'id': store_id, # 추가: 가게 ID 설정 + 'name': '', + 'category': '', + 'rating': '', + 'review_count': '', + 'status': '', + 'address': '' + } + + # 가게명 (수정: '장소명' 제거) + store_name = soup.select_one('h3.tit_place') + if store_name: + name_text = store_name.get_text(strip=True) + # '장소명' 텍스트 제거 + name_text = name_text.replace('장소명', '').strip() + store_info['name'] = name_text + logger.info(f" 가게ID: {store_info['id']}") + logger.info(f" 가게명: {store_info['name']}") + else: + logger.warning(" 가게명을 찾을 수 없음") + + # 카테고리 (수정: '장소 카테고리' 제거) + category = soup.select_one('span.info_cate') + if category: + category_text = category.get_text(strip=True) + # '장소 카테고리' 텍스트 제거 + category_text = category_text.replace('장소 카테고리', '').strip() + store_info['category'] = category_text + logger.info(f" 카테고리: {store_info['category']}") + else: + logger.warning(" 카테고리를 찾을 수 없음") + + # 별점 + rating = soup.select_one('span.num_star') + if rating: + store_info['rating'] = rating.get_text(strip=True) + logger.info(f" 별점: {store_info['rating']}") + else: + logger.warning(" 별점을 찾을 수 없음") + + # 후기 수 + review_count = soup.select_one('span.info_num') + if review_count: + count_text = review_count.get_text(strip=True) + numbers = re.findall(r'\d+', count_text) + if numbers: + store_info['review_count'] = numbers[0] + logger.info(f" 후기 수: {store_info['review_count']}") + else: + logger.warning(" 후기 수를 찾을 수 없음") + + # 영업 상태 + status = soup.select_one('span.info_state') + if status: + store_info['status'] = status.get_text(strip=True) + logger.info(f" 영업 상태: {store_info['status']}") + else: + logger.warning(" 영업 상태를 찾을 수 없음") + + # 주소 + address_meta = soup.find('meta', property='og:description') + if address_meta: + store_info['address'] = address_meta.get('content', '') + logger.info(f" 주소: {store_info['address']}") + else: + logger.warning(" 주소를 찾을 수 없음") + + return store_info + + def _extract_reviews(self, soup): + """리뷰 목록 추출 (로깅 강화)""" + reviews = [] + + review_list = soup.select_one('ul.list_review') + if not review_list: + logger.warning("리뷰 목록 컨테이너를 찾을 수 없음") + return reviews + + review_items = review_list.find_all('li', recursive=False) + logger.info(f"발견된 리뷰 아이템: {len(review_items)}개") + + skipped_count = 0 + processed_count = 0 + + for i, item in enumerate(review_items): + try: + if (i + 1) % 10 == 0: + logger.info(f"리뷰 처리 진행: {i+1}/{len(review_items)}") + + review_data = self._extract_single_review(item) + if review_data and review_data.get('reviewer_name'): + if self._is_review_within_date_range(review_data): + reviews.append(review_data) + processed_count += 1 + else: + skipped_count += 1 + if skipped_count > 5: + logger.info(f"연속 오래된 리뷰로 추출 중단 (총 {len(reviews)}개)") + break + + except Exception as e: + logger.warning(f"리뷰 #{i+1} 추출 실패: {e}") + + logger.info(f"리뷰 추출 완료: 처리됨={processed_count}개, 제외됨={skipped_count}개, 총={len(reviews)}개") + return reviews + + def _extract_single_review(self, item): + """개별 리뷰 데이터 추출 (수정됨: reviewer_name에서 '리뷰어 이름, ' 제거)""" + review_data = { + 'reviewer_name': '', + 'reviewer_level': '', + 'reviewer_stats': {}, + 'rating': 0, + 'date': '', + 'content': '', + 'badges': [], + 'likes': 0, + 'photo_count': 0, + 'has_photos': False + } + + # 리뷰어 이름 (수정: '리뷰어 이름, ' 제거) + name_elem = item.select_one('span.name_user') + if name_elem: + name_text = name_elem.get_text(strip=True) + # '리뷰어 이름, ' 텍스트 제거 (더 확실한 방법) + if '리뷰어 이름,' in name_text: + review_data['reviewer_name'] = name_text.replace('리뷰어 이름,', '').strip() + else: + review_data['reviewer_name'] = name_text + + # 리뷰어 레벨 + level_elem = item.select_one('span.txt_badge') + if level_elem: + review_data['reviewer_level'] = level_elem.get_text(strip=True) + + # 리뷰어 통계 + detail_list = item.select_one('ul.list_detail') + if detail_list: + stats_json = {} + for li in detail_list.find_all('li'): + stat = li.get_text(strip=True) + if '후기' in stat: + numbers = re.findall(r'\d+', stat) + if numbers: + stats_json['reviews'] = int(numbers[0]) + elif '별점평균' in stat: + numbers = re.findall(r'\d+\.?\d*', stat) + if numbers: + stats_json['average_rating'] = float(numbers[0]) + elif '팔로워' in stat: + numbers = re.findall(r'\d+', stat) + if numbers: + stats_json['followers'] = int(numbers[0]) + review_data['reviewer_stats'] = stats_json + + # 별점 + star_wrapper = item.select_one('span.wrap_grade') + if star_wrapper: + stars = star_wrapper.select('span.figure_star.on') + review_data['rating'] = len(stars) + + # 날짜 + date_elem = item.select_one('span.txt_date') + if date_elem: + date_text = date_elem.get_text(strip=True) + if date_text.endswith('.'): + date_text = date_text[:-1] + review_data['date'] = date_text + + # 리뷰 내용 + content_elem = item.select_one('p.desc_review') + if content_elem: + review_data['content'] = content_elem.get_text(strip=True) + + # 태그/배지 + badges = item.select('span.badge_point') + for badge in badges: + badge_text = badge.get_text(strip=True) + if badge_text and badge_text not in review_data['badges']: + review_data['badges'].append(badge_text) + + # 좋아요 수 + like_btn = item.select_one('button .txt_btn') + if like_btn: + like_text = like_btn.get_text(strip=True) + try: + review_data['likes'] = int(like_text) + except ValueError: + review_data['likes'] = 0 + + # 사진 개수 + photos = item.select('.review_thumb img, .thumb_img img') + review_data['photo_count'] = len(photos) + review_data['has_photos'] = len(photos) > 0 + + return review_data + + def _is_review_within_date_range(self, review_data): + """리뷰가 날짜 범위 내에 있는지 확인""" + if not self.cutoff_date: + return True + + date_str = review_data.get('date', '') + if not date_str: + return True + + try: + if date_str.endswith('.'): + date_str = date_str[:-1] + review_date = datetime.strptime(date_str, '%Y.%m.%d') + return review_date >= self.cutoff_date + except ValueError: + return True + +# API 엔드포인트 +@app.get("/", response_class=HTMLResponse, include_in_schema=False) +async def root(): + """메인 페이지 (진단 정보 포함)""" + + # 진단 정보 생성 + diagnosis_info = f""" +
+

🔧 시스템 진단 정보

+
    +
  • Selenium 사용 가능: {'✅ 예' if SELENIUM_AVAILABLE else '❌ 아니오'}
  • +
  • Chrome 설치됨: {'✅ 예' if chrome_available else '❌ 아니오'}
  • +
  • Import 오류 개수: {len(SELENIUM_IMPORT_ERRORS)}
  • +
  • Python 버전: {sys.version.split()[0]}
  • +
  • 플랫폼: {platform.platform()}
  • +
+ + {f''' +

❌ Import 오류 목록:

+
    + {''.join([f"
  • {error}
  • " for error in SELENIUM_IMPORT_ERRORS])} +
+ ''' if SELENIUM_IMPORT_ERRORS else ''} +
+ """ + + warning_section = "" + if config.LEGAL_WARNING_ENABLED: + warning_section = f""" +
+

⚠️ 중요 법적 경고사항

+

{config.APP_DESCRIPTION}

+
    +
  • 실제 웹사이트 크롤링은 불법입니다
  • +
  • 카카오 이용약관 위반 위험
  • +
  • 개인정보보호법 위반 위험
  • +
  • 업무방해죄 처벌 가능
  • +
+

합법적 대안을 사용하세요:

+
    +
  • 카카오 공식 API (developers.kakao.com)
  • +
  • 정식 파트너십 체결
  • +
  • 사용자 동의 기반 데이터 수집
  • +
+
+ """ + + return f""" + + + {config.APP_TITLE} + + + +
+

🔍 {config.APP_TITLE}

+

버전: {config.APP_VERSION}

+

환경: 로컬 개발

+

연락처: {config.CONTACT_EMAIL}

+
+ + {diagnosis_info} + + {warning_section} + +
+

🔧 환경 설정 정보

+
    +
  • 기본 최대 시간: {config.DEFAULT_MAX_TIME}초
  • +
  • 기본 날짜 제한: {config.DEFAULT_DAYS_LIMIT}일
  • +
  • 스크롤 체크 간격: {config.SCROLL_CHECK_INTERVAL}회
  • +
  • Chrome 옵션: {len(config.CHROME_OPTIONS_LIST)}개 설정
  • +
  • 로그 레벨: {config.LOG_LEVEL}
  • +
+
+ +

📚 API 문서

+ Swagger UI 문서 + ReDoc 문서 + 헬스 체크 + 진단 정보 + +

🛠️ 사용 방법

+

POST /analyze - 리뷰 분석 수행

+
+{{
+  "store_id": "501745730",
+  "days_limit": {config.DEFAULT_DAYS_LIMIT},
+  "max_time": {config.DEFAULT_MAX_TIME}
+}}
+            
+ + + """ + +@app.post( + "/analyze", + response_model=ReviewAnalysisResponse, + summary="카카오맵 리뷰 분석 (로깅 강화)", + description=f""" + **⚠️ {config.APP_DESCRIPTION}** + + **진단 정보:** + - Selenium 사용 가능: {SELENIUM_AVAILABLE} + - Chrome 설치됨: {chrome_available} + - Import 오류 개수: {len(SELENIUM_IMPORT_ERRORS)} + + 지정한 카카오맵 가게의 리뷰를 분석합니다. + + **주요 기능:** + - 실시간 리뷰 수집 및 분석 + - 날짜 범위 필터링 (최대 {config.MAX_DAYS_LIMIT}일) + - 감정 분석 및 통계 + - JSON 형태 응답 + - 상세한 로깅 및 디버그 정보 + + **응답 시간:** 설정에 따라 {config.MIN_MAX_TIME//60}-{config.MAX_MAX_TIME//60}분 소요 + + **환경:** 로컬 개발 버전 + """, + responses={ + 200: {"description": "분석 성공", "model": ReviewAnalysisResponse}, + 400: {"description": "잘못된 요청", "model": ErrorResponse}, + 500: {"description": "서버 오류", "model": ErrorResponse} + } +) +async def analyze_reviews(request: ReviewAnalysisRequest): + """리뷰 분석 API (수정됨: environment 섹션 제거)""" + + logger.info("="*60) + logger.info("analyze_reviews API 호출") + logger.info("="*60) + logger.info(f"요청 파라미터: {request}") + + # Selenium 사용 가능성 확인 + if not SELENIUM_AVAILABLE: + error_detail = { + "success": False, + "error": "SELENIUM_NOT_AVAILABLE", + "message": "Selenium이 설치되지 않았습니다. pip install selenium webdriver-manager", + "timestamp": datetime.now().isoformat(), + "debug_info": { + "import_errors": SELENIUM_IMPORT_ERRORS, + "python_version": sys.version, + "platform": platform.platform(), + "chrome_available": chrome_available + } + } + logger.error("Selenium 사용 불가능!") + logger.error(f"Import 오류: {SELENIUM_IMPORT_ERRORS}") + + raise HTTPException(status_code=500, detail=error_detail) + + # Chrome 사용 가능성 확인 + if not chrome_available: + error_detail = { + "success": False, + "error": "CHROME_NOT_AVAILABLE", + "message": "Chrome 브라우저가 설치되지 않았습니다.", + "timestamp": datetime.now().isoformat() + } + logger.error("Chrome 브라우저 사용 불가능!") + raise HTTPException(status_code=500, detail=error_detail) + + start_time = time.time() + + try: + logger.info(f"리뷰 분석 시작: store_id={request.store_id}, days_limit={request.days_limit}, max_time={request.max_time}") + + # 분석기 실행 + def run_analysis(): + analyzer = KakaoReviewAnalyzerAPI() + return analyzer.analyze_by_store_id( + store_id=request.store_id, + days_limit=request.days_limit, + max_scroll_time=request.max_time + ) + + # 스레드에서 실행 + loop = asyncio.get_event_loop() + store_info, reviews = await loop.run_in_executor(None, run_analysis) + + execution_time = time.time() - start_time + + # 응답 데이터 구성 (environment 섹션 제거됨) + response_data = ReviewAnalysisResponse( + success=True, + message="분석이 성공적으로 완료되었습니다.", + store_info=StoreInfo(**store_info) if store_info else None, + reviews=[ + ReviewData( + reviewer_name=review['reviewer_name'], + reviewer_level=review['reviewer_level'], + reviewer_stats=ReviewerStats(**review['reviewer_stats']), + rating=review['rating'], + date=review['date'], + content=review['content'], + badges=review['badges'], + likes=review['likes'], + photo_count=review['photo_count'], + has_photos=review['has_photos'] + ) for review in reviews + ], + analysis_date=datetime.now().isoformat(), + total_reviews=len(reviews), + date_filter=DateFilter( + cutoff_date=(datetime.now() - timedelta(days=request.days_limit)).isoformat() if request.days_limit else None, + filtered=request.days_limit is not None + ), + execution_time=execution_time + ) + + logger.info(f"분석 완료: {len(reviews)}개 리뷰, {execution_time:.1f}초 소요") + logger.info("="*60) + return response_data + + except Exception as e: + execution_time = time.time() - start_time + logger.error(f"분석 실패: {str(e)}") + logger.error("="*60) + + raise HTTPException( + status_code=500, + detail={ + "success": False, + "error": "ANALYSIS_FAILED", + "message": f"리뷰 분석 중 오류가 발생했습니다: {str(e)}", + "timestamp": datetime.now().isoformat(), + "execution_time": execution_time, + "debug_info": { + "selenium_available": SELENIUM_AVAILABLE, + "chrome_available": chrome_available, + "import_errors": SELENIUM_IMPORT_ERRORS, + "request_params": { + "store_id": request.store_id, + "days_limit": request.days_limit, + "max_time": request.max_time + } + } + } + ) + +@app.get("/diagnostic", summary="상세 진단 정보", description="시스템 상태를 상세히 진단합니다.") +async def diagnostic(): + """상세 진단 정보 제공""" + + # 패키지 버전 확인 + package_info = {} + required_packages = ['selenium', 'webdriver-manager', 'beautifulsoup4', 'fastapi', 'uvicorn', 'pydantic'] + + for package in required_packages: + try: + result = subprocess.run([sys.executable, '-m', 'pip', 'show', package], + capture_output=True, text=True) + if result.returncode == 0: + version_line = [line for line in result.stdout.split('\n') if line.startswith('Version:')] + version = version_line[0].split(':')[1].strip() if version_line else 'Unknown' + package_info[package] = {"installed": True, "version": version} + else: + package_info[package] = {"installed": False, "version": None} + except Exception as e: + package_info[package] = {"installed": False, "error": str(e)} + + return { + "system_info": { + "python_version": sys.version, + "platform": platform.platform(), + "architecture": platform.architecture(), + "current_directory": os.getcwd(), + "python_path": sys.path[:5] # 처음 5개만 + }, + "selenium_info": { + "available": SELENIUM_AVAILABLE, + "import_errors": SELENIUM_IMPORT_ERRORS, + "imported_modules": list(imported_modules.keys()) if SELENIUM_AVAILABLE else [] + }, + "chrome_info": { + "available": chrome_available, + "common_paths_checked": [ + '/usr/bin/google-chrome', + '/usr/bin/google-chrome-stable', + '/usr/bin/chromium-browser', + '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome' + ] + }, + "package_info": package_info, + "timestamp": datetime.now().isoformat() + } + +@app.get( + "/health", + summary="헬스 체크", + description="API 서버 상태를 확인합니다." +) +async def health_check(): + """헬스 체크 엔드포인트 (진단 정보 포함)""" + return { + "status": "healthy", + "timestamp": datetime.now().isoformat(), + "selenium_available": SELENIUM_AVAILABLE, + "chrome_available": chrome_available, + "version": config.APP_VERSION, + "message": f"{config.APP_TITLE}이 정상 작동 중입니다." if SELENIUM_AVAILABLE else "Selenium 모듈 문제로 제한된 기능만 사용 가능합니다." + } + +@app.get("/config", summary="환경 설정 확인", description="현재 적용된 환경 변수 설정을 확인합니다.") +async def get_config(): + """환경 설정 확인 엔드포인트""" + return { + "app_info": { + "title": config.APP_TITLE, + "version": config.APP_VERSION, + "description": config.APP_DESCRIPTION + }, + "server_config": { + "host": config.HOST, + "port": config.PORT, + "workers": config.WORKERS, + "log_level": config.LOG_LEVEL + }, + "api_defaults": { + "default_max_time": config.DEFAULT_MAX_TIME, + "default_days_limit": config.DEFAULT_DAYS_LIMIT, + "max_days_limit": config.MAX_DAYS_LIMIT, + "min_max_time": config.MIN_MAX_TIME, + "max_max_time": config.MAX_MAX_TIME + }, + "scroll_settings": { + "check_interval": config.SCROLL_CHECK_INTERVAL, + "no_change_limit": config.SCROLL_NO_CHANGE_LIMIT, + "wait_time_short": config.SCROLL_WAIT_TIME_SHORT, + "wait_time_long": config.SCROLL_WAIT_TIME_LONG + }, + "chrome_options": { + "count": len(config.CHROME_OPTIONS_LIST), + "options": config.CHROME_OPTIONS_LIST + }, + "features": { + "legal_warning_enabled": config.LEGAL_WARNING_ENABLED, + "contact_email": config.CONTACT_EMAIL, + "health_check_timeout": config.HEALTH_CHECK_TIMEOUT + }, + "diagnosis": { + "selenium_available": SELENIUM_AVAILABLE, + "chrome_available": chrome_available, + "import_error_count": len(SELENIUM_IMPORT_ERRORS) + }, + "timestamp": datetime.now().isoformat() + } + +@app.get("/legal-warning", summary="법적 경고사항", description="API 사용 시 주의해야 할 법적 사항을 안내합니다.") +async def legal_warning(): + """법적 경고사항 (환경변수 활용)""" + return { + "warning": f"⚠️ {config.APP_DESCRIPTION}", + "legal_risks": [ + "카카오 이용약관 위반 → 계정 정지, 법적 조치", + "개인정보보호법(PIPA) 위반 → 과태료 최대 3억원", + "저작권 및 데이터베이스권 침해 → 손해배상", + "업무방해죄 → 5년 이하 징역 또는 1천500만원 이하 벌금" + ], + "legal_alternatives": [ + "카카오 공식 API 활용 (developers.kakao.com)", + "점주 대상 자체 가게 관리 서비스 개발", + "사용자 동의 기반 데이터 수집 앱", + "카카오와 정식 파트너십 체결" + ], + "message": "이 API를 실제 서비스에 사용하지 마세요!", + "contact": config.CONTACT_EMAIL, + "version": config.APP_VERSION, + "timestamp": datetime.now().isoformat() + } + +if __name__ == "__main__": + import uvicorn + + print("🚨 " + "="*60) + print(" ⚠️ 중요 법적 경고사항 ⚠️") + print("="*64) + print(f"❌ {config.APP_DESCRIPTION}") + print("❌ 실제 웹사이트 크롤링은 불법입니다.") + print("✅ 카카오 공식 API를 사용하세요: developers.kakao.com") + print("="*64) + print() + + print(f"🚀 {config.APP_TITLE} 서버 시작") + print(f"📊 진단 정보:") + print(f" - Selenium 사용 가능: {SELENIUM_AVAILABLE}") + print(f" - Chrome 설치됨: {chrome_available}") + print(f" - Import 오류: {len(SELENIUM_IMPORT_ERRORS)}개") + print() + print(f"📚 Swagger 문서: http://{config.HOST}:{config.PORT}/docs") + print(f"📖 ReDoc 문서: http://{config.HOST}:{config.PORT}/redoc") + print(f"🏠 메인 페이지: http://{config.HOST}:{config.PORT}/") + print(f"🔧 진단 정보: http://{config.HOST}:{config.PORT}/diagnostic") + print() + + uvicorn.run( + app, + host=config.HOST, + port=config.PORT, + log_level=config.LOG_LEVEL + ) + diff --git a/review/app/requirements.txt b/review/app/requirements.txt new file mode 100644 index 0000000..9549d8a --- /dev/null +++ b/review/app/requirements.txt @@ -0,0 +1,9 @@ +fastapi==0.104.1 +uvicorn[standard]==0.24.0 +beautifulsoup4==4.12.2 +selenium==4.15.2 +webdriver-manager==4.0.1 +pydantic==2.5.0 +lxml==4.9.3 +python-multipart==0.0.6 +python-dotenv==1.0.0 \ No newline at end of file diff --git a/review/build-base.sh b/review/build-base.sh new file mode 100755 index 0000000..1981048 --- /dev/null +++ b/review/build-base.sh @@ -0,0 +1,217 @@ +#!/bin/bash +# build-base.sh - Base Image 빌드 스크립트 + +set -e + +# 변수 설정 +BASE_IMAGE_NAME="kakao-review-api-base" +BASE_IMAGE_TAG="${1:-latest}" +ACR_NAME="${2:-acrdigitalgarage03}" # ACR 이름 (예: acrdigitalgarage01) +RESOURCE_GROUP="${3:-rg-digitalgarage-03}" # 리소스 그룹 + +# ACR URL 자동 구성 +if [ -n "${ACR_NAME}" ]; then + REGISTRY="${ACR_NAME}.azurecr.io" + FULL_BASE_IMAGE_NAME="${REGISTRY}/${BASE_IMAGE_NAME}:${BASE_IMAGE_TAG}" +else + FULL_BASE_IMAGE_NAME="${BASE_IMAGE_NAME}:${BASE_IMAGE_TAG}" +fi + +# 고정된 Dockerfile 경로 +BASE_DOCKERFILE_PATH="deployment/container/Dockerfile-base" +BUILD_CONTEXT="." + +echo "=====================================================" +echo " 카카오맵 리뷰 분석 API Base Image 빌드" +echo "=====================================================" +echo "Base 이미지명: ${FULL_BASE_IMAGE_NAME}" +if [ -n "${ACR_NAME}" ]; then + echo "ACR 이름: ${ACR_NAME}" + echo "리소스 그룹: ${RESOURCE_GROUP}" +fi +echo "빌드 시작: $(date)" +echo "" + +# 사용법 표시 함수 +show_usage() { + echo "사용법:" + echo " $0 [BASE_IMAGE_TAG] [ACR_NAME] [RESOURCE_GROUP]" + echo "" + echo "파라미터:" + echo " BASE_IMAGE_TAG: Base 이미지 태그 (기본값: latest)" + echo " ACR_NAME : Azure Container Registry 이름" + echo " RESOURCE_GROUP: Azure 리소스 그룹" + echo "" + echo "예시:" + echo " $0 v1.0.0 # 로컬 빌드만" + echo " $0 v1.0.0 acrdigitalgarage01 rg-digitalgarage-03 # ACR 빌드 + 푸시" + echo "" + echo "주의: Base Image는 자주 빌드할 필요가 없습니다." + echo " Chrome 업데이트나 기본 패키지 변경 시에만 재빌드하세요." +} + +# ACR 로그인 함수 +acr_login() { + local acr_name="$1" + local resource_group="$2" + + echo "🔐 Azure Container Registry 로그인 중..." + + if ! command -v az &> /dev/null; then + echo "❌ Azure CLI (az)가 설치되지 않았습니다." + exit 1 + fi + + if ! command -v jq &> /dev/null; then + echo "❌ jq가 설치되지 않았습니다." + exit 1 + fi + + if ! az account show &> /dev/null; then + echo "❌ Azure에 로그인되지 않았습니다." + echo "로그인 명령: az login" + exit 1 + fi + + local credential_json + credential_json=$(az acr credential show --name "${acr_name}" --resource-group "${resource_group}" 2>/dev/null) + + if [ $? -ne 0 ]; then + echo "❌ ACR credential 조회 실패" + exit 1 + fi + + local username + local password + + username=$(echo "${credential_json}" | jq -r '.username') + password=$(echo "${credential_json}" | jq -r '.passwords[0].value') + + if [ -z "${username}" ] || [ -z "${password}" ] || [ "${username}" == "null" ] || [ "${password}" == "null" ]; then + echo "❌ ACR credential 파싱 실패" + exit 1 + fi + + echo "🔐 Docker 로그인 실행 중..." + echo "${password}" | docker login "${REGISTRY}" -u "${username}" --password-stdin + + if [ $? -eq 0 ]; then + echo "✅ ACR 로그인 성공!" + return 0 + else + echo "❌ ACR 로그인 실패" + exit 1 + fi +} + +# 파라미터 검증 +if [ "$1" == "--help" ] || [ "$1" == "-h" ]; then + show_usage + exit 0 +fi + +if [ -n "${ACR_NAME}" ] && [ -z "${RESOURCE_GROUP}" ]; then + echo "❌ ACR_NAME이 제공된 경우 RESOURCE_GROUP도 필요합니다." + echo "" + show_usage + exit 1 +fi + +# 필수 파일 확인 +echo "📁 필수 파일 확인 중..." + +if [ ! -f "${BASE_DOCKERFILE_PATH}" ]; then + echo "❌ ${BASE_DOCKERFILE_PATH} 파일을 찾을 수 없습니다." + exit 1 +fi + +echo "✅ Base Dockerfile 확인 완료" +echo "📄 Base Dockerfile: ${BASE_DOCKERFILE_PATH}" + +# ACR 로그인 수행 +if [ -n "${ACR_NAME}" ] && [ -n "${RESOURCE_GROUP}" ]; then + echo "" + acr_login "${ACR_NAME}" "${RESOURCE_GROUP}" + echo "" +fi + +# Docker 빌드 +echo "🔨 Base Image 빌드 시작... (시간이 다소 소요됩니다)" +echo "명령어: docker build -t \"${FULL_BASE_IMAGE_NAME}\" -f \"${BASE_DOCKERFILE_PATH}\" \"${BUILD_CONTEXT}\"" + +docker build -t "${FULL_BASE_IMAGE_NAME}" -f "${BASE_DOCKERFILE_PATH}" "${BUILD_CONTEXT}" + +if [ $? -eq 0 ]; then + echo "✅ Base Image 빌드 완료!" + echo "이미지명: ${FULL_BASE_IMAGE_NAME}" + + # 이미지 정보 표시 + echo "" + echo "📊 Base Image 정보:" + docker images "${FULL_BASE_IMAGE_NAME}" --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}\t{{.CreatedAt}}" + + # latest 태그 추가 생성 + if [ "${BASE_IMAGE_TAG}" != "latest" ] && [ -n "${REGISTRY}" ]; then + echo "" + echo "🏷️ latest 태그 생성 중..." + docker tag "${FULL_BASE_IMAGE_NAME}" "${REGISTRY}/${BASE_IMAGE_NAME}:latest" + echo "✅ latest 태그 생성 완료: ${REGISTRY}/${BASE_IMAGE_NAME}:latest" + fi + + # ACR 푸시 + if [ -n "${ACR_NAME}" ]; then + echo "" + echo "🚀 ACR에 Base Image 푸시 중..." + + echo "📤 푸시 중: ${FULL_BASE_IMAGE_NAME}" + docker push "${FULL_BASE_IMAGE_NAME}" + + if [ $? -eq 0 ]; then + echo "✅ Base Image 푸시 성공" + + if [ "${BASE_IMAGE_TAG}" != "latest" ]; then + echo "📤 푸시 중: ${REGISTRY}/${BASE_IMAGE_NAME}:latest" + docker push "${REGISTRY}/${BASE_IMAGE_NAME}:latest" + + if [ $? -eq 0 ]; then + echo "✅ latest 태그 푸시 성공" + fi + fi + else + echo "❌ Base Image 푸시 실패" + exit 1 + fi + fi + + echo "" + echo "🎉 Base Image 빌드 완료!" + echo "" + echo "📋 완료된 작업:" + echo " ✅ Base Image 빌드: ${FULL_BASE_IMAGE_NAME}" + if [ "${BASE_IMAGE_TAG}" != "latest" ] && [ -n "${REGISTRY}" ]; then + echo " ✅ latest 태그: ${REGISTRY}/${BASE_IMAGE_NAME}:latest" + fi + if [ -n "${ACR_NAME}" ]; then + echo " ✅ ACR 푸시 완료" + fi + + echo "" + echo "🧪 테스트 명령어:" + echo " docker run --rm ${FULL_BASE_IMAGE_NAME} google-chrome --version" + + echo "" + echo "📝 다음 단계:" + echo " 이제 Service Image를 빌드하세요:" + if [ -n "${ACR_NAME}" ]; then + echo " ./scripts/build.sh v1.0.0 ${ACR_NAME} ${RESOURCE_GROUP}" + else + echo " ./scripts/build.sh v1.0.0" + fi + +else + echo "❌ Base Image 빌드 실패!" + exit 1 +fi + +echo "" +echo "🏁 Base Image 빌드 프로세스 완료 - $(date)" diff --git a/review/build.sh b/review/build.sh new file mode 100755 index 0000000..8dd35f6 --- /dev/null +++ b/review/build.sh @@ -0,0 +1,257 @@ +#!/bin/bash +# build.sh - Service Image 빌드 스크립트 (Base Image 활용) + +set -e + +# 변수 설정 +IMAGE_NAME="kakao-review-api" +IMAGE_TAG="${1:-latest}" +ACR_NAME="${2:-acrdigitalgarage03}" # ACR 이름 (예: acrdigitalgarage01) +RESOURCE_GROUP="${3:-rg-digitalgarage-03}" # 리소스 그룹 +BASE_IMAGE_TAG="${4:-latest}" # Base Image 태그 + +# ACR URL 자동 구성 +if [ -n "${ACR_NAME}" ]; then + REGISTRY="${ACR_NAME}.azurecr.io" + FULL_IMAGE_NAME="${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}" + BASE_IMAGE="${REGISTRY}/kakao-review-api-base:${BASE_IMAGE_TAG}" +else + FULL_IMAGE_NAME="${IMAGE_NAME}:${IMAGE_TAG}" + BASE_IMAGE="kakao-review-api-base:${BASE_IMAGE_TAG}" +fi + +# 고정된 Dockerfile 경로 +DOCKERFILE_PATH="deployment/container/Dockerfile" +BUILD_CONTEXT="." + +echo "=====================================================" +echo " 카카오맵 리뷰 분석 API Service Image 빌드" +echo "=====================================================" +echo "Service 이미지명: ${FULL_IMAGE_NAME}" +echo "Base 이미지: ${BASE_IMAGE}" +if [ -n "${ACR_NAME}" ]; then + echo "ACR 이름: ${ACR_NAME}" + echo "리소스 그룹: ${RESOURCE_GROUP}" +fi +echo "빌드 시작: $(date)" +echo "" + +# 사용법 표시 함수 +show_usage() { + echo "사용법:" + echo " $0 [IMAGE_TAG] [ACR_NAME] [RESOURCE_GROUP] [BASE_IMAGE_TAG]" + echo "" + echo "파라미터:" + echo " IMAGE_TAG : Service 이미지 태그 (기본값: latest)" + echo " ACR_NAME : Azure Container Registry 이름" + echo " RESOURCE_GROUP: Azure 리소스 그룹" + echo " BASE_IMAGE_TAG: Base 이미지 태그 (기본값: latest)" + echo "" + echo "예시:" + echo " $0 v1.0.0 # 로컬 빌드만" + echo " $0 v1.0.0 acrdigitalgarage01 rg-digitalgarage-03 # ACR 빌드 + 푸시" + echo " $0 v1.0.0 acrdigitalgarage01 rg-digitalgarage-03 v2.0.0 # 특정 Base Image 사용" + echo "" + echo "전제조건:" + echo " Base Image가 먼저 빌드되어 있어야 합니다:" + echo " ./scripts/build-base.sh ${BASE_IMAGE_TAG} [ACR_NAME] [RESOURCE_GROUP]" +} + +# ACR 로그인 함수 +acr_login() { + local acr_name="$1" + local resource_group="$2" + + echo "🔐 Azure Container Registry 로그인 중..." + + if ! command -v az &> /dev/null; then + echo "❌ Azure CLI (az)가 설치되지 않았습니다." + exit 1 + fi + + if ! command -v jq &> /dev/null; then + echo "❌ jq가 설치되지 않았습니다." + exit 1 + fi + + if ! az account show &> /dev/null; then + echo "❌ Azure에 로그인되지 않았습니다." + echo "로그인 명령: az login" + exit 1 + fi + + local credential_json + credential_json=$(az acr credential show --name "${acr_name}" --resource-group "${resource_group}" 2>/dev/null) + + if [ $? -ne 0 ]; then + echo "❌ ACR credential 조회 실패" + exit 1 + fi + + local username + local password + + username=$(echo "${credential_json}" | jq -r '.username') + password=$(echo "${credential_json}" | jq -r '.passwords[0].value') + + if [ -z "${username}" ] || [ -z "${password}" ] || [ "${username}" == "null" ] || [ "${password}" == "null" ]; then + echo "❌ ACR credential 파싱 실패" + exit 1 + fi + + echo "🔐 Docker 로그인 실행 중..." + echo "${password}" | docker login "${REGISTRY}" -u "${username}" --password-stdin + + if [ $? -eq 0 ]; then + echo "✅ ACR 로그인 성공!" + return 0 + else + echo "❌ ACR 로그인 실패" + exit 1 + fi +} + +# 파라미터 검증 +if [ "$1" == "--help" ] || [ "$1" == "-h" ]; then + show_usage + exit 0 +fi + +if [ -n "${ACR_NAME}" ] && [ -z "${RESOURCE_GROUP}" ]; then + echo "❌ ACR_NAME이 제공된 경우 RESOURCE_GROUP도 필요합니다." + echo "" + show_usage + exit 1 +fi + +# 법적 경고 +echo "⚠️ 중요 법적 경고사항 ⚠️" +echo "이 이미지는 교육 목적으로만 사용하세요!" +echo "실제 서비스 사용 시 법적 책임을 질 수 있습니다." +echo "" + +# 필수 파일 확인 +echo "📁 필수 파일 확인 중..." + +if [ ! -f "app/main.py" ]; then + echo "❌ app/main.py 파일을 찾을 수 없습니다." + exit 1 +fi + +if [ ! -f "app/requirements.txt" ]; then + echo "❌ app/requirements.txt 파일을 찾을 수 없습니다." + exit 1 +fi + +if [ ! -f "${DOCKERFILE_PATH}" ]; then + echo "❌ ${DOCKERFILE_PATH} 파일을 찾을 수 없습니다." + exit 1 +fi + +echo "✅ 모든 필수 파일이 확인되었습니다." +echo "📄 Dockerfile: ${DOCKERFILE_PATH}" +echo "🏗️ 빌드 컨텍스트: ${BUILD_CONTEXT}" + +# Base Image 존재 확인 +echo "" +echo "🔍 Base Image 확인 중..." +if docker image inspect "${BASE_IMAGE}" >/dev/null 2>&1; then + echo "✅ Base Image 확인됨: ${BASE_IMAGE}" +else + echo "❌ Base Image를 찾을 수 없습니다: ${BASE_IMAGE}" + echo "" + echo "Base Image를 먼저 빌드하세요:" + if [ -n "${ACR_NAME}" ]; then + echo " ./scripts/build-base.sh ${BASE_IMAGE_TAG} ${ACR_NAME} ${RESOURCE_GROUP}" + else + echo " ./scripts/build-base.sh ${BASE_IMAGE_TAG}" + fi + exit 1 +fi + +# ACR 로그인 수행 +if [ -n "${ACR_NAME}" ] && [ -n "${RESOURCE_GROUP}" ]; then + echo "" + acr_login "${ACR_NAME}" "${RESOURCE_GROUP}" + echo "" +fi + +# Docker 빌드 +echo "🔨 Service Image 빌드 시작... (빠른 빌드 예상)" +echo "명령어: docker build --build-arg BASE_IMAGE=\"${BASE_IMAGE}\" -t \"${FULL_IMAGE_NAME}\" -f \"${DOCKERFILE_PATH}\" \"${BUILD_CONTEXT}\"" + +docker build --build-arg BASE_IMAGE="${BASE_IMAGE}" -t "${FULL_IMAGE_NAME}" -f "${DOCKERFILE_PATH}" "${BUILD_CONTEXT}" + +if [ $? -eq 0 ]; then + echo "✅ Service Image 빌드 완료!" + echo "이미지명: ${FULL_IMAGE_NAME}" + + # 이미지 정보 표시 + echo "" + echo "📊 Service Image 정보:" + docker images "${FULL_IMAGE_NAME}" --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}\t{{.CreatedAt}}" + + # latest 태그 추가 생성 + if [ "${IMAGE_TAG}" != "latest" ] && [ -n "${REGISTRY}" ]; then + echo "" + echo "🏷️ latest 태그 생성 중..." + docker tag "${FULL_IMAGE_NAME}" "${REGISTRY}/${IMAGE_NAME}:latest" + echo "✅ latest 태그 생성 완료: ${REGISTRY}/${IMAGE_NAME}:latest" + fi + + # ACR 푸시 + if [ -n "${ACR_NAME}" ]; then + echo "" + echo "🚀 ACR에 Service Image 푸시 중..." + + echo "📤 푸시 중: ${FULL_IMAGE_NAME}" + docker push "${FULL_IMAGE_NAME}" + + if [ $? -eq 0 ]; then + echo "✅ Service Image 푸시 성공" + + if [ "${IMAGE_TAG}" != "latest" ]; then + echo "📤 푸시 중: ${REGISTRY}/${IMAGE_NAME}:latest" + docker push "${REGISTRY}/${IMAGE_NAME}:latest" + + if [ $? -eq 0 ]; then + echo "✅ latest 태그 푸시 성공" + fi + fi + else + echo "❌ Service Image 푸시 실패" + exit 1 + fi + fi + + echo "" + echo "🎉 Service Image 빌드 완료!" + echo "" + echo "📋 완료된 작업:" + echo " ✅ Service Image 빌드: ${FULL_IMAGE_NAME}" + echo " ✅ 사용된 Base Image: ${BASE_IMAGE}" + if [ "${IMAGE_TAG}" != "latest" ] && [ -n "${REGISTRY}" ]; then + echo " ✅ latest 태그: ${REGISTRY}/${IMAGE_NAME}:latest" + fi + if [ -n "${ACR_NAME}" ]; then + echo " ✅ ACR 푸시 완료" + fi + + echo "" + echo "🧪 테스트 명령어:" + echo " docker run -p 8000:8000 ${FULL_IMAGE_NAME}" + echo " curl http://localhost:8000/health" + + if [ -n "${ACR_NAME}" ]; then + echo "" + echo "🔍 ACR 이미지 확인:" + echo " az acr repository show-tags --name ${ACR_NAME} --repository ${IMAGE_NAME}" + fi + +else + echo "❌ Service Image 빌드 실패!" + exit 1 +fi + +echo "" +echo "🏁 Service Image 빌드 프로세스 완료 - $(date)" \ No newline at end of file diff --git a/review/create-imagepullsecret.sh b/review/create-imagepullsecret.sh new file mode 100755 index 0000000..d9287c1 --- /dev/null +++ b/review/create-imagepullsecret.sh @@ -0,0 +1,103 @@ +#!/bin/bash +# create-image-pull-secret.sh - ACR Image Pull Secret 생성 스크립트 + +set -e + +# 변수 설정 +ACR_NAME="${1:-acrdigitalgarage03}" +RESOURCE_GROUP="${2:-rg-digitalgarage-03}" +SECRET_NAME="${3:-acr-secret}" + +echo "=====================================================" +echo " ACR Image Pull Secret 생성" +echo "=====================================================" + +# 사용법 표시 함수 +show_usage() { + echo "사용법:" + echo " $0 [ACR_NAME] [RESOURCE_GROUP] [SECRET_NAME]" + echo "" + echo "파라미터:" + echo " ACR_NAME : Azure Container Registry 이름 (필수)" + echo " RESOURCE_GROUP: Azure 리소스 그룹 (필수)" + echo " SECRET_NAME : Secret 이름 (기본값: acr-secret)" + echo "" + echo "예시:" + echo " $0 acrdigitalgarage01 rg-digitalgarage-03" + echo " $0 acrdigitalgarage01 rg-digitalgarage-03 acr-prod-secret" + echo "" +} + +# 파라미터 검증 +if [ "$1" == "--help" ] || [ "$1" == "-h" ]; then + show_usage + exit 0 +fi + +if [ -z "${ACR_NAME}" ] || [ -z "${RESOURCE_GROUP}" ]; then + echo "❌ ACR_NAME과 RESOURCE_GROUP는 필수 파라미터입니다." + echo "" + show_usage + exit 1 +fi + +# ACR 정보 설정 +REGISTRY_URL="${ACR_NAME}.azurecr.io" + +echo "" +echo "📋 ACR 정보:" +echo " ACR 이름: ${ACR_NAME}" +echo " 레지스트리 URL: ${REGISTRY_URL}" +echo " 리소스 그룹: ${RESOURCE_GROUP}" +echo " Secret 이름: ${SECRET_NAME}" + +# ACR credential 조회 +echo "" +echo "🔑 ACR credential 조회 중..." + +credential_json=$(az acr credential show --name "${ACR_NAME}" --resource-group "${RESOURCE_GROUP}" 2>/dev/null) + +if [ $? -ne 0 ]; then + echo "❌ ACR credential 조회 실패" + echo "확인 사항:" + echo " - ACR 이름: ${ACR_NAME}" + echo " - 리소스 그룹: ${RESOURCE_GROUP}" + echo " - Azure 권한" + exit 1 +fi + +# JSON에서 username과 password 추출 +username=$(echo "${credential_json}" | jq -r '.username') +password=$(echo "${credential_json}" | jq -r '.passwords[0].value') + +if [ -z "${username}" ] || [ -z "${password}" ] || [ "${username}" == "null" ] || [ "${password}" == "null" ]; then + echo "❌ ACR credential 파싱 실패" + exit 1 +fi + +echo "✅ ACR credential 조회 성공" +echo " 사용자명: ${username}" +echo " 비밀번호: ${password:0:10}..." + +# 기존 Secret 삭제 (있는 경우) +if kubectl get secret "${SECRET_NAME}" &> /dev/null; then + echo "" + echo "🗑️ 기존 Secret 삭제 중..." + kubectl delete secret "${SECRET_NAME}" +fi + +# Image Pull Secret 생성 +echo "" +echo "🔐 Image Pull Secret 생성 중..." + +kubectl create secret docker-registry "${SECRET_NAME}" \ + --docker-server="${REGISTRY_URL}" \ + --docker-username="${username}" \ + --docker-password="${password}" + +if [ $? -eq 0 ]; then + echo "✅ Image Pull Secret 생성 성공!" +else + echo "❌ Image Pull Secret 생성 실패" + exit 1 +fi diff --git a/review/deployment/container/Dockerfile b/review/deployment/container/Dockerfile new file mode 100644 index 0000000..9012187 --- /dev/null +++ b/review/deployment/container/Dockerfile @@ -0,0 +1,72 @@ +# deployment/container/Dockerfile +# Service Image - Base Image 위에 애플리케이션만 추가 (AKS 최적화) +ARG BASE_IMAGE=kakao-review-api-base:latest +FROM ${BASE_IMAGE} + +# 메타데이터 +LABEL maintainer="admin@example.com" +LABEL version="1.0.1" +LABEL description="카카오맵 리뷰 분석 API - Service Image (AKS 최적화)" + +# root로 전환 (패키지 설치용) +USER root + +# 🔧 Chrome 실행을 위한 환경 변수 설정 (VM 환경과 동일) +ENV HOME=/home/appuser \ + WDM_LOCAL=1 \ + WDM_LOG_LEVEL=0 \ + CHROME_BIN=/usr/bin/google-chrome \ + CHROMEDRIVER_BIN=/usr/local/bin/chromedriver \ + DISPLAY=:99 \ + DBUS_SESSION_BUS_ADDRESS=/dev/null + +# Python 의존성 파일 복사 및 설치 +COPY app/requirements.txt /app/ +RUN pip install --no-cache-dir -r /app/requirements.txt + +# 애플리케이션 소스 복사 +COPY app/main.py /app/ + +# 🔧 Chrome 실행을 위한 디렉토리 및 권한 설정 (VM 환경과 동일) +RUN mkdir -p /home/appuser/.cache/selenium \ + && mkdir -p /home/appuser/.wdm \ + && mkdir -p /home/appuser/.local/share \ + && mkdir -p /tmp/chrome-user-data \ + && mkdir -p /tmp/.wdm \ + && chown -R appuser:appuser /home/appuser \ + && chown -R appuser:appuser /tmp/chrome-user-data \ + && chown -R appuser:appuser /tmp/.wdm \ + && chmod -R 755 /home/appuser \ + && chmod -R 777 /tmp/chrome-user-data \ + && chmod -R 777 /tmp/.wdm + +# /app 디렉토리 권한 설정 +RUN chown -R appuser:appuser /app + +# 🔧 ChromeDriver 접근 권한 확인 및 테스트 +RUN echo "=== ChromeDriver 정보 ===" \ + && ls -la /usr/local/bin/chromedriver \ + && chromedriver --version \ + && echo "=== Chrome 정보 ===" \ + && google-chrome --version \ + && echo "=== 권한 테스트 ===" \ + && su - appuser -c "chromedriver --version" || echo "appuser ChromeDriver 접근 확인" + +# 🔧 간단한 Selenium import 테스트만 수행 +RUN python3 -c "from selenium import webdriver; from selenium.webdriver.chrome.options import Options; from selenium.webdriver.chrome.service import Service; print('✅ Selenium 모듈 import 성공')" + +# 🔧 컨테이너 내 사용자를 appuser로 변경 (하지만 Chrome은 root 권한 필요) +USER appuser + +# 작업 디렉토리 설정 +WORKDIR /app + +# 포트 노출 (review 서비스용) +EXPOSE 19000 + +# 🔧 헬스체크 (개선된 타임아웃) +HEALTHCHECK --interval=30s --timeout=15s --start-period=60s --retries=3 \ + CMD curl -f http://localhost:19000/health || exit 1 + +# 🔧 애플리케이션 실행 (로그 레벨 설정) +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "19000", "--log-level", "info"] diff --git a/review/deployment/container/Dockerfile-base b/review/deployment/container/Dockerfile-base new file mode 100644 index 0000000..48595e6 --- /dev/null +++ b/review/deployment/container/Dockerfile-base @@ -0,0 +1,103 @@ +# deployment/container/Dockerfile-base +FROM python:3.11-slim + +# 메타데이터 +LABEL maintainer="admin@example.com" +LABEL description="카카오맵 리뷰 분석 API - Base Image with Chrome (AKS 최적화)" +LABEL version="base-1.0.2" + +# 환경 변수 설정 +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + DEBIAN_FRONTEND=noninteractive \ + CHROME_BIN=/usr/bin/google-chrome \ + CHROMEDRIVER_BIN=/usr/local/bin/chromedriver + +# 🔧 필수 패키지 설치 (VM setup.sh와 동일한 패키지들) +RUN apt-get update && apt-get install -y \ + python3-pip \ + python3-venv \ + unzip \ + wget \ + curl \ + ca-certificates \ + fonts-liberation \ + libasound2 \ + libatk-bridge2.0-0 \ + libdrm2 \ + libxcomposite1 \ + libxdamage1 \ + libxrandr2 \ + libgbm1 \ + libxss1 \ + libnss3 \ + libxext6 \ + libxfixes3 \ + libxi6 \ + libxrender1 \ + libcairo-gobject2 \ + libgtk-3-0 \ + libgdk-pixbuf2.0-0 \ + libgtk-3-dev \ + libgconf-2-4 \ + && rm -rf /var/lib/apt/lists/* + +# 🔧 Google Chrome 설치 (VM setup.sh와 동일한 방식) +RUN wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | apt-key add - \ + && echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google-chrome.list \ + && apt-get update \ + && apt-get install -y google-chrome-stable \ + && rm -rf /var/lib/apt/lists/* + +# Chrome 설치 확인 +RUN google-chrome --version + +# 🔧 ChromeDriver 설치 (VM setup.sh와 동일한 방식) +RUN CHROME_VERSION=$(google-chrome --version | grep -oP '\d+\.\d+\.\d+\.\d+' | cut -d'.' -f1) \ + && echo "Chrome 주 버전: $CHROME_VERSION" \ + && CHROMEDRIVER_VERSION=$(curl -s "https://googlechromelabs.github.io/chrome-for-testing/LATEST_RELEASE_$CHROME_VERSION") \ + && echo "ChromeDriver 버전: $CHROMEDRIVER_VERSION" \ + && wget -q "https://storage.googleapis.com/chrome-for-testing-public/$CHROMEDRIVER_VERSION/linux64/chromedriver-linux64.zip" -O /tmp/chromedriver.zip \ + && unzip -q /tmp/chromedriver.zip -d /tmp/ \ + && mv /tmp/chromedriver-linux64/chromedriver /usr/local/bin/chromedriver \ + && chmod +x /usr/local/bin/chromedriver \ + && rm -rf /tmp/chromedriver* \ + && chromedriver --version + +# Chrome 및 ChromeDriver 최종 확인 +RUN echo "=== Chrome 정보 ===" \ + && google-chrome --version \ + && echo "=== ChromeDriver 정보 ===" \ + && chromedriver --version \ + && echo "=== 설치 경로 확인 ===" \ + && ls -la /usr/bin/google-chrome* \ + && ls -la /usr/local/bin/chromedriver + +# 🔧 Chrome 실행을 위한 디렉토리 생성 +RUN mkdir -p /tmp/chrome-user-data \ + && mkdir -p /home/appuser/.cache/selenium \ + && mkdir -p /home/appuser/.wdm \ + && mkdir -p /home/appuser/.local/share \ + && chmod 777 /tmp/chrome-user-data + +# 🔧 비root 사용자 생성 (하지만 Chrome은 root로 실행) +RUN groupadd -r appuser && useradd -r -g appuser -d /home/appuser -s /bin/bash appuser \ + && mkdir -p /home/appuser \ + && chown -R appuser:appuser /home/appuser + +# 작업 디렉토리 생성 +WORKDIR /app +RUN chown appuser:appuser /app + +# pip 업그레이드 +RUN pip install --no-cache-dir --upgrade pip + +# Chrome 실행 테스트 (빌드 시 검증) +RUN google-chrome --headless --no-sandbox --disable-dev-shm-usage --virtual-time-budget=1000 --run-all-compositor-stages-before-draw data:text/html,\\\Test\\\ || echo "Chrome 테스트 완료" + +# 포트 노출 +EXPOSE 8000 + +# 기본 명령어 (오버라이드 가능) +CMD ["python", "--version"] + diff --git a/review/deployment/manifests/configmap.yaml b/review/deployment/manifests/configmap.yaml new file mode 100644 index 0000000..0e64b6b --- /dev/null +++ b/review/deployment/manifests/configmap.yaml @@ -0,0 +1,78 @@ +# deployment/manifests/configmap.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: kakao-review-api-config +data: + # 애플리케이션 설정 + APP_TITLE: "카카오맵 리뷰 분석 API" + APP_VERSION: "1.0.1" + APP_DESCRIPTION: "교육 목적 전용 - 실제 서비스 사용 금지" + + # 서버 설정 + HOST: "0.0.0.0" + PORT: "8000" + WORKERS: "1" + LOG_LEVEL: "info" + + # API 기본값 설정 (AKS 환경에 최적화) + DEFAULT_MAX_TIME: "300" + DEFAULT_DAYS_LIMIT: "60" + MAX_DAYS_LIMIT: "365" + MIN_MAX_TIME: "60" + MAX_MAX_TIME: "600" + + # 🔧 Chrome 브라우저 설정 (AKS 환경 최적화) + CHROME_OPTIONS: | + --headless=new + --no-sandbox + --disable-dev-shm-usage + --disable-gpu + --disable-software-rasterizer + --window-size=1920,1080 + --disable-extensions + --disable-plugins + --disable-usb-keyboard-detect + --no-first-run + --no-default-browser-check + --disable-logging + --log-level=3 + --disable-background-timer-throttling + --disable-backgrounding-occluded-windows + --disable-renderer-backgrounding + --disable-features=TranslateUI,VizDisplayCompositor + --disable-ipc-flooding-protection + --memory-pressure-off + --max_old_space_size=4096 + --no-zygote + --disable-setuid-sandbox + --disable-background-networking + --disable-default-apps + --disable-sync + --metrics-recording-only + --safebrowsing-disable-auto-update + --disable-prompt-on-repost + --disable-hang-monitor + --disable-client-side-phishing-detection + --disable-component-update + --disable-domain-reliability + --user-data-dir=/tmp/chrome-user-data + --data-path=/tmp/chrome-user-data + --disk-cache-dir=/tmp/chrome-cache + --aggressive-cache-discard + --disable-web-security + --allow-running-insecure-content + --disable-blink-features=AutomationControlled + + # 스크롤링 설정 (AKS 환경에 맞게 조정) + SCROLL_CHECK_INTERVAL: "5" + SCROLL_NO_CHANGE_LIMIT: "6" + SCROLL_WAIT_TIME_SHORT: "2.0" + SCROLL_WAIT_TIME_LONG: "3.0" + + # 법적 경고 메시지 + LEGAL_WARNING_ENABLED: "true" + CONTACT_EMAIL: "admin@example.com" + + # 건강 체크 설정 + HEALTH_CHECK_TIMEOUT: "10" diff --git a/review/deployment/manifests/deployment.yaml b/review/deployment/manifests/deployment.yaml new file mode 100644 index 0000000..226eb1a --- /dev/null +++ b/review/deployment/manifests/deployment.yaml @@ -0,0 +1,130 @@ +# deployment/manifests/deployment.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: kakao-review-api + labels: + app: kakao-review-api + version: v1 +spec: + replicas: 1 + selector: + matchLabels: + app: kakao-review-api + template: + metadata: + labels: + app: kakao-review-api + version: v1 + spec: + imagePullSecrets: + - name: acr-secret + containers: + - name: api + image: acrdigitalgarage03.azurecr.io/kakao-review-api:latest + imagePullPolicy: Always + ports: + - containerPort: 19000 + name: http + + # 🔧 ConfigMap 환경 변수 + envFrom: + - configMapRef: + name: kakao-review-api-config + + # 🔧 Secret 환경 변수 + env: + - name: EXTERNAL_API_KEY + valueFrom: + secretKeyRef: + name: kakao-review-api-secret + key: EXTERNAL_API_KEY + - name: DB_USERNAME + valueFrom: + secretKeyRef: + name: kakao-review-api-secret + key: DB_USERNAME + - name: DB_PASSWORD + valueFrom: + secretKeyRef: + name: kakao-review-api-secret + key: DB_PASSWORD + - name: JWT_SECRET + valueFrom: + secretKeyRef: + name: kakao-review-api-secret + key: JWT_SECRET + # 🔧 Chrome/ChromeDriver 환경 변수 (VM과 동일) + - name: WDM_LOCAL + value: "/tmp/.wdm" + - name: WDM_LOG_LEVEL + value: "0" + - name: CHROME_BIN + value: "/usr/bin/google-chrome" + - name: CHROMEDRIVER_BIN + value: "/usr/local/bin/chromedriver" + - name: DISPLAY + value: ":99" + - name: DBUS_SESSION_BUS_ADDRESS + value: "/dev/null" + + # 🔧 리소스 제한 (Chrome 실행에 충분한 리소스) + resources: + requests: + memory: "2Gi" + cpu: "1000m" + limits: + memory: "4Gi" + cpu: "2000m" + + # 🔧 헬스 체크 (타임아웃 증가) + livenessProbe: + httpGet: + path: /health + port: 19000 + initialDelaySeconds: 60 + periodSeconds: 30 + timeoutSeconds: 15 + failureThreshold: 5 + + readinessProbe: + httpGet: + path: /health + port: 19000 + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 10 + failureThreshold: 5 + + # 🔧 간소화된 보안 컨텍스트 (AKS 호환) + securityContext: + runAsNonRoot: false + runAsUser: 0 + allowPrivilegeEscalation: true + readOnlyRootFilesystem: false + capabilities: + add: + - SYS_ADMIN + drop: [] + + # 🔧 볼륨 마운트 (Chrome 실행 최적화) + volumeMounts: + - name: tmp-volume + mountPath: /tmp + - name: dev-shm + mountPath: /dev/shm + + # 🔧 볼륨 정의 (간소화) + volumes: + - name: tmp-volume + emptyDir: {} + - name: dev-shm + emptyDir: + medium: Memory + sizeLimit: 2Gi + + restartPolicy: Always + + # 🔧 Pod 레벨 보안 설정 제거 (AKS 호환을 위해) + # securityContext: 제거 + diff --git a/review/deployment/manifests/ingress.yaml b/review/deployment/manifests/ingress.yaml new file mode 100644 index 0000000..e11b843 --- /dev/null +++ b/review/deployment/manifests/ingress.yaml @@ -0,0 +1,39 @@ +# deployment/manifests/ingress.yaml +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: kakao-review-api-ingress + annotations: + nginx.ingress.kubernetes.io/rewrite-target: / + nginx.ingress.kubernetes.io/ssl-redirect: "false" + nginx.ingress.kubernetes.io/proxy-body-size: "10m" + # 🔧 타임아웃 설정 (Chrome 분석 시간 고려) + nginx.ingress.kubernetes.io/proxy-read-timeout: "1800" + nginx.ingress.kubernetes.io/proxy-send-timeout: "1800" + nginx.ingress.kubernetes.io/client-body-timeout: "1800" + nginx.ingress.kubernetes.io/proxy-connect-timeout: "60" + # 🔧 CORS 설정 (필요시) + nginx.ingress.kubernetes.io/enable-cors: "true" + nginx.ingress.kubernetes.io/cors-allow-origin: "*" + nginx.ingress.kubernetes.io/cors-allow-methods: "GET, POST, OPTIONS" + nginx.ingress.kubernetes.io/cors-allow-headers: "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization" +spec: + ingressClassName: nginx + rules: + # 🔧 환경에 맞게 호스트명 수정 필요 + - host: kakao-review-api.20.249.191.180.nip.io + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: kakao-review-api-service + port: + number: 80 + # 🔧 TLS 설정 (HTTPS 필요시 주석 해제) + # tls: + # - hosts: + # - kakao-review-api.example.com + # secretName: kakao-review-api-tls + diff --git a/review/deployment/manifests/secret.yaml b/review/deployment/manifests/secret.yaml new file mode 100644 index 0000000..db8b8cb --- /dev/null +++ b/review/deployment/manifests/secret.yaml @@ -0,0 +1,21 @@ +# deployment/manifests/secret.yaml +apiVersion: v1 +kind: Secret +metadata: + name: kakao-review-api-secret +type: Opaque +data: + # 🔧 현재 사용하지 않지만 향후 확장을 위한 플레이스홀더 + # 실제 값 설정 시: echo -n "your-value" | base64 + + # API 키 (향후 카카오 공식 API 연동용) + EXTERNAL_API_KEY: "" + + # 데이터베이스 연결 정보 (향후 DB 연동용) + DB_USERNAME: "" + DB_PASSWORD: "" + DB_HOST: "" + + # JWT 시크릿 (향후 인증 기능용) + JWT_SECRET: "" + diff --git a/review/deployment/manifests/service.yaml b/review/deployment/manifests/service.yaml new file mode 100644 index 0000000..874417d --- /dev/null +++ b/review/deployment/manifests/service.yaml @@ -0,0 +1,17 @@ +# deployment/manifests/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: kakao-review-api-service + labels: + app: kakao-review-api +spec: + type: ClusterIP + ports: + - port: 80 + targetPort: 19000 + protocol: TCP + name: http + selector: + app: kakao-review-api + diff --git a/review/setup.sh b/review/setup.sh new file mode 100755 index 0000000..aa2154d --- /dev/null +++ b/review/setup.sh @@ -0,0 +1,280 @@ +#!/bin/bash +# setup.sh - Review Analysis API 환경 설정 스크립트 + +set -e + +echo "🔍 Review Analysis API 환경 설정 시작..." + +# 시스템 정보 확인 +echo "📊 시스템 정보:" +echo " - OS: $(lsb_release -d | cut -f2)" +echo " - Python: $(python3 --version 2>/dev/null || echo 'Python3 미설치')" +echo " - pip: $(pip3 --version 2>/dev/null || echo 'pip3 미설치')" + +# 필수 패키지 설치 +echo "" +echo "📦 필수 패키지 설치 중..." +sudo apt update +sudo apt install -y python3-pip python3-venv unzip wget curl jq + +# Chrome 브라우저 설치 (리뷰 분석용) +echo "" +echo "🌐 Chrome 브라우저 설치 확인 중..." +if ! command -v google-chrome &> /dev/null; then + echo "Chrome 브라우저 설치 중..." + wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | sudo apt-key add - + echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" | sudo tee /etc/apt/sources.list.d/google-chrome.list + sudo apt update + sudo apt install -y google-chrome-stable + echo "✅ Chrome 브라우저 설치 완료" +else + CHROME_VERSION=$(google-chrome --version) + echo "✅ Chrome 브라우저 이미 설치됨: ${CHROME_VERSION}" +fi + +# ChromeDriver 설치 +echo "" +echo "🚗 ChromeDriver 설치 중..." +if [ -f "/usr/local/bin/chromedriver" ]; then + echo "기존 ChromeDriver 제거 중..." + sudo rm -f /usr/local/bin/chromedriver +fi + +CHROME_VERSION=$(google-chrome --version | grep -oP '\d+\.\d+\.\d+\.\d+' | cut -d'.' -f1) +echo "Chrome 주 버전: ${CHROME_VERSION}" + +CHROMEDRIVER_VERSION=$(curl -s "https://googlechromelabs.github.io/chrome-for-testing/LATEST_RELEASE_${CHROME_VERSION}") +echo "ChromeDriver 버전: ${CHROMEDRIVER_VERSION}" + +TEMP_DIR=$(mktemp -d) +cd "$TEMP_DIR" + +wget -q "https://storage.googleapis.com/chrome-for-testing-public/${CHROMEDRIVER_VERSION}/linux64/chromedriver-linux64.zip" +unzip -q chromedriver-linux64.zip +sudo mv chromedriver-linux64/chromedriver /usr/local/bin/ +sudo chmod +x /usr/local/bin/chromedriver + +cd - > /dev/null +rm -rf "$TEMP_DIR" + +echo "✅ ChromeDriver 설치 완료: $(chromedriver --version)" + +# Python 버전 확인 +PYTHON_VERSION=$(python3 -c "import sys; print('.'.join(map(str, sys.version_info[:2])))") +echo "" +echo "📍 Python 버전: ${PYTHON_VERSION}" + +if [ "$(echo "${PYTHON_VERSION} < 3.8" | bc)" -eq 1 ]; then + echo "⚠️ Python 3.8 이상이 권장됩니다." +fi + +# Python 가상환경 설정 +echo "" +echo "🐍 Python 가상환경 설정 중..." +if [ ! -d "venv" ]; then + python3 -m venv venv + echo "✅ 가상환경 생성 완료" +else + echo "✅ 기존 가상환경 발견" +fi + +# 가상환경 활성화 및 라이브러리 설치 +echo "📚 Python 라이브러리 설치 중..." +source venv/bin/activate + +# pip 업그레이드 +pip install --upgrade pip + +# 필요한 라이브러리 설치 +if [ -f "app/requirements.txt" ]; then + pip install -r app/requirements.txt + echo "✅ requirements.txt에서 라이브러리 설치 완료" +else + echo "⚠️ app/requirements.txt 파일을 찾을 수 없습니다." + echo "🔧 기본 라이브러리들을 직접 설치합니다..." + pip install fastapi uvicorn beautifulsoup4 selenium webdriver-manager pydantic python-multipart lxml +fi + +# 데이터 디렉토리 생성 +echo "" +echo "📁 데이터 디렉토리 설정 중..." +mkdir -p data +chmod 755 data +echo "✅ 데이터 디렉토리 생성: $(pwd)/data" + +# 환경변수 파일 생성 (예시) +echo "" +echo "⚙️ 환경변수 파일 생성 중..." +cat > .env << EOF +# 애플리케이션 설정 +APP_TITLE=카카오맵 리뷰 분석 API +APP_VERSION=1.0.1 +APP_DESCRIPTION=교육 목적 전용 - 실제 서비스 사용 금지 + +# 서버 설정 +HOST=0.0.0.0 +PORT=19000 +LOG_LEVEL=info + +# API 기본값 설정 +DEFAULT_MAX_TIME=300 +DEFAULT_DAYS_LIMIT=60 +MAX_DAYS_LIMIT=365 +MIN_MAX_TIME=60 +MAX_MAX_TIME=600 + +# Chrome 브라우저 설정 +CHROME_OPTIONS=--headless=new --no-sandbox --disable-dev-shm-usage --disable-gpu --window-size=1920,1080 +SCROLL_CHECK_INTERVAL=5 +SCROLL_NO_CHANGE_LIMIT=6 +SCROLL_WAIT_TIME_SHORT=2.0 +SCROLL_WAIT_TIME_LONG=3.0 + +# 법적 경고 메시지 +LEGAL_WARNING_ENABLED=true +CONTACT_EMAIL=admin@example.com + +# 건강 체크 설정 +HEALTH_CHECK_TIMEOUT=10 + +# AI API 설정 (향후 Claude API 연동용) +# CLAUDE_API_KEY=your_claude_api_key_here + +# 데이터베이스 설정 (향후 PostgreSQL 연동용) +# DB_HOST=localhost +# DB_PORT=5432 +# DB_NAME=review_analysis +# DB_USERNAME=your_db_username +# DB_PASSWORD=your_db_password +EOF + echo "✅ .env 파일 생성 완료" + +# 네트워크 연결 테스트 +echo "" +echo "🌐 네트워크 연결 테스트 중..." + +# 카카오맵 연결 테스트 +if curl -s --connect-timeout 5 "https://place.map.kakao.com" > /dev/null; then + echo "✅ 카카오맵 서버 연결 가능" +else + echo "❌ 카카오맵 서버 연결 실패" + echo " 인터넷 연결 상태를 확인해주세요." +fi + +# Selenium 및 Chrome 테스트 +echo "" +echo "🧪 Selenium 및 Chrome 테스트 중..." +source venv/bin/activate +python3 -c " +try: + from selenium import webdriver + from selenium.webdriver.chrome.options import Options + from selenium.webdriver.chrome.service import Service + print('✅ Selenium 모듈 import 성공') + + options = Options() + options.add_argument('--headless') + options.add_argument('--no-sandbox') + options.add_argument('--disable-dev-shm-usage') + + try: + service = Service('/usr/local/bin/chromedriver') + driver = webdriver.Chrome(service=service, options=options) + driver.get('data:text/html,

Test

') + title = driver.title + driver.quit() + print('✅ Chrome WebDriver 테스트 성공') + except Exception as e: + print(f'❌ Chrome WebDriver 테스트 실패: {e}') + +except ImportError as e: + print(f'❌ Selenium import 실패: {e}') +except Exception as e: + print(f'❌ 테스트 중 오류: {e}') +" + +# Docker 설치 확인 (선택사항) +echo "" +echo "🐳 Docker 설치 확인 중..." +if command -v docker &> /dev/null; then + DOCKER_VERSION=$(docker --version) + echo "✅ Docker 설치됨: ${DOCKER_VERSION}" + + # Docker 권한 확인 + if docker ps &> /dev/null; then + echo "✅ Docker 권한 확인됨" + else + echo "⚠️ Docker 권한 없음. 다음 명령어로 권한을 추가하세요:" + echo " sudo usermod -aG docker $USER" + echo " newgrp docker" + fi +else + echo "⚠️ Docker가 설치되지 않음 (컨테이너 배포 시 필요)" + echo " 설치 방법: https://docs.docker.com/engine/install/ubuntu/" +fi + +# 방화벽 설정 확인 +echo "" +echo "🔥 방화벽 설정 확인 중..." +if command -v ufw &> /dev/null; then + UFW_STATUS=$(sudo ufw status | head -n1 | awk '{print $2}') + if [ "${UFW_STATUS}" = "active" ]; then + echo "🔥 UFW 방화벽 활성화됨" + if sudo ufw status | grep -q "19000"; then + echo "✅ 포트 19000 방화벽 규칙 확인됨" + else + echo "⚠️ 포트 19000 방화벽 규칙 없음" + echo " 다음 명령어로 포트를 열어주세요:" + echo " sudo ufw allow 19000" + fi + else + echo "✅ UFW 방화벽 비활성화됨" + fi +else + echo "✅ UFW 방화벽 미설치" +fi + +echo "" +echo "🎉 환경 설정 완료!" +echo "" +echo "📋 다음 단계:" +echo "1. 가상환경 활성화:" +echo " source venv/bin/activate" +echo "" +echo "2. 환경변수 설정:" +echo " export DATA_DIR=\"./data\"" +echo "" +echo "3. 애플리케이션 실행:" +echo " python app/main.py" +echo " 또는" +echo " uvicorn app.main:app --host 0.0.0.0 --port 19000 --reload" +echo "" +echo "4. 웹 브라우저에서 접속:" +echo " http://localhost:19000 (메인 페이지)" +echo " http://localhost:19000/docs (Swagger UI)" +echo " http://localhost:19000/health (헬스체크)" +echo "" +echo "5. API 테스트:" +echo " curl -X POST \"http://localhost:19000/analyze\" \\" +echo " -H \"Content-Type: application/json\" \\" +echo " -d '{\"store_id\":\"501745730\",\"days_limit\":7,\"max_time\":300}'" +echo "" +echo "⚠️ 중요 법적 경고사항:" +echo " 이 서비스는 교육 목적으로만 사용하세요!" +echo " 실제 웹사이트 크롤링은 법적 위험이 있습니다." +echo " 카카오 공식 API를 사용하는 것을 권장합니다: developers.kakao.com" +echo "" +echo "💡 문제 발생 시:" +echo " - 로그 확인: tail -f 로그파일" +echo " - 환경변수 확인: cat .env" +echo " - Chrome 확인: google-chrome --version" +echo " - ChromeDriver 확인: chromedriver --version" +echo "" +echo "🔧 설정 파일 위치:" +echo " - 환경변수: $(pwd)/.env" +echo " - 데이터 저장: $(pwd)/data/" +echo " - 애플리케이션: $(pwd)/app/" + +deactivate 2>/dev/null || true +echo "" +echo "✅ Review Analysis API 환경 설정이 완료되었습니다!" \ No newline at end of file diff --git a/vector/README.md b/vector/README.md new file mode 100644 index 0000000..f8d1eba --- /dev/null +++ b/vector/README.md @@ -0,0 +1,614 @@ +# 음식점 Vector DB 구축 서비스 + +카카오 로컬 API를 활용하여 음식점 정보를 수집하고 Vector DB로 구축한 후, Claude AI와 연동하여 점주의 비즈니스 개선 요청을 처리하는 서비스입니다. + +## 📋 프로젝트 개요 + +### 주요 기능 +- 🔍 **카카오 로컬 API 연동**: 키워드 및 지역 기반 음식점 검색 +- 🤖 **Vector DB 구축**: Sentence Transformer를 이용한 임베딩 및 Chroma DB 저장 +- 🔄 **중복 처리 개선**: 기존 데이터 존재 여부 확인 및 업데이트/신규 추가 분리 +- 🧠 **Claude AI 연동**: 점주 액션 요청에 대한 맞춤형 조언 생성 +- 🎯 **유사도 검색**: Vector DB를 활용한 유사 음식점 검색 +- 📊 **처리 통계**: 신규 추가, 업데이트, 중복 등 상세 통계 제공 +- 🚀 **RESTful API 제공**: FastAPI 기반의 완전한 API 서비스 +- 📚 **Swagger UI 지원**: 자동 생성되는 API 문서 +- ☸️ **Kubernetes 배포**: 완전한 컨테이너 오케스트레이션 지원 +- 🔧 **환경변수 설정**: ConfigMap과 Secret을 통한 유연한 설정 관리 + +### 기술 스택 +- **Backend**: Python 3.11, FastAPI, aiohttp +- **Vector DB**: Chroma DB +- **ML**: Sentence Transformers, torch +- **AI**: Anthropic Claude API +- **External API**: Kakao Local API +- **Container**: Docker, Multi-stage build +- **Orchestration**: Kubernetes (AKS) +- **Registry**: Azure Container Registry (ACR) +- **Documentation**: Swagger UI, ReDoc + +## 🏗️ 프로젝트 구조 + +``` +review-api/vector/ +├── app/ # 애플리케이션 소스 +│ ├── main.py # 메인 애플리케이션 +│ ├── requirements.txt # Python 의존성 +│ ├── config/ # 설정 관리 +│ │ └── settings.py # 환경 설정 +│ ├── models/ # 데이터 모델 +│ ├── services/ # 비즈니스 로직 +│ │ ├── vector_service.py # Vector DB 서비스 +│ │ ├── claude_service.py # Claude AI 서비스 +│ │ └── kakao_service.py # 카카오 API 서비스 +│ └── utils/ # 유틸리티 함수 +│ └── data_utils.py # 데이터 처리 유틸 +├── deployment/ # 배포 관련 파일 +│ ├── container/ # 컨테이너 이미지 빌드 +│ │ ├── Dockerfile # 서비스 이미지 빌드 +│ │ └── Dockerfile-base # 베이스 이미지 빌드 +│ └── manifests/ # Kubernetes 매니페스트 +│ ├── configmap.yaml # 환경 설정 +│ ├── secret.yaml # 민감 정보 (API 키) +│ ├── deployment.yaml # 애플리케이션 배포 +│ ├── service.yaml # 서비스 노출 +│ ├── ingress.yaml # 외부 접근 +│ └── pvc.yaml # 영구 저장소 (Vector DB) +├── build-base.sh # 베이스 이미지 빌드 스크립트 +├── build.sh # 서비스 이미지 빌드 스크립트 +├── create-imagepullsecret.sh # ACR 인증 설정 스크립트 +├── setup.sh # 로컬 환경 설정 스크립트 +└── README.md # 프로젝트 문서 +``` + +## 📋 사전 작업 + +### 카카오 API 설정 +카카오 developers 포탈에서 애플리케이션 등록이 필요합니다. + +1. **포탈 접속**: https://developers.kakao.com/console/app +2. **애플리케이션 등록**: + - 앱 이름: `VectorDBBuilder` + - 회사명: `{회사명}` + - 카테고리: `식음료` +3. **카카오맵 활성화**: 등록한 애플리케이션에서 좌측 '카카오맵' 메뉴 클릭하여 활성화 + +### Claude API 설정 +Anthropic Claude API 키가 필요합니다. + +1. **포탈 접속**: https://console.anthropic.com/ +2. **API 키 발급**: Console에서 API Keys 메뉴에서 발급 +3. **모델 확인**: `claude-sonnet-4-20250514` 사용 + +## 🚀 빠른 시작 + +### 1. 로컬 개발 환경 설정 + +```bash +# 저장소 클론 (review-api/vector 디렉토리로 이동) +cd review-api/vector + +# 환경 설정 스크립트 실행 (시간이 오래 걸림) +chmod +x setup.sh +./setup.sh + +# 가상환경 활성화 +source venv/bin/activate + +# Claude API 키 설정 (.env 파일 생성/수정) +cat > .env << EOF +CLAUDE_API_KEY=your-actual-claude-api-key +KAKAO_API_KEY=your-kakao-api-key +EOF + +# 애플리케이션 실행 +python app/main.py +``` + +### 2. 로컬 웹 브라우저 접속 + +```bash +# 애플리케이션이 정상 실행된 후 아래 URL로 접속 +``` + +- **메인 페이지**: http://localhost:8000 +- **Swagger UI**: http://localhost:8000/docs +- **ReDoc**: http://localhost:8000/redoc +- **헬스체크**: http://localhost:8000/health +- **Vector DB 상태**: http://localhost:8000/vector-status +- **환경 설정**: http://localhost:8000/config + +### 3. API 테스트 + +```bash +# 1. Vector DB 구축 (지역과 가게명으로 동종업체 분석) +curl -X POST "http://localhost:8000/build-vector-db" \ + -H "Content-Type: application/json" \ + -d '{ + "region": "서울특별시 강남구 역삼동", + "store_name": "맛있는 치킨집", + "force_rebuild": false + }' + +# 2. 점주 액션 요청 (Claude AI 기반 조언) +curl -X POST "http://localhost:8000/action-request" \ + -H "Content-Type: application/json" \ + -d '{ + "store_id": "12345", + "context": "매출이 감소하고 있어서 메뉴 개선이 필요합니다." + }' + +# 3. Vector DB 상태 확인 +curl "http://localhost:8000/vector-status" + +# 4. 환경 설정 확인 +curl "http://localhost:8000/config" + +# 5. 유사 음식점 검색 +curl -X POST "http://localhost:8000/search-similar" \ + -H "Content-Type: application/json" \ + -d '{ + "query": "분위기 좋은 카페", + "business_type": "카페", + "limit": 5 + }' +``` + +## 🐳 Docker 컨테이너 실행 + +### 베이스 이미지 빌드 + +```bash +# ACR에 베이스 이미지 빌드 및 푸시 +./build-base.sh latest acrdigitalgarage03 rg-digitalgarage-03 + +# 로컬 베이스 이미지 빌드 +./build-base.sh latest +``` + +### 서비스 이미지 빌드 + +```bash +# ACR에 서비스 이미지 빌드 및 푸시 +./build.sh latest acrdigitalgarage03 rg-digitalgarage-03 + +# 로컬 서비스 이미지 빌드 +./build.sh latest +``` + +### 로컬 Docker 실행 + +```bash +# 컨테이너 실행 (환경변수 설정 필요) +docker run -p 8000:8000 \ + -e KAKAO_API_KEY="your-kakao-api-key" \ + -e CLAUDE_API_KEY="your-claude-api-key" \ + -e PORT=8000 \ + -v $(pwd)/vector_db:/app/vectordb \ + vector-api:latest + +# 백그라운드 실행 (영구 볼륨 사용) +docker volume create vector_db_vol +docker run -d -p 8000:8000 \ + --name vector-api \ + -e KAKAO_API_KEY="your-kakao-api-key" \ + -e CLAUDE_API_KEY="your-claude-api-key" \ + -e PORT=8000 \ + -v vector_db_vol:/app/vectordb \ + vector-api:latest + +# 컨테이너 로그 확인 +docker logs vector-api -f + +# Vector DB 데이터 확인 +docker exec -it vector-api ls -la /app/vectordb/ +``` + +## ☸️ Kubernetes 배포 + +### 1. ACR Image Pull Secret 생성 + +```bash +# Image Pull Secret 생성 +./create-imagepullsecret.sh acrdigitalgarage03 rg-digitalgarage-03 +``` + +### 2. Kubernetes 리소스 배포 + +```bash +# 영구 볼륨 생성 (Vector DB 저장용) +kubectl apply -f deployment/manifests/pvc.yaml + +# ConfigMap 및 Secret 적용 +kubectl apply -f deployment/manifests/configmap.yaml +kubectl apply -f deployment/manifests/secret.yaml + +# 애플리케이션 배포 +kubectl apply -f deployment/manifests/deployment.yaml +kubectl apply -f deployment/manifests/service.yaml +kubectl apply -f deployment/manifests/ingress.yaml +``` + +### 3. 배포 상태 확인 + +```bash +# Pod 상태 확인 +kubectl get pods -l app=vector-api + +# 서비스 상태 확인 +kubectl get svc vector-api-service + +# Ingress 상태 확인 +kubectl get ingress vector-api-ingress + +# PVC 상태 확인 (Vector DB 저장소) +kubectl get pvc vector-db-pvc + +# 로그 확인 +kubectl logs -l app=vector-api -f + +# Vector DB 디렉토리 확인 +kubectl exec -it deployment/vector-api -- ls -la /app/vectordb/ +``` + +### 4. 🌐 외부 브라우저에서 접속하기 + +#### Ingress 주소 확인 방법 + +```bash +# 1. Ingress 설정된 호스트 확인 +kubectl get ingress vector-api-ingress -o jsonpath='{.spec.rules[0].host}' + +# 2. Ingress External IP 확인 (LoadBalancer 타입인 경우) +kubectl get ingress vector-api-ingress + +# 3. Ingress Controller의 External IP 확인 +kubectl get svc -n ingress-nginx ingress-nginx-controller + +# 4. 현재 설정된 ingress 주소 확인 +INGRESS_HOST=$(kubectl get ingress vector-api-ingress -o jsonpath='{.spec.rules[0].host}') +echo "🌐 Vector API URL: http://${INGRESS_HOST}" +``` + +#### 브라우저 접속 주소 + +현재 설정된 주소로 접속하세요: + +```bash +# 현재 설정된 기본 주소 (환경에 따라 다를 수 있음) +INGRESS_URL="http://vector-api.20.249.191.180.nip.io" +echo "브라우저에서 접속: ${INGRESS_URL}" +``` + +**주요 접속 페이지:** +- **🏠 메인 페이지**: http://vector-api.20.249.191.180.nip.io +- **📖 Swagger UI**: http://vector-api.20.249.191.180.nip.io/docs +- **📄 ReDoc**: http://vector-api.20.249.191.180.nip.io/redoc +- **❤️ 헬스체크**: http://vector-api.20.249.191.180.nip.io/health +- **🗂️ Vector DB 상태**: http://vector-api.20.249.191.180.nip.io/vector-status +- **⚙️ 환경 설정**: http://vector-api.20.249.191.180.nip.io/config + +#### 접속 테스트 + +```bash +# API 접속 테스트 +curl "http://vector-api.20.249.191.180.nip.io/health" + +# Vector DB 상태 확인 +curl "http://vector-api.20.249.191.180.nip.io/vector-status" + +# 설정 정보 확인 +curl "http://vector-api.20.249.191.180.nip.io/config" + +# Swagger UI 접속 확인 +curl -I "http://vector-api.20.249.191.180.nip.io/docs" +``` + +## ⚙️ 환경 설정 + +### 필수 환경변수 + +| 변수명 | 설명 | 기본값 | 예시 | +|--------|------|--------|------| +| `KAKAO_API_KEY` | 카카오 API 키 | - | `your-kakao-api-key` | +| `CLAUDE_API_KEY` | Claude API 키 | - | `sk-ant-api03-...` | +| `CLAUDE_MODEL` | Claude 모델명 | `claude-sonnet-4-20250514` | `claude-sonnet-4-20250514` | + +### 선택적 환경변수 + +| 변수명 | 설명 | 기본값 | 예시 | +|--------|------|--------|------| +| `HOST` | 서버 호스트 | `0.0.0.0` | `localhost` | +| `PORT` | 서버 포트 | `8000` | `8000` | +| `LOG_LEVEL` | 로그 레벨 | `info` | `debug` | +| `VECTOR_DB_PATH` | Vector DB 경로 | `/app/vectordb` | `/data/vectordb` | +| `VECTOR_DB_COLLECTION` | 컬렉션명 | `restaurant_reviews` | `stores` | +| `EMBEDDING_MODEL` | 임베딩 모델 | `sentence-transformers/all-MiniLM-L6-v2` | - | +| `MAX_RESTAURANTS_PER_CATEGORY` | 카테고리별 최대 음식점 수 | `50` | `100` | +| `MAX_REVIEWS_PER_RESTAURANT` | 음식점별 최대 리뷰 수 | `100` | `200` | + +## 📊 API 엔드포인트 + +### 주요 엔드포인트 + +| Method | Endpoint | 설명 | +|--------|----------|------| +| `GET` | `/` | 메인 페이지 | +| `GET` | `/docs` | Swagger UI 문서 | +| `GET` | `/health` | 헬스체크 | +| `GET` | `/vector-status` | Vector DB 상태 확인 | +| `GET` | `/config` | 환경 설정 확인 | +| `POST` | `/build-vector-db` | Vector DB 구축 | +| `POST` | `/action-request` | AI 액션 요청 | +| `POST` | `/search-similar` | 유사 음식점 검색 | +| `GET` | `/collections` | 컬렉션 목록 조회 | + +### Vector DB 구축 API 예시 + +```json +POST /build-vector-db +{ + "region": "서울특별시 강남구 역삼동", + "store_name": "맛있는 치킨집", + "force_rebuild": false +} +``` + +**응답:** +```json +{ + "success": true, + "message": "Vector DB 구축이 완료되었습니다.", + "statistics": { + "total_processed": 150, + "newly_added": 120, + "updated": 25, + "duplicates": 5, + "total_vectors": 1450 + }, + "execution_time": 245.8, + "store_info": { + "name": "맛있는 치킨집", + "region": "서울특별시 강남구 역삼동", + "business_type": "치킨", + "similar_stores_found": 15 + } +} +``` + +### AI 액션 요청 API 예시 + +```json +POST /action-request +{ + "store_id": "12345", + "context": "매출이 감소하고 있어서 메뉴 개선이 필요합니다." +} +``` + +**응답:** +```json +{ + "success": true, + "recommendations": [ + { + "category": "메뉴 개선", + "priority": "high", + "action": "시즌 한정 메뉴 도입", + "description": "고객 선호도가 높은 트렌디한 메뉴를 계절별로 출시하여 재방문율을 높입니다.", + "timeframe": "1-2주", + "expected_impact": "매출 15-20% 증가 예상" + } + ], + "similar_cases": 3, + "analysis_date": "2024-06-12T10:30:00" +} +``` + +## 🔧 개발 및 확장 + +### 로컬 개발 + +```bash +# 개발 모드로 실행 (자동 재시작) +uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload + +# 의존성 추가 +pip install 새패키지명 +pip freeze > app/requirements.txt + +# Vector DB 디렉토리 확인 +ls -la ./vector_db/ + +# 코드 포맷팅 +black app/ +flake8 app/ +``` + +### Ingress 호스트 변경 + +현재 환경에 맞게 Ingress 호스트를 변경하려면: + +```bash +# 1. 현재 External IP 확인 +kubectl get svc -n ingress-nginx ingress-nginx-controller + +# 2. deployment/manifests/ingress.yaml 파일에서 host 수정 +# 예: vector-api.{YOUR_EXTERNAL_IP}.nip.io + +# 3. 변경사항 적용 +kubectl apply -f deployment/manifests/ingress.yaml + +# 4. 새로운 주소 확인 +kubectl get ingress vector-api-ingress +``` + +## 🐛 문제 해결 + +### 일반적인 문제 + +**1. Claude API 키 관련 문제** +```bash +# API 키 유효성 확인 +curl -X POST "https://api.anthropic.com/v1/messages" \ + -H "x-api-key: your-claude-api-key" \ + -H "Content-Type: application/json" \ + -d '{ + "model": "claude-sonnet-4-20250514", + "max_tokens": 10, + "messages": [{"role": "user", "content": "Hi"}] + }' + +# 환경변수 확인 +echo $CLAUDE_API_KEY +``` + +**2. Vector DB 관련 문제** +```bash +# Vector DB 디렉토리 권한 확인 +ls -la /app/vectordb/ + +# Vector DB 초기화 +curl -X POST "http://localhost:8000/reset-vector-db" + +# Collection 상태 확인 +curl "http://localhost:8000/vector-status" +``` + +**3. Kubernetes 배포 실패** +```bash +# Pod 로그 확인 (Vector DB 초기화 에러) +kubectl logs -l app=vector-api + +# PVC 상태 확인 +kubectl get pvc vector-db-pvc +kubectl describe pvc vector-db-pvc + +# ConfigMap 확인 +kubectl get configmap vector-api-config -o yaml + +# Secret 확인 +kubectl get secret vector-api-secret -o yaml +``` + +**4. 메모리 부족 문제** +```bash +# Vector DB 및 ML 모델이 메모리를 많이 사용 +kubectl top pods -l app=vector-api + +# 리소스 제한 확인 +kubectl describe pod -l app=vector-api +``` + +**5. 포트 관련 문제** +- 로컬 개발: 8000번 포트 +- Docker 컨테이너: 8000번 포트 +- Kubernetes: Service 80번 → Ingress 외부 접근 + +## 🎯 성능 최적화 + +### Vector DB 최적화 설정 + +```yaml +# deployment.yaml에서 Vector DB 최적화를 위한 리소스 설정 +resources: + requests: + memory: "2Gi" # Vector DB와 ML 모델이 메모리를 많이 사용 + cpu: "1000m" # 임베딩 계산을 위한 CPU + limits: + memory: "4Gi" # Vector 계산 및 Claude API 호출을 위한 충분한 메모리 + cpu: "2000m" # 병렬 처리를 위한 CPU +``` + +### 영구 저장소 설정 + +```yaml +# pvc.yaml에서 Vector DB 저장소 설정 +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 10Gi # Vector 데이터 저장을 위한 충분한 공간 +``` + +### 타임아웃 설정 + +```yaml +# ingress.yaml에서 Vector DB 구축 시간을 고려한 타임아웃 +nginx.ingress.kubernetes.io/proxy-read-timeout: "1800" # 30분 +nginx.ingress.kubernetes.io/proxy-send-timeout: "1800" # 30분 +``` + +## 📈 Vector DB 워크플로우 + +### 1. Vector DB 구축 과정 + +```mermaid +graph TD + A[점주 요청] --> B[지역 및 업종 분석] + B --> C[카카오 API 음식점 수집] + C --> D[리뷰 데이터 수집] + D --> E[텍스트 전처리] + E --> F[Sentence Transformer 임베딩] + F --> G[Chroma DB 저장] + G --> H[중복 제거 및 메타데이터 구성] + H --> I[Vector DB 완성] +``` + +### 2. AI 액션 추천 과정 + +```mermaid +graph TD + A[점주 액션 요청] --> B[컨텍스트 분석] + B --> C[Vector 유사도 검색] + C --> D[관련 케이스 추출] + D --> E[Claude AI 프롬프트 구성] + E --> F[Claude API 호출] + F --> G[맞춤형 액션 플랜 생성] + G --> H[우선순위 및 실행 방안 제시] +``` + +## 📈 향후 확장 계획 + +- [ ] **다중 Vector DB 지원**: FAISS, Pinecone 추가 지원 +- [ ] **실시간 업데이트**: 새로운 리뷰 자동 임베딩 및 추가 +- [ ] **감정 분석 고도화**: 더 정교한 감정 및 의도 분류 +- [ ] **업종별 특화**: 업종별 맞춤형 Vector 모델 개발 +- [ ] **A/B 테스트**: 추천 효과 측정 및 개선 +- [ ] **대시보드**: Vector DB 현황 및 성능 모니터링 +- [ ] **배치 처리**: 대규모 데이터 처리 최적화 +- [ ] **API 버전 관리**: 하위 호환성 보장 +- [ ] **멀티모달 지원**: 이미지, 텍스트 통합 임베딩 +- [ ] **비즈니스 인텔리전스**: 시장 트렌드 분석 기능 + +## 🧠 AI/ML 구성 요소 + +### Sentence Transformers +- **모델**: `all-MiniLM-L6-v2` (다국어 지원, 384차원) +- **용도**: 한국어 텍스트 임베딩 +- **특징**: 빠른 속도, 합리적인 정확도 + +### Chroma Vector DB +- **저장 방식**: 영구 저장 (PVC) +- **인덱싱**: HNSW 알고리즘 +- **메타데이터**: 필터링 및 검색 최적화 + +### Claude AI +- **모델**: `claude-sonnet-4-20250514` +- **용도**: 비즈니스 액션 추천 +- **특징**: 높은 품질의 한국어 분석 + +--- + +## 📞 지원 및 문의 + +- **이슈 리포트**: GitHub Issues +- **기술 문의**: 개발팀 Slack +- **API 문의**: Anthropic Support +- **API 문서**: Swagger UI에서 상세 확인 + +--- + +**💡 팁: Vector DB 구축에는 시간이 오래 걸릴 수 있으니 충분한 타임아웃을 설정하고 진행 상황을 모니터링하세요.** + diff --git a/vector/app/config/settings.py b/vector/app/config/settings.py new file mode 100644 index 0000000..e5a59bb --- /dev/null +++ b/vector/app/config/settings.py @@ -0,0 +1,80 @@ +# app/config/settings.py +import os +from typing import Optional + +class Settings: + """환경 변수 기반 설정 클래스""" + + # 애플리케이션 메타데이터 + APP_TITLE = os.getenv("APP_TITLE", "음식점 Vector DB 구축 서비스") + APP_VERSION = os.getenv("APP_VERSION", "1.0.0") + APP_DESCRIPTION = os.getenv("APP_DESCRIPTION", "소상공인을 위한 AI 기반 경쟁업체 분석 및 액션 추천 시스템") + + # 서버 설정 + HOST = os.getenv("HOST", "0.0.0.0") + PORT = int(os.getenv("PORT", "8000")) + LOG_LEVEL = os.getenv("LOG_LEVEL", "info") + + # Restaurant API 설정 + RESTAURANT_API_HOST = os.getenv("RESTAURANT_API_HOST", "4.217.217.207") + RESTAURANT_API_PORT = os.getenv("RESTAURANT_API_PORT", "18000") + + @property + def RESTAURANT_API_URL(self) -> str: + return f"http://{self.RESTAURANT_API_HOST}:{self.RESTAURANT_API_PORT}" + + # Review API 설정 + REVIEW_API_HOST = os.getenv("REVIEW_API_HOST", "4.217.217.207") + REVIEW_API_PORT = os.getenv("REVIEW_API_PORT", "19000") + + @property + def REVIEW_API_URL(self) -> str: + return f"http://{self.REVIEW_API_HOST}:{self.REVIEW_API_PORT}" + + # Claude API 설정 + CLAUDE_API_KEY = os.getenv("CLAUDE_API_KEY", "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") + CLAUDE_MODEL = os.getenv("CLAUDE_MODEL", "claude-sonnet-4-20250514") + + # Vector DB 설정 + VECTOR_DB_PATH = os.getenv("VECTOR_DB_PATH", "/app/vectordb") + VECTOR_DB_COLLECTION = os.getenv("VECTOR_DB_COLLECTION", "restaurant_reviews") + EMBEDDING_MODEL = os.getenv("EMBEDDING_MODEL", "sentence-transformers/all-MiniLM-L6-v2") + + # 데이터 수집 설정 + MAX_RESTAURANTS_PER_CATEGORY = int(os.getenv("MAX_RESTAURANTS_PER_CATEGORY", "50")) + MAX_REVIEWS_PER_RESTAURANT = int(os.getenv("MAX_REVIEWS_PER_RESTAURANT", "100")) + REQUEST_DELAY = float(os.getenv("REQUEST_DELAY", "0.1")) + REQUEST_TIMEOUT = int(os.getenv("REQUEST_TIMEOUT", "600")) + + # 환경 감지 + @property + def IS_K8S_ENV(self) -> bool: + """Kubernetes 환경인지 확인""" + return ( + os.getenv("KUBERNETES_SERVICE_HOST") is not None or + self.RESTAURANT_API_HOST in ["restaurant-api-service", "kakao-review-api-service"] or + self.REVIEW_API_HOST in ["restaurant-api-service", "kakao-review-api-service"] + ) + + def get_restaurant_api_url(self) -> str: + """환경에 따른 Restaurant API URL 반환""" + if self.IS_K8S_ENV: + host = "restaurant-api-service" + port = "80" + else: + host = self.RESTAURANT_API_HOST + port = self.RESTAURANT_API_PORT + return f"http://{host}:{port}" + + def get_review_api_url(self) -> str: + """환경에 따른 Review API URL 반환""" + if self.IS_K8S_ENV: + host = "kakao-review-api-service" + port = "80" + else: + host = self.REVIEW_API_HOST + port = self.REVIEW_API_PORT + return f"http://{host}:{port}" + +# 설정 인스턴스 +settings = Settings() diff --git a/vector/app/main.py b/vector/app/main.py new file mode 100644 index 0000000..50e3aa9 --- /dev/null +++ b/vector/app/main.py @@ -0,0 +1,917 @@ +# vector/app/main.py +import os +import sys +import logging +from contextlib import asynccontextmanager +from datetime import datetime +from typing import Optional + +# 현재 디렉토리를 Python 경로에 추가 +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +# ============================================================================= +# .env 파일 로딩 (다른 import보다 먼저) +# ============================================================================= +from dotenv import load_dotenv + +# .env 파일에서 환경변수 로드 +load_dotenv() + +import asyncio +import fastapi +from fastapi import FastAPI, HTTPException, BackgroundTasks, Depends +from fastapi.responses import HTMLResponse, JSONResponse +from fastapi.middleware.cors import CORSMiddleware +from pydantic import BaseModel, Field + +# 프로젝트 모듈 import +from app.config.settings import settings +from app.models.restaurant_models import RestaurantSearchRequest, ErrorResponse +from app.models.vector_models import ( + VectorBuildRequest, VectorBuildResponse, + ActionRecommendationRequest, ActionRecommendationResponse, + VectorDBStatusResponse, VectorDBStatus +) +from app.services.restaurant_service import RestaurantService +from app.services.review_service import ReviewService +from app.services.vector_service import VectorService +from app.services.claude_service import ClaudeService +from app.utils.category_utils import extract_food_category + +# 로깅 설정 +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[logging.StreamHandler(sys.stdout)] +) +logger = logging.getLogger(__name__) + +# 🔧 전역 변수 대신 애플리케이션 상태로 관리 +app_state = { + "vector_service": None, + "restaurant_service": None, + "review_service": None, + "claude_service": None, + "initialization_errors": {}, + "startup_completed": False +} + +# 추가 모델 정의 (find-reviews API용) +class FindReviewsRequest(BaseModel): + """리뷰 검색 요청 모델""" + region: str = Field( + ..., + description="지역 (시군구 + 읍면동)", + example="서울특별시 강남구 역삼동" + ) + store_name: str = Field( + ..., + description="가게명", + example="맛있는 한식당" + ) + +class RestaurantInfo(BaseModel): + """음식점 정보 모델""" + id: str = Field(description="카카오 장소 ID") + place_name: str = Field(description="장소명") + category_name: str = Field(description="카테고리명") + address_name: str = Field(description="전체 주소") + phone: str = Field(description="전화번호") + place_url: str = Field(description="장소 상세페이지 URL") + x: str = Field(description="X 좌표값 (경도)") + y: str = Field(description="Y 좌표값 (위도)") + +class FindReviewsResponse(BaseModel): + """리뷰 검색 응답 모델""" + success: bool = Field(description="검색 성공 여부") + message: str = Field(description="응답 메시지") + target_store: RestaurantInfo = Field(description="대상 가게 정보") + total_stores: int = Field(description="수집된 총 가게 수") + total_reviews: int = Field(description="수집된 총 리뷰 수") + food_category: str = Field(description="추출된 음식 카테고리") + region: str = Field(description="검색 지역") + +@asynccontextmanager +async def lifespan(app: FastAPI): + """🔧 애플리케이션 생명주기 관리 - 안전한 서비스 초기화""" + # 🚀 Startup 이벤트 + logger.info("🚀 Vector API 서비스 시작 중...") + startup_start_time = datetime.now() + + # 각 서비스 안전하게 초기화 + services_to_init = [ + ("restaurant_service", RestaurantService, "Restaurant API 서비스"), + ("review_service", ReviewService, "Review API 서비스"), + ("claude_service", ClaudeService, "Claude AI 서비스"), + ("vector_service", VectorService, "Vector DB 서비스") # 마지막에 초기화 + ] + + initialized_count = 0 + + for service_key, service_class, service_name in services_to_init: + try: + logger.info(f"🔧 {service_name} 초기화 중...") + app_state[service_key] = service_class() + logger.info(f"✅ {service_name} 초기화 완료") + initialized_count += 1 + + except Exception as e: + logger.error(f"❌ {service_name} 초기화 실패: {e}") + app_state["initialization_errors"][service_key] = str(e) + # 🔧 중요: 서비스 초기화 실패해도 앱은 시작 (헬스체크에서 확인) + continue + + startup_time = (datetime.now() - startup_start_time).total_seconds() + app_state["startup_completed"] = True + + logger.info(f"✅ Vector API 서비스 시작 완료!") + logger.info(f"📊 초기화 결과: {initialized_count}/{len(services_to_init)}개 서비스 성공") + logger.info(f"⏱️ 시작 소요시간: {startup_time:.2f}초") + + if app_state["initialization_errors"]: + logger.warning(f"⚠️ 초기화 실패 서비스: {list(app_state['initialization_errors'].keys())}") + + yield + + # 🛑 Shutdown 이벤트 + logger.info("🛑 Vector API 서비스 종료 중...") + + # 리소스 정리 + for service_key in ["vector_service", "restaurant_service", "review_service", "claude_service"]: + if app_state[service_key] is not None: + try: + # 서비스별 정리 작업이 있다면 여기서 수행 + logger.info(f"🔧 {service_key} 정리 중...") + except Exception as e: + logger.warning(f"⚠️ {service_key} 정리 실패: {e}") + finally: + app_state[service_key] = None + + app_state["startup_completed"] = False + logger.info("✅ Vector API 서비스 종료 완료") + +# 🔧 FastAPI 앱 초기화 (lifespan 이벤트 포함) +app = FastAPI( + title=settings.APP_TITLE, + description=f""" + {settings.APP_DESCRIPTION} + + **주요 기능:** + - 지역과 가게명으로 대상 가게 찾기 + - 동종 업체 리뷰 수집 및 분석 + - Vector DB 구축 및 관리 + - Claude AI 기반 액션 추천 + - 영속적 Vector DB 저장 + + **API 연동:** + - Restaurant API: {settings.get_restaurant_api_url() if hasattr(settings, 'get_restaurant_api_url') else 'N/A'} + - Review API: {settings.get_review_api_url() if hasattr(settings, 'get_review_api_url') else 'N/A'} + - Claude AI API: {settings.CLAUDE_MODEL} + + **Vector DB:** + - 경로: {settings.VECTOR_DB_PATH} + - 컬렉션: {settings.VECTOR_DB_COLLECTION} + - 임베딩 모델: {settings.EMBEDDING_MODEL} + + **버전:** {settings.APP_VERSION} + """, + version=settings.APP_VERSION, + contact={ + "name": "개발팀", + "email": "admin@example.com" + }, + lifespan=lifespan # 🔧 lifespan 이벤트 등록 +) + +# CORS 설정 +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# 🔧 Dependency Injection - 서비스 제공자들 +def get_vector_service() -> VectorService: + """VectorService 의존성 주입""" + if app_state["vector_service"] is None: + error_msg = app_state["initialization_errors"].get("vector_service") + if error_msg: + raise HTTPException( + status_code=503, + detail=f"Vector service not available: {error_msg}" + ) + + # 런타임에 재시도 + try: + logger.info("🔧 VectorService 런타임 초기화 시도...") + app_state["vector_service"] = VectorService() + logger.info("✅ VectorService 런타임 초기화 성공") + except Exception as e: + logger.error(f"❌ VectorService 런타임 초기화 실패: {e}") + raise HTTPException( + status_code=503, + detail=f"Vector service initialization failed: {str(e)}" + ) + + return app_state["vector_service"] + +def get_restaurant_service() -> RestaurantService: + """RestaurantService 의존성 주입""" + if app_state["restaurant_service"] is None: + error_msg = app_state["initialization_errors"].get("restaurant_service") + if error_msg: + raise HTTPException( + status_code=503, + detail=f"Restaurant service not available: {error_msg}" + ) + + try: + app_state["restaurant_service"] = RestaurantService() + except Exception as e: + raise HTTPException(status_code=503, detail=str(e)) + + return app_state["restaurant_service"] + +def get_review_service() -> ReviewService: + """ReviewService 의존성 주입""" + if app_state["review_service"] is None: + error_msg = app_state["initialization_errors"].get("review_service") + if error_msg: + raise HTTPException( + status_code=503, + detail=f"Review service not available: {error_msg}" + ) + + try: + app_state["review_service"] = ReviewService() + except Exception as e: + raise HTTPException(status_code=503, detail=str(e)) + + return app_state["review_service"] + +def get_claude_service() -> ClaudeService: + """ClaudeService 의존성 주입""" + if app_state["claude_service"] is None: + error_msg = app_state["initialization_errors"].get("claude_service") + if error_msg: + raise HTTPException( + status_code=503, + detail=f"Claude service not available: {error_msg}" + ) + + try: + app_state["claude_service"] = ClaudeService() + except Exception as e: + raise HTTPException(status_code=503, detail=str(e)) + + return app_state["claude_service"] + +@app.get("/", response_class=HTMLResponse, include_in_schema=False) +async def root(): + """메인 페이지""" + # 🔧 안전한 DB 상태 조회 + try: + vector_service = app_state.get("vector_service") + if vector_service: + db_status = vector_service.get_db_status() + else: + db_status = { + 'collection_name': settings.VECTOR_DB_COLLECTION, + 'total_documents': 0, + 'total_stores': 0, + 'db_path': settings.VECTOR_DB_PATH, + 'status': 'not_initialized' + } + except Exception as e: + logger.warning(f"DB 상태 조회 실패: {e}") + db_status = {'status': 'error', 'error': str(e)} + + return f""" + + + {settings.APP_TITLE} + + + +

🍽️ {settings.APP_TITLE}

+

{settings.APP_DESCRIPTION}

+ +
+

📊 Vector DB 상태

+
    +
  • 컬렉션: {db_status.get('collection_name', 'N/A')}
  • +
  • 총 문서 수: {db_status.get('total_documents', 0)}
  • +
  • 가게 수: {db_status.get('total_stores', 0)}
  • +
  • DB 경로: {db_status.get('db_path', 'N/A')}
  • +
  • 상태: {db_status.get('status', 'Unknown')}
  • +
+ {f''' +
+

⚠️ 초기화 실패 서비스

+
    + {"".join([f"
  • {k}: {v}
  • " for k, v in app_state["initialization_errors"].items()])} +
+
+ ''' if app_state["initialization_errors"] else ''} +
+ +
+

🔧 시스템 구성

+
    +
  • Claude Model: {settings.CLAUDE_MODEL}
  • +
  • Embedding Model: {settings.EMBEDDING_MODEL}
  • +
  • Vector DB Path: {settings.VECTOR_DB_PATH}
  • +
  • 환경: {'Kubernetes' if hasattr(settings, 'IS_K8S_ENV') and settings.IS_K8S_ENV else 'Local'}
  • +
+
+ +

📚 API 문서

+ Swagger UI 문서 + ReDoc 문서 + 헬스 체크 + Vector DB 상태 + +

🛠️ 사용 방법

+

POST /find-reviews - 리뷰 검색 및 Vector DB 저장 (본인 가게 우선)

+
+{{
+  "region": "서울특별시 강남구 역삼동",
+  "store_name": "맛있는 한식당"
+}}
+            
+ +

POST /build-vector - Vector DB 구축

+
+{{
+  "region": "서울특별시 강남구 역삼동",
+  "store_name": "맛있는 한식당",
+  "force_rebuild": false
+}}
+            
+ +

POST /action-recommendation - 액션 추천 요청

+
+{{
+  "store_id": "12345",
+  "context": "매출이 감소하고 있어서 개선이 필요합니다"
+}}
+            
+ + + """ + +@app.post("/find-reviews", response_model=FindReviewsResponse) +async def find_reviews( + request: FindReviewsRequest, + vector_service: VectorService = Depends(get_vector_service), + restaurant_service: RestaurantService = Depends(get_restaurant_service), + review_service: ReviewService = Depends(get_review_service) +): + """ + 지역과 가게명으로 리뷰를 찾아 Vector DB에 저장합니다. + 🔥 본인 가게 리뷰는 반드시 포함됩니다. (수정된 버전) + """ + start_time = datetime.now() + logger.info(f"🔍 리뷰 검색 요청: {request.region} - {request.store_name}") + + try: + # 1단계: 본인 가게 검색 + logger.info("1단계: 본인 가게 검색 중... (최우선)") + target_restaurant = await restaurant_service.find_store_by_name_and_region( + request.region, request.store_name + ) + + if not target_restaurant: + logger.error(f"❌ 본인 가게를 찾을 수 없음: {request.store_name}") + raise HTTPException( + status_code=404, + detail=f"'{request.store_name}' 가게를 찾을 수 없습니다. 가게명과 지역을 정확히 입력해주세요." + ) + + logger.info(f"✅ 본인 가게 발견: {target_restaurant.place_name} (ID: {target_restaurant.id})") + + # 2단계: 동종 업체 검색 + logger.info("2단계: 동종 업체 검색 중...") + similar_restaurants = [] + food_category = "기타" # 기본값 + + try: + food_category = extract_food_category(target_restaurant.category_name) + logger.info(f"추출된 음식 카테고리: {food_category}") + + similar_restaurants = await restaurant_service.find_similar_stores( + request.region, food_category, settings.MAX_RESTAURANTS_PER_CATEGORY + ) + + logger.info(f"✅ 동종 업체 {len(similar_restaurants)}개 발견") + + except Exception as e: + logger.warning(f"⚠️ 동종 업체 검색 실패 (본인 가게는 계속 진행): {e}") + + # 3단계: 전체 가게 목록 구성 (본인 가게 우선 + 중복 제거) + logger.info("3단계: 전체 가게 목록 구성 중...") + + # 본인 가게를 첫 번째로 배치 + all_restaurants = [target_restaurant] + + # 동종 업체 추가 (개선된 중복 제거) + for restaurant in similar_restaurants: + if not _is_duplicate_restaurant(target_restaurant, restaurant): + all_restaurants.append(restaurant) + + logger.info(f"✅ 전체 가게 목록 구성 완료: {len(all_restaurants)}개 (본인 가게 포함)") + + # 4단계: 전체 리뷰 수집 (본인 가게 우선 처리) + logger.info("4단계: 리뷰 수집 중... (본인 가게 우선)") + + # 본인 가게 우선 처리를 위한 특별 로직 + review_results = [] + + # 4-1: 본인 가게 리뷰 수집 (실패 시 전체 중단) + try: + logger.info("본인 가게 리뷰 우선 수집 중... (필수)") + target_store_info, target_reviews = await review_service.collect_store_reviews( + target_restaurant.id, + max_reviews=settings.MAX_REVIEWS_PER_RESTAURANT * 2 # 본인 가게는 더 많이 + ) + + if not target_store_info or not target_reviews: + logger.error(f"❌ 본인 가게 리뷰 수집 실패: {target_restaurant.place_name}") + raise HTTPException( + status_code=422, + detail=f"본인 가게 '{target_restaurant.place_name}'의 리뷰를 수집할 수 없습니다." + ) + + # 본인 가게 결과를 첫 번째로 설정 + review_results.append((target_restaurant.id, target_store_info, target_reviews)) + logger.info(f"✅ 본인 가게 리뷰 수집 성공: {len(target_reviews)}개") + + except HTTPException: + raise + except Exception as e: + logger.error(f"❌ 본인 가게 리뷰 수집 중 오류: {e}") + raise HTTPException( + status_code=500, + detail=f"본인 가게 리뷰 수집 중 오류가 발생했습니다: {str(e)}" + ) + + # 4-2: 동종 업체 리뷰 수집 (실패해도 본인 가게는 유지) + if len(all_restaurants) > 1: # 본인 가게 외에 다른 가게가 있는 경우 + try: + logger.info(f"동종 업체 리뷰 수집 중... ({len(all_restaurants) - 1}개)") + + # 본인 가게 제외한 동종 업체만 수집 + similar_restaurants_only = all_restaurants[1:] + similar_results = await review_service.collect_multiple_stores_reviews(similar_restaurants_only) + + # 동종 업체 결과 추가 + review_results.extend(similar_results) + + similar_reviews_count = sum(len(reviews) for _, _, reviews in similar_results) + logger.info(f"✅ 동종 업체 리뷰 수집 완료: {len(similar_results)}개 가게, {similar_reviews_count}개 리뷰") + + except Exception as e: + logger.warning(f"⚠️ 동종 업체 리뷰 수집 실패 (본인 가게는 유지): {e}") + else: + logger.info("동종 업체가 없어 본인 가게 리뷰만 사용") + + # 5단계: Vector DB 구축 + logger.info("5단계: Vector DB 구축 중...") + try: + # 대상 가게 정보를 딕셔너리로 변환 + target_store_info_dict = { + 'id': target_restaurant.id, + 'place_name': target_restaurant.place_name, + 'category_name': target_restaurant.category_name, + 'address_name': target_restaurant.address_name, + 'phone': target_restaurant.phone, + 'place_url': target_restaurant.place_url, + 'x': target_restaurant.x, + 'y': target_restaurant.y + } + + # Vector DB에 저장 + vector_result = await vector_service.build_vector_store( + target_store_info_dict, review_results, food_category, request.region + ) + + if not vector_result.get('success', False): + raise Exception(f"Vector DB 저장 실패: {vector_result.get('error', 'Unknown error')}") + + logger.info("✅ Vector DB 구축 완료") + + except Exception as e: + logger.error(f"❌ Vector DB 구축 실패: {e}") + raise HTTPException( + status_code=500, + detail=f"Vector DB 구축 중 오류가 발생했습니다: {str(e)}" + ) + + # 최종 검증: 본인 가게가 첫 번째에 있는지 확인 + if not review_results or review_results[0][0] != target_restaurant.id: + logger.error("❌ 본인 가게가 첫 번째에 없음") + raise HTTPException( + status_code=500, + detail="본인 가게 리뷰 처리 순서 오류가 발생했습니다." + ) + + # 성공 응답 + total_reviews = sum(len(reviews) for _, _, reviews in review_results) + execution_time = (datetime.now() - start_time).total_seconds() + + return FindReviewsResponse( + success=True, + message=f"✅ 본인 가게 리뷰 포함 보장 완료! (총 {len(review_results)}개 가게, {total_reviews}개 리뷰)", + target_store=RestaurantInfo( + id=target_restaurant.id, + place_name=target_restaurant.place_name, + category_name=target_restaurant.category_name, + address_name=target_restaurant.address_name, + phone=target_restaurant.phone, + place_url=target_restaurant.place_url, + x=target_restaurant.x, + y=target_restaurant.y + ), + total_stores=len(review_results), + total_reviews=total_reviews, + food_category=food_category, + region=request.region + ) + + except HTTPException: + raise + except Exception as e: + execution_time = (datetime.now() - start_time).total_seconds() + logger.error(f"❌ 전체 프로세스 실패: {e}") + raise HTTPException( + status_code=500, + detail=f"서비스 처리 중 예상치 못한 오류가 발생했습니다: {str(e)}" + ) + +def _is_duplicate_restaurant(restaurant1: RestaurantInfo, restaurant2: RestaurantInfo) -> bool: + """ + 두 음식점이 중복인지 확인 (개선된 로직) + + Args: + restaurant1: 첫 번째 음식점 + restaurant2: 두 번째 음식점 + + Returns: + 중복 여부 + """ + # 1. ID 기준 확인 + if restaurant1.id == restaurant2.id: + return True + + # 2. place_url에서 추출한 store_id 기준 확인 + store_id1 = _extract_store_id_from_place_url(restaurant1.place_url) + store_id2 = _extract_store_id_from_place_url(restaurant2.place_url) + + if store_id1 and store_id2 and store_id1 == store_id2: + return True + + # 3. restaurant.id와 place_url store_id 교차 확인 + if restaurant1.id == store_id2 or restaurant2.id == store_id1: + return True + + # 4. 이름 + 주소 기준 확인 (최후 방법) + if (restaurant1.place_name == restaurant2.place_name and + restaurant1.address_name == restaurant2.address_name): + return True + + return False + +def _extract_store_id_from_place_url(place_url: str) -> Optional[str]: + """ + 카카오맵 URL에서 store_id를 추출합니다. + + Args: + place_url: 카카오맵 장소 URL + + Returns: + 추출된 store_id 또는 None + """ + try: + if not place_url: + return None + + import re + # URL 패턴: https://place.map.kakao.com/123456789 + pattern = r'/(\d+)(?:\?|$|#)' + match = re.search(pattern, place_url) + + if match: + return match.group(1) + else: + return None + + except Exception: + return None + +@app.post( + "/action-recommendation", + response_model=ActionRecommendationResponse, + summary="액션 추천 요청", + description="점주가 Claude AI에게 액션 추천을 요청합니다." +) +async def action_recommendation( + request: ActionRecommendationRequest, + claude_service: ClaudeService = Depends(get_claude_service), + vector_service: VectorService = Depends(get_vector_service) +): + """🧠 Claude AI 액션 추천 API""" + try: + logger.info(f"액션 추천 요청: store_id={request.store_id}") + + start_time = datetime.now() + + # 1단계: Vector DB에서 컨텍스트 조회 + try: + db_status = vector_service.get_db_status() + + if db_status.get('total_documents', 0) == 0: + raise HTTPException( + status_code=404, + detail={ + "success": False, + "error": "NO_VECTOR_DATA", + "message": "Vector DB에 데이터가 없습니다. 먼저 /build-vector API를 호출하여 데이터를 구축해주세요.", + "timestamp": datetime.now().isoformat() + } + ) + + # Vector DB에서 유사한 케이스 검색 + context_data = vector_service.search_similar_cases(request.store_id, request.context) + + except HTTPException: + raise + except Exception as e: + logger.error(f"Vector DB 조회 실패: {e}") + # Vector DB 조회 실패해도 일반적인 추천은 제공 + context_data = None + + # 2단계: Claude AI 호출 (프롬프트 구성부터 파싱까지 모두 포함) + try: + # 컨텍스트 구성 + full_context = f"가게 ID: {request.store_id}\n점주 요청: {request.context}" + additional_context = context_data if context_data else None + + # Claude AI 액션 추천 생성 (완전한 처리) + claude_response, parsed_response = await claude_service.generate_action_recommendations( + context=full_context, + additional_context=additional_context + ) + + if not claude_response: + raise Exception("Claude AI로부터 응답을 받지 못했습니다") + + logger.info(f"Claude 응답 길이: {len(claude_response)} 문자") + json_parse_success = parsed_response is not None + + except Exception as e: + logger.error(f"Claude AI 호출 실패: {e}") + raise HTTPException( + status_code=500, + detail=f"AI 추천 생성 중 오류: {str(e)}" + ) + + # 3단계: 응답 구성 + claude_execution_time = (datetime.now() - start_time).total_seconds() + + # 가게 정보 추출 (Vector DB에서) + store_name = request.store_id # 기본값 + food_category = "기타" # 기본값 + + try: + store_context = vector_service.get_store_context(request.store_id) + if store_context: + store_name = store_context.get('store_name', request.store_id) + food_category = store_context.get('food_category', '기타') + except Exception as e: + logger.warning(f"가게 정보 추출 실패: {e}") + + response = ActionRecommendationResponse( + success=True, + message=f"액션 추천이 완료되었습니다. (실행시간: {claude_execution_time:.1f}초, JSON 파싱: {'성공' if json_parse_success else '실패'})", + claude_input=full_context + (f"\n--- 동종 업체 분석 데이터 ---\n{additional_context}" if additional_context else ""), + claude_response=claude_response, + parsed_response=parsed_response, + store_name=store_name, + food_category=food_category, + similar_stores_count=len(context_data.split("---")) if context_data else 0, + execution_time=claude_execution_time, + json_parse_success=json_parse_success + ) + + logger.info(f"✅ 액션 추천 완료: Claude 응답 {len(claude_response) if claude_response else 0} 문자, JSON 파싱 {'성공' if json_parse_success else '실패'}, {claude_execution_time:.1f}초 소요") + return response + + except HTTPException: + raise + except Exception as e: + logger.error(f"❌ 액션 추천 요청 실패: {str(e)}") + + raise HTTPException( + status_code=500, + detail={ + "success": False, + "error": "RECOMMENDATION_FAILED", + "message": f"액션 추천 중 오류가 발생했습니다: {str(e)}", + "timestamp": datetime.now().isoformat() + } + ) + +@app.get( + "/vector-status", + response_model=VectorDBStatusResponse, + summary="Vector DB 상태 조회", + description="Vector DB의 현재 상태를 조회합니다." +) +async def get_vector_db_status(vector_service: VectorService = Depends(get_vector_service)): + """Vector DB 상태 조회 API""" + try: + status_info = vector_service.get_db_status() + + status = VectorDBStatus( + collection_name=status_info['collection_name'], + total_documents=status_info['total_documents'], + total_stores=status_info['total_stores'], + db_path=status_info['db_path'], + last_updated=datetime.now().isoformat() + ) + + return VectorDBStatusResponse( + success=True, + status=status, + message="Vector DB 상태 조회가 완료되었습니다." + ) + + except Exception as e: + logger.error(f"Vector DB 상태 조회 실패: {e}") + + raise HTTPException( + status_code=500, + detail={ + "success": False, + "error": "STATUS_CHECK_FAILED", + "message": f"Vector DB 상태 조회 중 오류가 발생했습니다: {str(e)}", + "timestamp": datetime.now().isoformat() + } + ) + +@app.get("/health", summary="헬스 체크", description="API 서버 및 외부 서비스 상태를 확인합니다.") +async def health_check(): + """🏥 헬스체크 API""" + health_result = { + "status": "healthy", + "timestamp": datetime.now().isoformat(), + "services": {}, + "app_info": { + "name": settings.APP_TITLE, + "version": settings.APP_VERSION, + "startup_completed": app_state["startup_completed"] + } + } + + # 서비스별 헬스체크 + services_to_check = [ + ("restaurant_service", "restaurant_api"), + ("review_service", "review_api"), + ("claude_service", "claude_ai"), + ("vector_service", "vector_db") + ] + + healthy_count = 0 + total_checks = len(services_to_check) + + for service_key, health_key in services_to_check: + try: + service = app_state.get(service_key) + if service is None: + health_result["services"][health_key] = "not_initialized" + continue + + # 서비스별 헬스체크 메서드 호출 + if hasattr(service, 'health_check'): + status = await service.health_check() + else: + status = True # 헬스체크 메서드가 없으면 초기화됐다고 가정 + + # Vector DB의 경우 상세 정보 추가 + if health_key == "vector_db" and status: + try: + db_status = service.get_db_status() + health_result["vector_db_info"] = { + "total_documents": db_status.get('total_documents', 0), + "total_stores": db_status.get('total_stores', 0), + "db_path": db_status.get('db_path', '') + } + except: + pass + + health_result["services"][health_key] = "healthy" if status else "unhealthy" + if status: + healthy_count += 1 + + except Exception as e: + logger.warning(f"헬스체크 실패 - {service_key}: {e}") + health_result["services"][health_key] = f"error: {str(e)}" + + # 전체 상태 결정 + if healthy_count == total_checks: + health_result["status"] = "healthy" + elif healthy_count > 0: + health_result["status"] = "degraded" + else: + health_result["status"] = "unhealthy" + + # 요약 정보 + health_result["summary"] = { + "healthy_services": healthy_count, + "total_services": total_checks, + "health_percentage": round((healthy_count / total_checks) * 100, 1) + } + + # 초기화 에러가 있으면 포함 + if app_state["initialization_errors"]: + health_result["initialization_errors"] = app_state["initialization_errors"] + + # 환경 정보 + health_result["environment"] = { + "python_version": sys.version.split()[0], + "fastapi_version": fastapi.__version__, + "is_k8s": hasattr(settings, 'IS_K8S_ENV') and settings.IS_K8S_ENV, + "claude_model": settings.CLAUDE_MODEL + } + + # HTTP 상태 코드 결정 + if health_result["status"] == "healthy": + return health_result + elif health_result["status"] == "degraded": + return JSONResponse(status_code=200, content=health_result) # 부분 장애는 200 + else: + return JSONResponse(status_code=503, content=health_result) # 전체 장애는 503 + +# 🔧 전역 예외 처리 +@app.exception_handler(Exception) +async def global_exception_handler(request, exc): + """전역 예외 처리""" + logger.error(f"Unhandled exception: {exc}") + return JSONResponse( + status_code=500, + content={ + "error": "Internal server error", + "detail": str(exc) if settings.LOG_LEVEL.lower() == "debug" else "An unexpected error occurred", + "timestamp": datetime.now().isoformat(), + "path": str(request.url) + } + ) + +if __name__ == "__main__": + import uvicorn + + print("🍽️ " + "="*60) + print(f" {settings.APP_TITLE} 서버 시작") + print("="*64) + print(f"📊 구성 정보:") + print(f" - Python 버전: {sys.version.split()[0]}") + print(f" - FastAPI 버전: {fastapi.__version__}") + print(f" - Vector DB Path: {settings.VECTOR_DB_PATH}") + print(f" - Claude Model: {settings.CLAUDE_MODEL}") + print(f" - 환경: {'Kubernetes' if hasattr(settings, 'IS_K8S_ENV') and settings.IS_K8S_ENV else 'Local'}") + print() + print(f"📚 문서:") + print(f" - Swagger UI: http://{settings.HOST}:{settings.PORT}/docs") + print(f" - ReDoc: http://{settings.HOST}:{settings.PORT}/redoc") + print(f" - 메인 페이지: http://{settings.HOST}:{settings.PORT}/") + print() + + try: + uvicorn.run( + "app.main:app", # 🔧 문자열로 지정 (리로드 지원) + host=settings.HOST, + port=settings.PORT, + log_level=settings.LOG_LEVEL.lower(), + reload=False, # 프로덕션에서는 False + access_log=True, + loop="uvloop" if sys.platform != "win32" else "asyncio" + ) + except KeyboardInterrupt: + print("\n🛑 서버가 사용자에 의해 중단되었습니다.") + except Exception as e: + print(f"\n❌ 서버 시작 실패: {e}") + import traceback + traceback.print_exc() + sys.exit(1) + diff --git a/vector/app/requirements.txt b/vector/app/requirements.txt new file mode 100644 index 0000000..97bd268 --- /dev/null +++ b/vector/app/requirements.txt @@ -0,0 +1,67 @@ +# app/requirements.txt - 안정화된 Vector DB 서비스용 + +# ========================================== +# 기본 웹 프레임워크 (안정 버전) +# ========================================== +fastapi==0.104.1 +uvicorn[standard]==0.24.0 +pydantic==2.5.0 +python-dotenv==1.0.0 +python-multipart==0.0.6 + +# ========================================== +# HTTP 클라이언트 +# ========================================== +aiohttp==3.9.1 +requests==2.31.0 + +# ========================================== +# 데이터 처리 (안정 버전) +# ========================================== +numpy==1.24.3 +pandas==2.1.4 + +# ========================================== +# AI/ML 라이브러리 (호환성 검증된 버전) +# ========================================== +# PyTorch CPU 버전 (안정화) +torch==2.1.0+cpu --index-url https://download.pytorch.org/whl/cpu +torchvision==0.16.0+cpu --index-url https://download.pytorch.org/whl/cpu +torchaudio==2.1.0+cpu --index-url https://download.pytorch.org/whl/cpu + +# Transformer 라이브러리들 +tokenizers==0.15.2 +transformers==4.35.2 +huggingface-hub==0.19.4 + +# Sentence Transformers (안정 버전) +sentence-transformers==2.2.2 + +# ========================================== +# Vector DB - ChromaDB (안정 버전) +# ========================================== +# ChromaDB 0.4.24 사용 (호환성 검증됨) +chromadb==0.4.24 + +# ChromaDB 의존성 (호환 버전 고정) +hnswlib==0.7.0 +duckdb==0.9.2 + +# ========================================== +# Claude API (최신 안정 버전) +# ========================================== +anthropic>=0.40.0,<1.0.0 + +# ========================================== +# 기타 필수 라이브러리 +# ========================================== +typing-extensions==4.8.0 +sqlalchemy==2.0.23 + +# ========================================== +# 개발/디버깅 도구 (선택사항) +# ========================================== +# pytest==7.4.3 +# black==23.11.0 +# isort==5.12.0 + diff --git a/vector/app/services/claude_service.py b/vector/app/services/claude_service.py new file mode 100644 index 0000000..38ec659 --- /dev/null +++ b/vector/app/services/claude_service.py @@ -0,0 +1,336 @@ +# app/services/claude_service.py +import json +import logging +from typing import Optional, Dict, Any, Tuple +import anthropic +from ..config.settings import settings + +logger = logging.getLogger(__name__) + +class ClaudeService: + """Claude API 연동 서비스""" + + def __init__(self): + try: + # API 키 유효성 검사 + if not settings.CLAUDE_API_KEY or settings.CLAUDE_API_KEY.strip() == "": + raise ValueError("CLAUDE_API_KEY가 설정되지 않았습니다") + + # Claude 클라이언트 초기화 + self.client = anthropic.Anthropic(api_key=settings.CLAUDE_API_KEY) + self.model = settings.CLAUDE_MODEL + self.initialization_error = None + + logger.info(f"✅ ClaudeService 초기화 완료 (모델: {self.model})") + + except Exception as e: + self.initialization_error = str(e) + self.client = None + logger.error(f"❌ ClaudeService 초기화 실패: {e}") + + def is_ready(self) -> bool: + """서비스 준비 상태 확인""" + return self.client is not None and self.initialization_error is None + + def get_initialization_error(self) -> Optional[str]: + """초기화 에러 메시지 반환""" + return self.initialization_error + + async def test_api_connection(self) -> Tuple[bool, Optional[str]]: + """Claude API 연결을 테스트합니다.""" + if not self.is_ready(): + return False, self.initialization_error + + try: + logger.info("🔍 Claude API 연결 테스트 시작...") + + response = self.client.messages.create( + model=self.model, + max_tokens=50, + messages=[ + { + "role": "user", + "content": "안녕하세요. 연결 테스트입니다. '연결 성공'이라고 답변해주세요." + } + ] + ) + + if response.content and len(response.content) > 0: + logger.info("✅ Claude API 연결 테스트 성공") + return True, None + else: + error_msg = "Claude API 응답이 비어있음" + logger.warning(f"⚠️ {error_msg}") + return False, error_msg + + except Exception as e: + error_msg = f"Claude API 연결 테스트 실패: {str(e)}" + logger.error(f"❌ {error_msg}") + return False, error_msg + + async def generate_action_recommendations(self, context: str, additional_context: Optional[str] = None) -> Tuple[Optional[str], Optional[Dict[str, Any]]]: + """점주를 위한 액션 추천을 생성합니다.""" + if not self.is_ready(): + logger.error("ClaudeService가 준비되지 않음") + return None, None + + try: + logger.info("🤖 Claude API를 통한 액션 추천 생성 시작") + + prompt = self._build_action_prompt(context, additional_context) + + response = self.client.messages.create( + model=self.model, + max_tokens=4000, + temperature=0.7, + messages=[ + { + "role": "user", + "content": prompt + } + ] + ) + + if response.content and len(response.content) > 0: + raw_response = response.content[0].text + logger.info(f"✅ 액션 추천 생성 완료: {len(raw_response)} 문자") + + parsed_response = self._parse_json_response(raw_response) + return raw_response, parsed_response + else: + logger.warning("⚠️ Claude API 응답이 비어있음") + return None, None + + except Exception as e: + logger.error(f"❌ 액션 추천 생성 중 오류: {e}") + return None, None + + def _parse_json_response(self, raw_response: str) -> Optional[Dict[str, Any]]: + """Claude의 원본 응답에서 JSON을 추출하고 파싱합니다.""" + try: + import re + + # JSON 블록 찾기 + json_match = re.search(r'```json\s*\n(.*?)\n```', raw_response, re.DOTALL) + if json_match: + json_str = json_match.group(1).strip() + else: + brace_start = raw_response.find('{') + brace_end = raw_response.rfind('}') + + if brace_start != -1 and brace_end != -1 and brace_end > brace_start: + json_str = raw_response[brace_start:brace_end + 1] + else: + logger.warning("⚠️ JSON 패턴을 찾을 수 없음") + return None + + parsed_json = json.loads(json_str) + logger.info("✅ JSON 파싱 성공") + return parsed_json + + except json.JSONDecodeError as e: + logger.warning(f"⚠️ JSON 파싱 실패: {e}") + return None + except Exception as e: + logger.warning(f"⚠️ JSON 추출 실패: {e}") + return None + + def _build_action_prompt(self, context: str, additional_context: Optional[str] = None) -> str: + """액션 추천을 위한 프롬프트를 구성합니다.""" + + base_prompt = f"""당신은 소상공인을 위한 경영 컨설턴트입니다. 아래 정보를 바탕으로 실질적이고 구체적인 액션 추천을 해주세요. + +**분석 데이터:** +{context} + +**추천 요구사항:** +1. 단기 계획 (1-3개월): 즉시 실행 가능한 개선사항 +2. 중기 계획 (3-6개월): 점진적 개선 방안 +3. 장기 계획 (6개월-1년): 전략적 발전 방향 + +**응답 형식:** +반드시 아래 JSON 형식으로만 응답해주세요: + +```json +{{ + "summary": {{ + "current_situation": "현재 상황 요약", + "key_insights": ["핵심 인사이트 1", "핵심 인사이트 2", "핵심 인사이트 3"], + "priority_areas": ["우선 개선 영역 1", "우선 개선 영역 2"] + }}, + "action_plans": {{ + "short_term": [ + {{ + "title": "액션 제목", + "description": "구체적인 실행 방법", + "expected_impact": "예상 효과", + "timeline": "실행 기간", + "cost": "예상 비용" + }} + ], + "mid_term": [ + {{ + "title": "액션 제목", + "description": "구체적인 실행 방법", + "expected_impact": "예상 효과", + "timeline": "실행 기간", + "cost": "예상 비용" + }} + ], + "long_term": [ + {{ + "title": "액션 제목", + "description": "구체적인 실행 방법", + "expected_impact": "예상 효과", + "timeline": "실행 기간", + "cost": "예상 비용" + }} + ] + }}, + "implementation_tips": [ + "실행 팁 1", + "실행 팁 2", + "실행 팁 3" + ] +}} +``` + +**응답은 반드시 유효한 JSON 형식으로만 작성하고, JSON 앞뒤에 다른 텍스트는 포함하지 마세요.**""" + + if additional_context: + base_prompt += f"\n\n**점주 추가 요청사항:**\n{additional_context}\n" + + return base_prompt + + # ============================================================================= + # 호환성을 위한 메서드들 (기존 코드가 사용할 수 있도록) + # ============================================================================= + + async def get_recommendation(self, prompt: str) -> Optional[str]: + """Claude API를 호출하여 추천을 받습니다. (호환성용)""" + if not self.is_ready(): + logger.error("ClaudeService가 준비되지 않음") + return None + + try: + logger.info("🤖 Claude API 호출 시작") + + response = self.client.messages.create( + model=self.model, + max_tokens=4000, + temperature=0.7, + messages=[ + { + "role": "user", + "content": prompt + } + ] + ) + + if response.content and len(response.content) > 0: + raw_response = response.content[0].text + logger.info(f"✅ Claude API 응답 성공: {len(raw_response)} 문자") + return raw_response + else: + logger.warning("⚠️ Claude API 응답이 비어있음") + return None + + except Exception as e: + logger.error(f"❌ Claude API 호출 실패: {e}") + return None + + def parse_recommendation_response(self, raw_response: str) -> Optional[Dict[str, Any]]: + """Claude 응답에서 JSON을 추출하고 파싱합니다. (호환성용)""" + return self._parse_json_response(raw_response) + + def build_recommendation_prompt(self, store_id: str, context: str, vector_context: Optional[str] = None) -> str: + """액션 추천용 프롬프트를 구성합니다. (호환성용)""" + + prompt_parts = [ + "당신은 소상공인을 위한 경영 컨설턴트입니다.", + f"가게 ID: {store_id}", + f"점주 요청: {context}" + ] + + if vector_context: + prompt_parts.extend([ + "\n--- 동종 업체 분석 데이터 ---", + vector_context, + "--- 분석 데이터 끝 ---\n" + ]) + + prompt_parts.extend([ + "\n위 정보를 바탕으로 실질적이고 구체적인 액션 추천을 해주세요.", + "응답은 반드시 아래 JSON 형식으로만 작성해주세요:", + "", + "```json", + "{", + ' "summary": {', + ' "current_situation": "현재 상황 요약",', + ' "key_insights": ["핵심 인사이트 1", "핵심 인사이트 2"],', + ' "priority_areas": ["우선 개선 영역 1", "우선 개선 영역 2"]', + ' },', + ' "action_plans": {', + ' "short_term": [', + ' {', + ' "title": "즉시 실행 가능한 액션",', + ' "description": "구체적인 실행 방법",', + ' "expected_impact": "예상 효과",', + ' "timeline": "1-2주",', + ' "cost": "예상 비용"', + ' }', + ' ],', + ' "mid_term": [', + ' {', + ' "title": "중기 개선 방안",', + ' "description": "구체적인 실행 방법",', + ' "expected_impact": "예상 효과",', + ' "timeline": "1-3개월",', + ' "cost": "예상 비용"', + ' }', + ' ]', + ' },', + ' "implementation_tips": ["실행 팁 1", "실행 팁 2"]', + "}", + "```" + ]) + + return "\n".join(prompt_parts) + + def create_prompt_for_api_response(self, context: str, additional_context: Optional[str] = None) -> str: + """API 응답용 프롬프트를 생성합니다. (호환성용)""" + return self._build_action_prompt(context, additional_context) + + # ============================================================================= + # 헬스체크 및 상태 확인 메서드들 + # ============================================================================= + + def get_health_status(self) -> Dict[str, Any]: + """ClaudeService 상태 확인""" + try: + status = { + "service": "claude_api", + "status": "healthy" if self.is_ready() else "unhealthy", + "model": self.model, + "api_key_configured": bool(settings.CLAUDE_API_KEY and settings.CLAUDE_API_KEY.strip()), + "timestamp": self._get_timestamp() + } + + if self.initialization_error: + status["initialization_error"] = self.initialization_error + status["status"] = "error" + + return status + + except Exception as e: + return { + "service": "claude_api", + "status": "error", + "error": str(e), + "timestamp": self._get_timestamp() + } + + def _get_timestamp(self) -> str: + """현재 시간 문자열 반환""" + from datetime import datetime + return datetime.now().isoformat() \ No newline at end of file diff --git a/vector/app/services/restaurant_service.py b/vector/app/services/restaurant_service.py new file mode 100644 index 0000000..5a7100e --- /dev/null +++ b/vector/app/services/restaurant_service.py @@ -0,0 +1,235 @@ +# app/services/restaurant_service.py (수정된 버전) +import aiohttp +import asyncio +import logging +from typing import List, Optional, Dict, Any +from ..config.settings import settings +from ..models.restaurant_models import RestaurantInfo +from ..utils.category_utils import extract_food_category + +logger = logging.getLogger(__name__) + +class RestaurantService: + """음식점 API 연동 서비스""" + + def __init__(self): + self.base_url = settings.get_restaurant_api_url() + self.timeout = aiohttp.ClientTimeout(total=settings.REQUEST_TIMEOUT) + + async def find_store_by_name_and_region(self, region: str, store_name: str) -> Optional[RestaurantInfo]: + """ + 지역과 가게명으로 가게를 찾습니다. + + Args: + region: 지역 (시군구 + 읍면동) + store_name: 가게명 + + Returns: + 찾은 가게 정보 (첫 번째 결과) + """ + try: + logger.info(f"가게 검색 시작: region={region}, store_name={store_name}") + + async with aiohttp.ClientSession(timeout=self.timeout) as session: + # Restaurant API 호출 + url = f"{self.base_url}/collect" + payload = { + "query": store_name, + "region": region, + "size": 15, + "pages": 3, + "save_to_file": False + } + + logger.info(f"Restaurant API 호출: {url}") + async with session.post(url, json=payload) as response: + if response.status == 200: + data = await response.json() + restaurants = data.get('restaurants', []) + + if restaurants: + # 첫 번째 결과 반환 + restaurant_data = restaurants[0] + restaurant = RestaurantInfo(**restaurant_data) + + logger.info(f"가게 찾기 성공: {restaurant.place_name}") + return restaurant + else: + logger.warning(f"가게를 찾을 수 없음: {store_name}") + return None + else: + logger.error(f"Restaurant API 호출 실패: HTTP {response.status}") + error_text = await response.text() + logger.error(f"Error response: {error_text}") + return None + + except asyncio.TimeoutError: + logger.error("Restaurant API 호출 타임아웃") + return None + except Exception as e: + logger.error(f"가게 검색 중 오류: {str(e)}") + return None + + def _clean_food_category(self, food_category: str) -> str: + """ + 음식 카테고리를 정리하여 검색 키워드로 변환합니다. + + Args: + food_category: 원본 음식 카테고리 (예: "육류,고기") + + Returns: + 정리된 검색 키워드 (예: "육류 고기") + """ + if not food_category: + return "음식점" + + # 콤마와 슬래시를 공백으로 변경 + cleaned = food_category.replace(',', ' ').replace('/', ' ') + + # 불필요한 단어 제거 + stop_words = ['음식점', '요리', '전문점', '맛집'] + keywords = [] + + for keyword in cleaned.split(): + keyword = keyword.strip() + if keyword and keyword not in stop_words: + keywords.append(keyword) + + # 키워드가 없으면 기본 검색어 사용 + if not keywords: + return "음식점" + + # 🔧 지역 정보는 포함하지 않고 음식 카테고리만 반환 + return ' '.join(keywords) + + async def find_similar_stores(self, region: str, food_category: str, max_count: int = 50) -> List[RestaurantInfo]: + """ + 동종 업체를 찾습니다. + + Args: + region: 지역 + food_category: 음식 카테고리 + max_count: 최대 검색 개수 + + Returns: + 동종 업체 목록 + """ + try: + logger.info(f"동종 업체 검색 시작: region={region}, food_category={food_category}") + + # 🔧 검색 쿼리 생성 (음식 카테고리만 포함) + search_query = self._clean_food_category(food_category) + logger.info(f"음식점 수집 요청: query='{search_query}' region='{region}' size=15 pages=1 save_to_file=False") + + similar_stores = [] + + async with aiohttp.ClientSession(timeout=self.timeout) as session: + # 페이지별로 검색 (최대 5페이지) + max_pages = min(5, (max_count // 15) + 1) + + for page in range(1, max_pages + 1): + if len(similar_stores) >= max_count: + break + + url = f"{self.base_url}/collect" + # 🔧 수정된 payload - query에는 음식 카테고리만, region은 분리 + payload = { + "query": search_query, # 🔧 음식 카테고리만 포함 + "region": region, # 🔧 지역 정보는 별도 파라미터 + "size": 15, + "pages": 1, # 페이지별로 하나씩 호출 + "save_to_file": False + } + + logger.info(f"동종 업체 검색 페이지 {page}: query='{search_query}' region='{region}'") + + try: + async with session.post(url, json=payload) as response: + if response.status == 200: + data = await response.json() + restaurants = data.get('restaurants', []) + + for restaurant_data in restaurants: + if len(similar_stores) >= max_count: + break + + try: + restaurant = RestaurantInfo(**restaurant_data) + + # 카테고리 필터링 + restaurant_category = extract_food_category(restaurant.category_name) + if self._is_similar_food_category(food_category, restaurant_category): + similar_stores.append(restaurant) + logger.debug(f"유사 카테고리 매치: {restaurant.place_name} ({restaurant_category})") + + except Exception as e: + logger.warning(f"음식점 데이터 파싱 실패: {e}") + continue + + logger.info(f"페이지 {page} 완료: {len(restaurants)}개 음식점 수집") + + else: + logger.warning(f"동종 업체 검색 실패 (페이지 {page}): HTTP {response.status}") + continue + + except Exception as e: + logger.warning(f"페이지 {page} 검색 중 오류: {e}") + continue + + # API 요청 제한을 위한 지연 + await asyncio.sleep(settings.REQUEST_DELAY) + + logger.info(f"동종 업체 검색 완료: 총 {len(similar_stores)}개") + return similar_stores + + except Exception as e: + logger.error(f"동종 업체 검색 중 오류: {str(e)}") + return [] + + def _is_similar_food_category(self, target_category: str, restaurant_category: str) -> bool: + """ + 음식 카테고리가 유사한지 확인합니다. + + Args: + target_category: 대상 카테고리 + restaurant_category: 음식점 카테고리 + + Returns: + 유사성 여부 + """ + if not target_category or not restaurant_category: + return False + + # 정규화 + target_lower = target_category.lower().strip() + restaurant_lower = restaurant_category.lower().strip() + + # 완전 일치 + if target_lower == restaurant_lower: + return True + + # 키워드 기반 매칭 + target_keywords = set(target_lower.replace(',', ' ').replace('/', ' ').split()) + restaurant_keywords = set(restaurant_lower.replace(',', ' ').replace('/', ' ').split()) + + # 교집합이 있으면 유사한 것으로 판단 + common_keywords = target_keywords.intersection(restaurant_keywords) + + return len(common_keywords) > 0 + + async def health_check(self) -> bool: + """ + Restaurant API 상태를 확인합니다. + + Returns: + API 상태 (True: 정상, False: 비정상) + """ + try: + async with aiohttp.ClientSession(timeout=self.timeout) as session: + url = f"{self.base_url}/health" + async with session.get(url) as response: + return response.status == 200 + except Exception as e: + logger.error(f"Restaurant API 헬스체크 실패: {e}") + return False + diff --git a/vector/app/services/review_service.py b/vector/app/services/review_service.py new file mode 100644 index 0000000..c3b7f48 --- /dev/null +++ b/vector/app/services/review_service.py @@ -0,0 +1,467 @@ +# vector/app/services/review_service.py +import aiohttp +import asyncio +import logging +from typing import List, Dict, Any, Tuple, Optional +from ..config.settings import settings +from ..models.review_models import ReviewAnalysisResponse, StoreInfo, ReviewData +from ..models.restaurant_models import RestaurantInfo + +logger = logging.getLogger(__name__) + +class ReviewService: + """리뷰 API 연동 서비스 (본인 가게 우선 처리 강화)""" + + def __init__(self): + self.base_url = settings.get_review_api_url() + self.timeout = aiohttp.ClientTimeout(total=settings.REQUEST_TIMEOUT) + + async def collect_store_reviews(self, store_id: str, max_reviews: int = 100) -> Tuple[Optional[Dict[str, Any]], List[Dict[str, Any]]]: + """ + 단일 가게의 리뷰를 수집합니다. (본인 가게용 강화 처리) + + Args: + store_id: 카카오맵 가게 ID + max_reviews: 최대 수집할 리뷰 수 + + Returns: + (가게 정보, 리뷰 목록) 튜플 + """ + try: + logger.info(f"🏪 가게 리뷰 수집 시작: store_id={store_id} (최대 {max_reviews}개)") + + # 본인 가게는 더 관대한 타임아웃 설정 + timeout = aiohttp.ClientTimeout(total=900) # 15분 + + async with aiohttp.ClientSession(timeout=timeout) as session: + url = f"{self.base_url}/analyze" + payload = { + "store_id": store_id, + "days_limit": None, # 모든 날짜의 리뷰 수집 + "max_time": min(600, max_reviews * 3) # 리뷰 수에 따라 시간 조정, 최대 10분 + } + + logger.info(f"Review API 호출: {url} (타임아웃: {payload['max_time']}초)") + + async with session.post(url, json=payload) as response: + if response.status == 200: + data = await response.json() + + if data.get('success', False): + store_info = data.get('store_info') + reviews = data.get('reviews', []) + + logger.info(f"📊 원본 리뷰 수집: {len(reviews)}개") + + # 리뷰 품질 필터링 + filtered_reviews = self._filter_quality_reviews(reviews) + logger.info(f"📊 품질 필터링 후: {len(filtered_reviews)}개") + + # 리뷰 개수 제한 + if len(filtered_reviews) > max_reviews: + filtered_reviews = filtered_reviews[:max_reviews] + logger.info(f"📊 최종 리뷰 수: {len(filtered_reviews)}개 (제한 적용)") + + # 가게 정보와 리뷰 변환 + converted_store_info = self._convert_store_info(store_info) + converted_reviews = self._convert_reviews(filtered_reviews) + + if converted_store_info and converted_reviews: + logger.info(f"✅ 리뷰 수집 성공: {len(converted_reviews)}개") + return converted_store_info, converted_reviews + else: + logger.warning("⚠️ 변환된 데이터가 비어있음") + return None, [] + else: + error_msg = data.get('message', 'Unknown error') + logger.error(f"❌ 리뷰 분석 실패: {error_msg}") + return None, [] + else: + logger.error(f"❌ Review API 호출 실패: HTTP {response.status}") + error_text = await response.text() + logger.error(f"Error response: {error_text}") + return None, [] + + except asyncio.TimeoutError: + logger.error("❌ Review API 호출 타임아웃") + return None, [] + except Exception as e: + logger.error(f"❌ 리뷰 수집 중 오류: {str(e)}") + return None, [] + + def _filter_quality_reviews(self, reviews: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + """리뷰 품질 필터링""" + try: + filtered = [] + + for review in reviews: + content = review.get('content', '').strip() + rating = review.get('rating', 0) + + # 품질 기준 + if (len(content) >= 10 and # 최소 10자 이상 + rating > 0 and # 별점이 있어야 함 + not self._is_spam_review(content)): # 스팸 제외 + filtered.append(review) + + logger.debug(f"품질 필터링: {len(reviews)} → {len(filtered)}") + return filtered + + except Exception as e: + logger.warning(f"⚠️ 리뷰 필터링 실패: {e}") + return reviews # 실패 시 원본 반환 + + def _is_spam_review(self, content: str) -> bool: + """스팸 리뷰 판별""" + try: + spam_keywords = [ + "추천추천", "최고최고", "맛있어요맛있어요", + "좋아요좋아요", "ㅎㅎㅎㅎ", "ㅋㅋㅋㅋ", + "굿굿굿", "Nice", "Good" + ] + + content_lower = content.lower() + + # 스팸 키워드 확인 + for keyword in spam_keywords: + if keyword.lower() in content_lower: + return True + + # 너무 짧거나 반복 문자 확인 + if len(set(content.replace(' ', ''))) < 3: # 고유 문자 3개 미만 + return True + + return False + + except Exception as e: + logger.warning(f"⚠️ 스팸 판별 실패: {e}") + return False + + def _convert_store_info(self, store_info): + """가게 정보 변환 (강화된 버전)""" + if not store_info: + logger.warning("⚠️ 가게 정보가 비어있음") + return None + + try: + converted = { + 'id': str(store_info.get('id', '')), + 'name': str(store_info.get('name', '')), + 'category': str(store_info.get('category', '')), + 'rating': str(store_info.get('rating', '')), + 'review_count': str(store_info.get('review_count', '')), + 'status': str(store_info.get('status', '')), + 'address': str(store_info.get('address', '')) + } + + # 필수 필드 확인 (ID와 이름은 반드시 있어야 함) + if not converted['id'] or not converted['name']: + logger.warning(f"⚠️ 필수 가게 정보 누락: ID={converted['id']}, Name={converted['name']}") + return None + + logger.debug(f"가게 정보 변환 성공: {converted['name']}") + return converted + + except Exception as e: + logger.error(f"❌ 가게 정보 변환 실패: {e}") + return None + + def _convert_reviews(self, reviews): + """리뷰 목록 변환 (강화된 버전)""" + if not reviews: + logger.warning("⚠️ 리뷰 목록이 비어있음") + return [] + + converted_reviews = [] + + for i, review in enumerate(reviews): + try: + # 안전한 형변환 + rating = 0 + try: + rating = int(review.get('rating', 0)) + except (ValueError, TypeError): + rating = 0 + + likes = 0 + try: + likes = int(review.get('likes', 0)) + except (ValueError, TypeError): + likes = 0 + + photo_count = 0 + try: + photo_count = int(review.get('photo_count', 0)) + except (ValueError, TypeError): + photo_count = 0 + + converted_review = { + 'reviewer_name': str(review.get('reviewer_name', 'Anonymous')), + 'rating': rating, + 'date': str(review.get('date', '')), + 'content': str(review.get('content', '')).strip(), + 'badges': list(review.get('badges', [])), + 'likes': likes, + 'photo_count': photo_count, + 'has_photos': bool(photo_count > 0) + } + + # 기본 검증 + if converted_review['content'] and converted_review['rating'] > 0: + converted_reviews.append(converted_review) + else: + logger.debug(f"⚠️ 리뷰 {i+1} 품질 미달로 제외") + + except Exception as e: + logger.warning(f"⚠️ 리뷰 {i+1} 변환 실패: {e}") + continue + + logger.info(f"리뷰 변환 완료: {len(reviews)} → {len(converted_reviews)}") + return converted_reviews + + async def collect_multiple_stores_reviews(self, restaurants: List[RestaurantInfo]) -> List[Tuple[str, Dict[str, Any], List[Dict[str, Any]]]]: + """ + 여러 가게의 리뷰를 수집합니다. + + Args: + restaurants: 음식점 목록 + + Returns: + (store_id, 가게 정보, 리뷰 목록) 튜플의 리스트 + """ + try: + logger.info(f"다중 가게 리뷰 수집 시작: {len(restaurants)}개 가게") + + results = [] + + # 동시성 제한을 위해 세마포어 사용 + semaphore = asyncio.Semaphore(3) # 최대 3개 동시 요청 + + async def collect_single_store(restaurant: RestaurantInfo): + async with semaphore: + try: + # 카카오맵 place_url에서 store_id 추출 시도 + store_id = self._extract_store_id_from_url(restaurant.place_url) + + if not store_id: + # URL에서 추출 실패 시 restaurant.id 사용 + store_id = restaurant.id + + if not store_id: + logger.warning(f"Store ID를 찾을 수 없음: {restaurant.place_name}") + return None + + logger.info(f"가게 {restaurant.place_name} 리뷰 수집 중...") + + # 리뷰 수집 (최대 50개로 제한) + store_info, reviews = await self.collect_store_reviews( + store_id, + max_reviews=settings.MAX_REVIEWS_PER_RESTAURANT + ) + + if store_info and reviews: + return (store_id, store_info, reviews) + else: + logger.warning(f"가게 {restaurant.place_name}의 리뷰 수집 실패") + return None + + except Exception as e: + logger.error(f"가게 {restaurant.place_name} 리뷰 수집 중 오류: {e}") + return None + + finally: + # API 요청 제한을 위한 지연 + await asyncio.sleep(settings.REQUEST_DELAY) + + # 병렬 처리 + tasks = [collect_single_store(restaurant) for restaurant in restaurants] + results_raw = await asyncio.gather(*tasks, return_exceptions=True) + + # 성공한 결과만 필터링 + for result in results_raw: + if result and not isinstance(result, Exception): + results.append(result) + + logger.info(f"다중 가게 리뷰 수집 완료: {len(results)}개 성공") + return results + + except Exception as e: + logger.error(f"다중 가게 리뷰 수집 중 오류: {str(e)}") + return [] + + def _extract_store_id_from_url(self, place_url: str) -> Optional[str]: + """ + 카카오맵 URL에서 store_id를 추출합니다. + + Args: + place_url: 카카오맵 장소 URL + + Returns: + 추출된 store_id 또는 None + """ + try: + if not place_url: + return None + + # URL 패턴: https://place.map.kakao.com/123456789 + import re + pattern = r'/(\d+)(?:\?|$|#)' + match = re.search(pattern, place_url) + + if match: + store_id = match.group(1) + logger.debug(f"URL에서 store_id 추출: {store_id}") + return store_id + else: + logger.debug(f"URL에서 store_id 추출 실패: {place_url}") + return None + + except Exception as e: + logger.warning(f"store_id 추출 중 오류: {e}") + return None + + def _build_store_info_from_restaurant(self, restaurant: RestaurantInfo) -> Dict[str, Any]: + """ + RestaurantInfo를 store_info 형식으로 변환합니다. + + Args: + restaurant: RestaurantInfo 객체 + + Returns: + store_info 딕셔너리 + """ + try: + return { + 'id': restaurant.id, + 'name': restaurant.place_name, + 'category': restaurant.category_name, + 'rating': '', # API에서 제공되지 않음 + 'review_count': '', # API에서 제공되지 않음 + 'status': '', # API에서 제공되지 않음 + 'address': restaurant.address_name + } + except Exception as e: + logger.error(f"RestaurantInfo 변환 실패: {e}") + return { + 'id': '', + 'name': '', + 'category': '', + 'rating': '', + 'review_count': '', + 'status': '', + 'address': '' + } + + def _convert_single_review_data(self, review_data: dict) -> dict: + """ + 단일 리뷰 데이터 변환 + + Args: + review_data: API 응답의 단일 리뷰 데이터 + + Returns: + 변환된 리뷰 데이터 + """ + try: + return { + 'reviewer_name': review_data.get('reviewer_name', ''), + 'reviewer_level': review_data.get('reviewer_level', ''), + 'reviewer_stats': review_data.get('reviewer_stats', {}), + 'rating': int(review_data.get('rating', 0)), + 'date': review_data.get('date', ''), + 'content': review_data.get('content', ''), + 'badges': review_data.get('badges', []), + 'likes': int(review_data.get('likes', 0)), + 'photo_count': int(review_data.get('photo_count', 0)), + 'has_photos': bool(review_data.get('has_photos', False)) + } + except Exception as e: + logger.warning(f"단일 리뷰 데이터 변환 실패: {e}") + return { + 'reviewer_name': 'Unknown', + 'reviewer_level': '', + 'reviewer_stats': {}, + 'rating': 0, + 'date': '', + 'content': '', + 'badges': [], + 'likes': 0, + 'photo_count': 0, + 'has_photos': False + } + + async def health_check(self) -> bool: + """ + Review API 상태를 확인합니다. + + Returns: + API 상태 (True: 정상, False: 비정상) + """ + try: + async with aiohttp.ClientSession(timeout=self.timeout) as session: + url = f"{self.base_url}/health" + async with session.get(url) as response: + is_healthy = response.status == 200 + if is_healthy: + logger.debug("Review API 헬스체크 성공") + else: + logger.warning(f"Review API 헬스체크 실패: HTTP {response.status}") + return is_healthy + except Exception as e: + logger.error(f"Review API 헬스체크 실패: {e}") + return False + + def get_api_info(self) -> Dict[str, Any]: + """ + Review API 정보를 반환합니다. + + Returns: + API 정보 딕셔너리 + """ + return { + "service_name": "Review API Service", + "base_url": self.base_url, + "timeout": self.timeout.total, + "max_reviews_per_restaurant": settings.MAX_REVIEWS_PER_RESTAURANT, + "request_delay": settings.REQUEST_DELAY + } + + async def test_connection(self) -> Dict[str, Any]: + """ + Review API 연결을 테스트합니다. + + Returns: + 테스트 결과 + """ + test_result = { + "service": "Review API", + "base_url": self.base_url, + "status": "unknown", + "response_time": None, + "error": None + } + + try: + import time + start_time = time.time() + + is_healthy = await self.health_check() + + response_time = time.time() - start_time + test_result["response_time"] = round(response_time, 3) + + if is_healthy: + test_result["status"] = "healthy" + logger.info(f"Review API 연결 테스트 성공: {response_time:.3f}초") + else: + test_result["status"] = "unhealthy" + test_result["error"] = "Health check failed" + logger.warning("Review API 연결 테스트 실패: 헬스체크 실패") + + except Exception as e: + test_result["status"] = "error" + test_result["error"] = str(e) + logger.error(f"Review API 연결 테스트 오류: {e}") + + return test_result + diff --git a/vector/app/services/vector_service.py b/vector/app/services/vector_service.py new file mode 100644 index 0000000..eb9ba99 --- /dev/null +++ b/vector/app/services/vector_service.py @@ -0,0 +1,728 @@ +# app/services/vector_service.py (개선된 버전) +import os +import json +import logging +import time +import shutil +import signal +from datetime import datetime +from typing import List, Dict, Any, Optional, Tuple +import chromadb +from chromadb.config import Settings as ChromaSettings +from sentence_transformers import SentenceTransformer +from ..config.settings import settings +from ..utils.data_utils import ( + create_store_hash, combine_store_and_reviews, generate_review_summary, + extract_text_for_embedding, create_metadata, is_duplicate_store +) + +logger = logging.getLogger(__name__) + +class VectorService: + """Vector DB 서비스 (개선된 초기화 로직)""" + + def __init__(self): + self.db_path = settings.VECTOR_DB_PATH + self.collection_name = settings.VECTOR_DB_COLLECTION + self.embedding_model_name = settings.EMBEDDING_MODEL + + # 상태 변수 + self.client = None + self.collection = None + self.embedding_model = None + self.initialization_error = None + + # 안전한 초기화 시도 + self._safe_initialize() + + def _safe_initialize(self): + """안전한 초기화 - 개선된 로직""" + try: + logger.info("🔧 VectorService 초기화 시작...") + + # 1단계: 디렉토리 권한 확인 + self._ensure_directory_permissions() + + # 2단계: ChromaDB 초기화 (호환성 확인 포함) + self._initialize_chromadb_with_compatibility_check() + + # 3단계: 임베딩 모델 로드 + self._initialize_embedding_model() + + logger.info("✅ VectorService 초기화 완료") + + except Exception as e: + self.initialization_error = str(e) + logger.error(f"❌ VectorService 초기화 실패: {e}") + logger.info("🔄 서비스는 런타임에 재시도 가능합니다") + + def _ensure_directory_permissions(self): + """Vector DB 디렉토리 권한을 확인하고 생성합니다""" + try: + logger.info(f"📁 Vector DB 디렉토리 설정: {self.db_path}") + + # 절대 경로로 변환 + abs_path = os.path.abspath(self.db_path) + + # 디렉토리 생성 + os.makedirs(abs_path, mode=0o755, exist_ok=True) + + # 권한 확인 + if not os.access(abs_path, os.W_OK): + logger.warning(f"⚠️ 쓰기 권한 없음: {abs_path}") + + # 권한 변경 시도 + try: + os.chmod(abs_path, 0o755) + logger.info("✅ 디렉토리 권한 변경 성공") + except Exception as chmod_error: + logger.warning(f"⚠️ 권한 변경 실패: {chmod_error}") + # 임시 디렉토리로 대체 + import tempfile + temp_dir = tempfile.mkdtemp(prefix="vectordb_") + logger.info(f"🔄 임시 디렉토리 사용: {temp_dir}") + self.db_path = temp_dir + abs_path = temp_dir + + # 테스트 파일 생성/삭제로 권한 확인 + test_file = os.path.join(abs_path, "test_permissions.tmp") + try: + with open(test_file, 'w') as f: + f.write("test") + os.remove(test_file) + logger.info("✅ 디렉토리 권한 확인 완료") + except Exception as test_error: + raise Exception(f"디렉토리 권한 테스트 실패: {test_error}") + + except Exception as e: + logger.error(f"❌ 디렉토리 설정 실패: {e}") + raise + + def _initialize_chromadb_with_compatibility_check(self): + """ChromaDB 초기화 (호환성 확인 포함)""" + max_retries = 3 + retry_delay = 2 + + for attempt in range(max_retries): + try: + logger.info(f"🔄 ChromaDB 초기화 시도 {attempt + 1}/{max_retries}") + + # 1단계: 기존 DB 호환성 확인 + existing_db_valid = self._check_existing_db_compatibility() + + # 2단계: ChromaDB 클라이언트 생성 + self._create_chromadb_client() + + # 3단계: 컬렉션 초기화 + self._initialize_collection(existing_db_valid) + + logger.info("✅ ChromaDB 초기화 완료") + return # 성공 시 루프 종료 + + except Exception as e: + logger.error(f"❌ ChromaDB 초기화 실패 (시도 {attempt + 1}): {e}") + + if attempt < max_retries - 1: + logger.info(f"🔄 {retry_delay}초 후 재시도...") + time.sleep(retry_delay) + retry_delay *= 2 # 지수 백오프 + else: + raise Exception(f"ChromaDB 초기화 최종 실패: {e}") + + def _check_existing_db_compatibility(self): + """기존 DB 호환성 확인""" + try: + if not os.path.exists(self.db_path): + logger.info("📁 새 DB 디렉토리 - 스키마 확인 불필요") + return False + + db_files = [f for f in os.listdir(self.db_path) if not f.startswith('.')] + if not db_files: + logger.info("📁 빈 DB 디렉토리 - 스키마 확인 불필요") + return False + + logger.info(f"📁 기존 DB 파일 발견: {db_files}") + + # 실제 호환성 테스트 + logger.info("🔍 기존 DB 호환성 테스트 중...") + + try: + # 테스트용 클라이언트 생성 + test_client = chromadb.PersistentClient(path=self.db_path) + test_client.heartbeat() + + # 컬렉션 접근 시도 + try: + test_collection = test_client.get_collection(name=self.collection_name) + count = test_collection.count() + logger.info(f"✅ 기존 DB 호환성 확인 완료: {count}개 벡터 존재") + return True + except Exception as collection_error: + logger.info(f"📝 기존 컬렉션 없음 (정상): {collection_error}") + return True # DB는 정상, 컬렉션만 새로 생성하면 됨 + + except Exception as compatibility_error: + # 실제 호환성 문제 발견 + logger.warning(f"⚠️ 실제 호환성 문제 발견: {compatibility_error}") + self._backup_incompatible_db() + return False + + except Exception as e: + logger.warning(f"⚠️ 호환성 확인 중 오류: {e}") + return False + + def _backup_incompatible_db(self): + """호환성 문제가 있는 DB 백업""" + try: + backup_path = f"{self.db_path}_backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}" + logger.warning(f"🔄 호환성 문제로 기존 DB 백업: {backup_path}") + + shutil.move(self.db_path, backup_path) + logger.info(f"✅ 기존 DB 백업 완료: {backup_path}") + + # 새 디렉토리 생성 + os.makedirs(self.db_path, exist_ok=True) + + # 오래된 백업 정리 (7일 이상) + self._cleanup_old_backups() + + except Exception as backup_error: + logger.warning(f"⚠️ 백업 실패, 강제 삭제 진행: {backup_error}") + shutil.rmtree(self.db_path, ignore_errors=True) + os.makedirs(self.db_path, exist_ok=True) + + def _cleanup_old_backups(self): + """오래된 백업 파일 정리 (7일 이상)""" + try: + base_path = os.path.dirname(self.db_path) + backup_pattern = f"{os.path.basename(self.db_path)}_backup_" + cutoff_time = time.time() - (7 * 24 * 3600) # 7일 전 + + for item in os.listdir(base_path): + if item.startswith(backup_pattern): + backup_path = os.path.join(base_path, item) + if os.path.isdir(backup_path) and os.path.getctime(backup_path) < cutoff_time: + shutil.rmtree(backup_path, ignore_errors=True) + logger.info(f"🗑️ 오래된 백업 삭제: {backup_path}") + + except Exception as e: + logger.warning(f"⚠️ 백업 정리 중 오류: {e}") + + def _create_chromadb_client(self): + """ChromaDB 클라이언트 생성""" + try: + # 최신 버전 호환 설정 + chroma_settings = ChromaSettings( + anonymized_telemetry=False, + allow_reset=True, + is_persistent=True + ) + + self.client = chromadb.PersistentClient( + path=self.db_path, + settings=chroma_settings + ) + logger.info("✅ ChromaDB 클라이언트 생성 성공") + + except Exception as modern_error: + logger.warning(f"⚠️ 최신 설정 실패, 간단한 설정으로 재시도: {modern_error}") + + # 간단한 설정으로 재시도 + self.client = chromadb.PersistentClient(path=self.db_path) + logger.info("✅ ChromaDB 간단 설정 클라이언트 생성 성공") + + # 연결 테스트 + try: + self.client.heartbeat() + logger.info("✅ ChromaDB 연결 테스트 성공") + except Exception as heartbeat_error: + logger.warning(f"⚠️ Heartbeat 실패 (무시): {heartbeat_error}") + + def _initialize_collection(self, existing_db_valid: bool): + """컬렉션 초기화""" + try: + if existing_db_valid: + # 기존 컬렉션 로드 시도 + try: + self.collection = self.client.get_collection(name=self.collection_name) + count = self.collection.count() + logger.info(f"✅ 기존 컬렉션 로드 성공: {self.collection_name} ({count}개 벡터)") + return + except Exception as get_error: + logger.info(f"📝 기존 컬렉션 없음, 새로 생성: {get_error}") + + # 새 컬렉션 생성 + self.collection = self.client.create_collection( + name=self.collection_name, + metadata={ + "description": "Restaurant reviews vector store", + "created_at": datetime.now().isoformat(), + "version": "1.0" + } + ) + logger.info(f"✅ 새 컬렉션 생성 성공: {self.collection_name}") + + except Exception as create_error: + logger.error(f"❌ 컬렉션 초기화 실패: {create_error}") + + # 대체 컬렉션명으로 재시도 + fallback_name = f"{self.collection_name}_{int(time.time())}" + logger.info(f"🔄 대체 컬렉션명으로 재시도: {fallback_name}") + + self.collection = self.client.create_collection( + name=fallback_name, + metadata={"description": "Restaurant reviews (fallback)"} + ) + self.collection_name = fallback_name + logger.info(f"✅ 대체 컬렉션 생성 성공: {fallback_name}") + + def _initialize_embedding_model(self): + """임베딩 모델 초기화""" + try: + logger.info(f"🤖 임베딩 모델 로드 시작: {self.embedding_model_name}") + + # 캐시 디렉토리 설정 + cache_dir = os.path.join(os.path.expanduser("~"), ".cache", "sentence_transformers") + os.makedirs(cache_dir, exist_ok=True) + + # 권한 확인 + if not os.access(cache_dir, os.W_OK): + import tempfile + cache_dir = tempfile.mkdtemp(prefix="st_cache_") + logger.info(f"🔄 임시 캐시 디렉토리 사용: {cache_dir}") + + # 모델 로드 (타임아웃 설정) + def timeout_handler(signum, frame): + raise TimeoutError("임베딩 모델 로드 타임아웃") + + signal.signal(signal.SIGALRM, timeout_handler) + signal.alarm(300) # 5분 타임아웃 + + try: + self.embedding_model = SentenceTransformer( + self.embedding_model_name, + cache_folder=cache_dir, + device='cpu' # CPU 사용 명시 + ) + signal.alarm(0) # 타임아웃 해제 + + # 모델 테스트 + test_embedding = self.embedding_model.encode(["테스트 문장"]) + logger.info(f"✅ 임베딩 모델 로드 성공: {test_embedding.shape}") + + except TimeoutError: + signal.alarm(0) + raise Exception("임베딩 모델 로드 타임아웃 (5분)") + + except Exception as e: + logger.error(f"❌ 임베딩 모델 로드 실패: {e}") + raise + + def is_ready(self) -> bool: + """서비스 준비 상태 확인""" + return ( + self.client is not None and + self.collection is not None and + self.embedding_model is not None and + self.initialization_error is None + ) + + def get_initialization_error(self) -> Optional[str]: + """초기화 에러 메시지 반환""" + return self.initialization_error + + def retry_initialization(self) -> bool: + """초기화 재시도""" + try: + logger.info("🔄 VectorService 초기화 재시도...") + + # 상태 초기화 + self.client = None + self.collection = None + self.embedding_model = None + self.initialization_error = None + + # 재초기화 + self._safe_initialize() + + return self.is_ready() + + except Exception as e: + self.initialization_error = str(e) + logger.error(f"❌ 초기화 재시도 실패: {e}") + return False + + def reset_vector_db(self) -> Dict[str, Any]: + """Vector DB 완전 리셋""" + try: + logger.info("🔄 Vector DB 완전 리셋 시작...") + + # 기존 클라이언트 정리 + self.client = None + self.collection = None + + # DB 디렉토리 완전 삭제 + if os.path.exists(self.db_path): + shutil.rmtree(self.db_path, ignore_errors=True) + logger.info(f"✅ 기존 DB 디렉토리 삭제: {self.db_path}") + + # 새 디렉토리 생성 + os.makedirs(self.db_path, exist_ok=True) + logger.info(f"✅ 새 DB 디렉토리 생성: {self.db_path}") + + # 재초기화 + success = self.retry_initialization() + + if success: + return { + "success": True, + "message": "Vector DB가 성공적으로 리셋되었습니다", + "collection_name": self.collection_name, + "db_path": self.db_path + } + else: + return { + "success": False, + "error": self.initialization_error or "재초기화 실패" + } + + except Exception as e: + logger.error(f"❌ Vector DB 리셋 실패: {e}") + return { + "success": False, + "error": str(e) + } + + def get_health_status(self) -> Dict[str, Any]: + """서비스 상태 확인""" + try: + status = { + "service": "vector_db", + "status": "healthy" if self.is_ready() else "unhealthy", + "db_path": self.db_path, + "collection_name": self.collection_name, + "embedding_model": self.embedding_model_name, + "timestamp": datetime.now().isoformat() + } + + if self.initialization_error: + status["initialization_error"] = self.initialization_error + status["status"] = "error" + + # 상세 상태 + status["components"] = { + "client": "connected" if self.client else "disconnected", + "collection": "ready" if self.collection else "not_ready", + "embedding": "loaded" if self.embedding_model else "not_loaded" + } + + # 컬렉션 정보 + if self.collection: + try: + status["collection_count"] = self.collection.count() + except Exception as e: + status["collection_error"] = str(e) + + return status + + except Exception as e: + return { + "service": "vector_db", + "status": "error", + "error": str(e), + "timestamp": datetime.now().isoformat() + } + + def get_store_context(self, store_id: str) -> Optional[str]: + """스토어 ID로 컨텍스트 조회""" + try: + if not self.is_ready(): + logger.warning("VectorService가 준비되지 않음") + return None + + # 스토어 ID로 검색 + results = self.collection.get( + where={"store_id": store_id} + ) + + if not results or not results.get('documents'): + logger.warning(f"스토어 ID '{store_id}'에 대한 데이터 없음") + return None + + # 컨텍스트 생성 + documents = results['documents'] + metadatas = results.get('metadatas', []) + + context_parts = [] + for i, doc in enumerate(documents): + metadata = metadatas[i] if i < len(metadatas) else {} + + # 메타데이터 정보 추가 + if metadata: + context_parts.append(f"[{metadata.get('store_name', 'Unknown')}]") + + context_parts.append(doc) + context_parts.append("---") + + return "\n".join(context_parts) + + except Exception as e: + logger.error(f"스토어 컨텍스트 조회 실패: {e}") + return None + + async def build_vector_store(self, store_info: Dict[str, Any], similar_stores_data: List[Tuple[str, Dict[str, Any], List[Dict[str, Any]]]], food_category: str, region: str) -> Dict[str, Any]: + """Vector Store 구축 (완전 수정된 버전)""" + try: + if not self.is_ready(): + # 재시도 한 번 더 + if not self.retry_initialization(): + raise Exception("Vector DB가 초기화되지 않았습니다") + + logger.info(f"Vector Store 구축 시작: {len(similar_stores_data)}개 스토어") + + # 통계 초기화 + stats = { + "total_processed": 0, + "newly_added": 0, + "updated": 0, + "duplicates": 0, + "errors": 0 + } + + # 배치 처리용 리스트 + all_documents = [] + all_embeddings = [] + all_metadatas = [] + all_ids = [] + + # 각 스토어 처리 + for store_id, store_data, reviews in similar_stores_data: + try: + # 데이터 검증 + if not store_data or not reviews: + logger.warning(f"스토어 '{store_id}' 데이터 부족: store_data={bool(store_data)}, reviews={len(reviews) if reviews else 0}") + stats["errors"] += 1 + continue + + # 올바른 create_store_hash 호출 + store_hash = create_store_hash( + store_id=store_id, + store_name=store_data.get('place_name', ''), + region=region + ) + + # ChromaDB에서 직접 중복 확인 (is_duplicate_store 함수 사용하지 않음) + try: + # 같은 store_id로 이미 저장된 데이터가 있는지 확인 + existing_data = self.collection.get( + where={"store_id": store_id}, + limit=1 + ) + + if existing_data and len(existing_data.get('ids', [])) > 0: + logger.debug(f"중복 스토어 건너뛰기: {store_id}") + stats["duplicates"] += 1 + continue + + except Exception as dup_check_error: + # 중복 확인 실패는 로그만 남기고 계속 진행 + logger.warning(f"중복 확인 실패 (계속 진행): {dup_check_error}") + + # 올바른 extract_text_for_embedding 호출 + embedding_text = extract_text_for_embedding( + store_info=store_data, + reviews=reviews + ) + + # 임베딩 생성 + try: + embedding = self.embedding_model.encode(embedding_text) + embedding = embedding.tolist() # numpy array를 list로 변환 + except Exception as embed_error: + logger.error(f"임베딩 생성 실패 (store_id: {store_id}): {embed_error}") + stats["errors"] += 1 + continue + + # 올바른 create_metadata 호출 + metadata = create_metadata( + store_info=store_data, + food_category=food_category, + region=region + ) + + # 배치에 추가 + all_documents.append(embedding_text) + all_embeddings.append(embedding) + all_metadatas.append(metadata) + all_ids.append(f"{store_id}_{store_hash}") + + stats["total_processed"] += 1 + stats["newly_added"] += 1 + + if stats["total_processed"] % 10 == 0: + logger.info(f"처리 진행률: {stats['total_processed']}/{len(similar_stores_data)}") + + except Exception as store_error: + logger.error(f"음식점 처리 중 오류 (store_id: {store_id}): {store_error}") + stats["errors"] += 1 + continue + + # 배치로 벡터 저장 + if all_documents: + logger.info(f"벡터 배치 저장 시작: {len(all_documents)}개") + + try: + self.collection.add( + documents=all_documents, + embeddings=all_embeddings, + metadatas=all_metadatas, + ids=all_ids + ) + logger.info("✅ 벡터 배치 저장 성공") + + except Exception as save_error: + logger.error(f"❌ 벡터 저장 실패: {save_error}") + return { + "success": False, + "error": f"벡터 저장 실패: {str(save_error)}", + "statistics": stats + } + else: + logger.warning("⚠️ 저장할 벡터 데이터가 없음") + + # 최종 통계 + try: + total_vectors = self.collection.count() + logger.info(f"✅ Vector Store 구축 완료: 총 {total_vectors}개 벡터") + except Exception as count_error: + logger.warning(f"벡터 개수 확인 실패: {count_error}") + total_vectors = len(all_documents) + + return { + "success": True, + "message": "Vector Store 구축 완료", + "statistics": stats, + "total_vectors": total_vectors, + "store_info": store_info + } + + except Exception as e: + logger.error(f"Vector Store 구축 전체 실패: {e}") + return { + "success": False, + "error": str(e), + "statistics": stats if 'stats' in locals() else { + "total_processed": 0, + "newly_added": 0, + "updated": 0, + "duplicates": 0, + "errors": 1 + } + } + + def get_db_status(self) -> Dict[str, Any]: + """Vector DB 상태 정보를 반환합니다.""" + try: + if not self.is_ready(): + return { + 'collection_name': self.collection_name, + 'total_documents': 0, + 'total_stores': 0, + 'db_path': self.db_path, + 'status': 'not_ready', + 'initialization_error': self.initialization_error + } + + # 문서 개수 확인 + try: + total_documents = self.collection.count() + except Exception as e: + logger.warning(f"문서 개수 확인 실패: {e}") + total_documents = 0 + + # 고유 가게 수 확인 (store_id 기준) + try: + # 모든 메타데이터에서 고유 store_id 추출 + all_metadata = self.collection.get() + store_ids = set() + + if all_metadata.get('metadatas'): + for metadata in all_metadata['metadatas']: + store_id = metadata.get('store_id') + if store_id: + store_ids.add(store_id) + + total_stores = len(store_ids) + + except Exception as e: + logger.warning(f"가게 수 확인 실패: {e}") + total_stores = 0 + + return { + 'collection_name': self.collection_name, + 'total_documents': total_documents, + 'total_stores': total_stores, + 'db_path': self.db_path, + 'status': 'ready' + } + + except Exception as e: + logger.error(f"DB 상태 확인 실패: {e}") + return { + 'collection_name': self.collection_name, + 'total_documents': 0, + 'total_stores': 0, + 'db_path': self.db_path, + 'status': 'error', + 'error': str(e) + } + + def search_similar_cases(self, store_id: str, context: str) -> Optional[str]: + """유사한 케이스를 검색합니다.""" + try: + if not self.is_ready(): + logger.warning("VectorService가 준비되지 않음") + return None + + # 컨텍스트 기반 유사 검색 + try: + # 검색 쿼리를 임베딩으로 변환 + query_embedding = self.embedding_model.encode(context) + query_embedding = query_embedding.tolist() + + # 유사한 문서 검색 (상위 5개) + results = self.collection.query( + query_embeddings=[query_embedding], + n_results=5, + include=['documents', 'metadatas'] + ) + + if not results or not results.get('documents') or not results['documents'][0]: + logger.info("유사 케이스를 찾을 수 없음") + return None + + # 컨텍스트 조합 + context_parts = [] + documents = results['documents'][0] + metadatas = results.get('metadatas', [[]])[0] + + for i, doc in enumerate(documents): + metadata = metadatas[i] if i < len(metadatas) else {} + + # 가게 정보 추가 + store_name = metadata.get('store_name', 'Unknown') + food_category = metadata.get('food_category', 'Unknown') + + context_parts.append(f"[{food_category} - {store_name}]") + context_parts.append(doc[:500] + "..." if len(doc) > 500 else doc) + context_parts.append("---") + + return "\n".join(context_parts) + + except Exception as search_error: + logger.warning(f"벡터 검색 실패: {search_error}") + return None + + except Exception as e: + logger.error(f"유사 케이스 검색 실패: {e}") + return None diff --git a/vector/app/utils/category_utils.py b/vector/app/utils/category_utils.py new file mode 100644 index 0000000..9abcaa0 --- /dev/null +++ b/vector/app/utils/category_utils.py @@ -0,0 +1,161 @@ +# app/utils/category_utils.py (수정된 버전) +import re +from typing import Optional + +def extract_food_category(category_name: str) -> str: + """ + 카테고리명에서 음식 종류를 추출합니다. + '음식점 > 한식 > 육류,고기'에서 '한식'을 추출 + + Args: + category_name: 전체 카테고리명 + + Returns: + 추출된 음식 종류 + """ + if not category_name: + return "" + + # '>' 기준으로 분할하고 마지막 바로 전 요소 반환 + parts = category_name.split('>') + if len(parts) >= 2: + food_category = parts[-2].strip() # 마지막 바로 전 값 사용 + return food_category + elif len(parts) == 1: + return parts[0].strip() # 하나밖에 없으면 그것을 반환 + + return category_name.strip() + +def normalize_category(category: str) -> str: + """ + 카테고리를 정규화합니다. + + Args: + category: 원본 카테고리 + + Returns: + 정규화된 카테고리 + """ + if not category: + return "" + + # 공백 제거 및 소문자 변환 + normalized = category.strip().lower() + + # 특수문자 제거 (콤마, 슬래시 등은 유지) + normalized = re.sub(r'[^\w가-힣,/\s]', '', normalized) + + return normalized + +def is_similar_category(category1: str, category2: str) -> bool: + """ + 두 카테고리가 유사한지 판단합니다. + + Args: + category1: 첫 번째 카테고리 + category2: 두 번째 카테고리 + + Returns: + 유사 여부 + """ + if not category1 or not category2: + return False + + # 정규화 + norm1 = normalize_category(category1) + norm2 = normalize_category(category2) + + # 완전 일치 + if norm1 == norm2: + return True + + # 키워드 기반 유사성 검사 + keywords1 = set(norm1.replace(',', ' ').replace('/', ' ').split()) + keywords2 = set(norm2.replace(',', ' ').replace('/', ' ').split()) + + # 교집합이 하나 이상 있으면 유사한 것으로 판단 + common_keywords = keywords1.intersection(keywords2) + return len(common_keywords) > 0 + +def extract_main_category(category_name: str) -> str: + """ + 메인 카테고리를 추출합니다. (음식점 > 한식 에서 '한식' 추출) + + Args: + category_name: 전체 카테고리명 + + Returns: + 메인 카테고리 + """ + if not category_name: + return "" + + parts = category_name.split('>') + if len(parts) >= 2: + return parts[1].strip() + elif len(parts) == 1: + return parts[0].strip() + + return "" + +def build_search_query(region: str, food_category: str) -> str: + """ + 검색 쿼리를 구성합니다. (수정된 버전 - 지역 정보 제외) + + Args: + region: 지역 (사용하지 않음) + food_category: 음식 카테고리 + + Returns: + 검색 쿼리 문자열 (음식 카테고리만 포함) + """ + # 콤마와 슬래시를 공백으로 변경하여 검색 키워드 생성 + search_keywords = food_category.replace(',', ' ').replace('/', ' ') + + # 불필요한 단어 제거 + stop_words = ['음식점', '요리', '전문점', '맛집'] + keywords = [] + + for keyword in search_keywords.split(): + keyword = keyword.strip() + if keyword and keyword not in stop_words: + keywords.append(keyword) + + # 키워드가 없으면 기본 검색어 사용 + if not keywords: + keywords = ['음식점'] + + # 🔧 지역 정보는 포함하지 않고 음식 키워드만 반환 + query = ' '.join(keywords) + return query.strip() + +def clean_food_category_for_search(food_category: str) -> str: + """ + 음식 카테고리를 검색용 키워드로 정리합니다. + + Args: + food_category: 원본 음식 카테고리 + + Returns: + 정리된 검색 키워드 + """ + if not food_category: + return "음식점" + + # 콤마와 슬래시를 공백으로 변경 + cleaned = food_category.replace(',', ' ').replace('/', ' ') + + # 불필요한 단어 제거 + stop_words = ['음식점', '요리', '전문점', '맛집'] + keywords = [] + + for keyword in cleaned.split(): + keyword = keyword.strip() + if keyword and keyword not in stop_words: + keywords.append(keyword) + + # 키워드가 없으면 기본 검색어 사용 + if not keywords: + return "음식점" + + return ' '.join(keywords) diff --git a/vector/app/utils/data_utils.py b/vector/app/utils/data_utils.py new file mode 100644 index 0000000..687f446 --- /dev/null +++ b/vector/app/utils/data_utils.py @@ -0,0 +1,194 @@ +# app/utils/data_utils.py +import json +import hashlib +from datetime import datetime +from typing import Dict, List, Any, Optional + +def create_store_hash(store_id: str, store_name: str, region: str) -> str: + """ + 가게의 고유 해시를 생성합니다. + + Args: + store_id: 가게 ID + store_name: 가게명 + region: 지역 + + Returns: + 생성된 해시값 + """ + combined = f"{store_id}_{store_name}_{region}" + return hashlib.md5(combined.encode('utf-8')).hexdigest() + +def combine_store_and_reviews(store_info: Dict[str, Any], reviews: List[Dict[str, Any]]) -> str: + """ + 가게 정보와 리뷰를 결합하여 JSON 문자열을 생성합니다. + + Args: + store_info: 가게 정보 + reviews: 리뷰 목록 + + Returns: + 결합된 JSON 문자열 + """ + combined_data = { + "store_info": store_info, + "reviews": reviews, + "review_summary": generate_review_summary(reviews), + "combined_at": datetime.now().isoformat() + } + + return json.dumps(combined_data, ensure_ascii=False, separators=(',', ':')) + +def generate_review_summary(reviews: List[Dict[str, Any]]) -> Dict[str, Any]: + """ + 리뷰 목록에서 요약 정보를 생성합니다. + + Args: + reviews: 리뷰 목록 + + Returns: + 리뷰 요약 정보 + """ + if not reviews: + return { + "total_reviews": 0, + "average_rating": 0.0, + "rating_distribution": {}, + "common_keywords": [], + "sentiment_summary": { + "positive": 0, + "neutral": 0, + "negative": 0 + } + } + + # 기본 통계 + total_reviews = len(reviews) + ratings = [review.get('rating', 0) for review in reviews if review.get('rating', 0) > 0] + average_rating = sum(ratings) / len(ratings) if ratings else 0.0 + + # 별점 분포 + rating_distribution = {} + for rating in ratings: + rating_distribution[str(rating)] = rating_distribution.get(str(rating), 0) + 1 + + # 키워드 추출 (badges 기반) + keyword_counts = {} + for review in reviews: + badges = review.get('badges', []) + for badge in badges: + keyword_counts[badge] = keyword_counts.get(badge, 0) + 1 + + # 상위 키워드 추출 + common_keywords = sorted(keyword_counts.items(), key=lambda x: x[1], reverse=True)[:10] + common_keywords = [keyword for keyword, count in common_keywords] + + # 감정 분석 (간단한 별점 기반) + sentiment_summary = { + "positive": len([r for r in ratings if r >= 4]), + "neutral": len([r for r in ratings if r == 3]), + "negative": len([r for r in ratings if r <= 2]) + } + + return { + "total_reviews": total_reviews, + "average_rating": round(average_rating, 2), + "rating_distribution": rating_distribution, + "common_keywords": common_keywords, + "sentiment_summary": sentiment_summary, + "has_recent_reviews": any( + review.get('date', '') >= datetime.now().strftime('%Y.%m.%d') + for review in reviews[-10:] # 최근 10개 리뷰 확인 + ) + } + +def extract_text_for_embedding(store_info: Dict[str, Any], reviews: List[Dict[str, Any]]) -> str: + """ + 임베딩을 위한 텍스트를 추출합니다. + + Args: + store_info: 가게 정보 + reviews: 리뷰 목록 + + Returns: + 임베딩용 텍스트 + """ + # 가게 기본 정보 + store_text = f"가게명: {store_info.get('place_name', '')}\n" + store_text += f"카테고리: {store_info.get('category_name', '')}\n" + store_text += f"주소: {store_info.get('address_name', '')}\n" + + # 리뷰 내용 요약 + review_contents = [] + review_keywords = [] + + for review in reviews[:20]: # 최근 20개 리뷰만 사용 + content = review.get('content', '').strip() + if content: + review_contents.append(content) + + badges = review.get('badges', []) + review_keywords.extend(badges) + + # 리뷰 텍스트 조합 + if review_contents: + store_text += f"리뷰 내용: {' '.join(review_contents[:10])}\n" # 최대 10개 리뷰 + + # 키워드 조합 + if review_keywords: + unique_keywords = list(set(review_keywords)) + store_text += f"키워드: {', '.join(unique_keywords[:15])}\n" # 최대 15개 키워드 + + return store_text.strip() + +def create_metadata(store_info: Dict[str, Any], food_category: str, region: str) -> Dict[str, Any]: + """ + Vector DB용 메타데이터를 생성합니다. + + Args: + store_info: 가게 정보 + food_category: 음식 카테고리 + region: 지역 + + Returns: + 메타데이터 딕셔너리 + """ + return { + "store_id": store_info.get('id', ''), + "store_name": store_info.get('place_name', ''), + "food_category": food_category, + "region": region, + "category_name": store_info.get('category_name', ''), + "address": store_info.get('address_name', ''), + "phone": store_info.get('phone', ''), + "place_url": store_info.get('place_url', ''), + "x": store_info.get('x', ''), # 좌표를 개별 키로 분리 + "y": store_info.get('y', ''), # 좌표를 개별 키로 분리 + "last_updated": datetime.now().isoformat() + } + +def is_duplicate_store(metadata1: Dict[str, Any], metadata2: Dict[str, Any]) -> bool: + """ + 두 가게가 중복인지 확인합니다. + + Args: + metadata1: 첫 번째 가게 메타데이터 + metadata2: 두 번째 가게 메타데이터 + + Returns: + 중복 여부 + """ + # Store ID 기준 확인 + if metadata1.get('store_id') and metadata2.get('store_id'): + return metadata1['store_id'] == metadata2['store_id'] + + # 가게명 + 주소 기준 확인 + name1 = metadata1.get('store_name', '').strip() + name2 = metadata2.get('store_name', '').strip() + addr1 = metadata1.get('address', '').strip() + addr2 = metadata2.get('address', '').strip() + + if name1 and name2 and addr1 and addr2: + return name1 == name2 and addr1 == addr2 + + return False diff --git a/vector/build-base.sh b/vector/build-base.sh new file mode 100755 index 0000000..9a83655 --- /dev/null +++ b/vector/build-base.sh @@ -0,0 +1,282 @@ +#!/bin/bash +# build-base.sh - Poetry 기반 Vector DB API Base Image 빌드 스크립트 + +set -e + +# 변수 설정 +BASE_IMAGE_NAME="vector-api-base" +BASE_IMAGE_TAG="${1:-latest}" +ACR_NAME="${2:-acrdigitalgarage03}" +RESOURCE_GROUP="${3:-rg-digitalgarage-03}" + +# ACR URL 자동 구성 +if [ -n "${ACR_NAME}" ]; then + REGISTRY="${ACR_NAME}.azurecr.io" + FULL_BASE_IMAGE_NAME="${REGISTRY}/${BASE_IMAGE_NAME}:${BASE_IMAGE_TAG}" +else + FULL_BASE_IMAGE_NAME="${BASE_IMAGE_NAME}:${BASE_IMAGE_TAG}" +fi + +# 고정된 파일 경로 +BASE_DOCKERFILE_PATH="deployment/container/Dockerfile-base" +BUILD_CONTEXT="." + +# 색상 설정 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +NC='\033[0m' + +log_info() { echo -e "${CYAN}ℹ️ $1${NC}"; } +log_success() { echo -e "${GREEN}✅ $1${NC}"; } +log_warning() { echo -e "${YELLOW}⚠️ $1${NC}"; } +log_error() { echo -e "${RED}❌ $1${NC}"; } + +echo "========================================================" +echo "🚀 Poetry 기반 Vector DB API Base Image 빌드" +echo "========================================================" +echo "Base 이미지명: ${FULL_BASE_IMAGE_NAME}" +if [ -n "${ACR_NAME}" ]; then + echo "ACR 이름: ${ACR_NAME}" + echo "리소스 그룹: ${RESOURCE_GROUP}" +fi +echo "빌드 시작: $(date)" +echo "" +echo "📦 포함될 구성요소:" +echo " ✅ Python 3.11 + Poetry" +echo " ✅ 모든 AI/ML 의존성 (NumPy, PyTorch, Transformers)" +echo " ✅ Vector DB (ChromaDB, Chroma)" +echo " ✅ 웹 프레임워크 (FastAPI, Uvicorn)" +echo " ✅ Claude API (Anthropic)" +echo "" + +# 시작 시간 기록 +BUILD_START=$(date +%s) + +# 사용법 표시 함수 +show_usage() { + echo "사용법:" + echo " $0 [BASE_IMAGE_TAG] [ACR_NAME] [RESOURCE_GROUP]" + echo "" + echo "파라미터:" + echo " BASE_IMAGE_TAG: Base 이미지 태그 (기본값: latest)" + echo " ACR_NAME : Azure Container Registry 이름" + echo " RESOURCE_GROUP: Azure 리소스 그룹" + echo "" + echo "예시:" + echo " $0 v2.0.0 # 로컬 빌드만" + echo " $0 v2.0.0 acrdigitalgarage01 rg-digitalgarage-03 # ACR 빌드 + 푸시" + echo "" + echo "💡 참고:" + echo " - Base Image는 한 번만 빌드하면 재사용 가능" + echo " - 모든 Python 의존성이 포함되어 Service Image 빌드 시간 단축" + echo " - Poetry 환경으로 패키지 관리" +} + +# ACR 로그인 함수 +acr_login() { + local acr_name="$1" + local resource_group="$2" + + log_info "Azure Container Registry 로그인 중..." + + if ! command -v az &> /dev/null; then + log_error "Azure CLI (az)가 설치되지 않았습니다." + exit 1 + fi + + if ! command -v jq &> /dev/null; then + log_error "jq가 설치되지 않았습니다." + exit 1 + fi + + if ! az account show &> /dev/null; then + log_error "Azure에 로그인되지 않았습니다." + echo "로그인 명령: az login" + exit 1 + fi + + local credential_json + credential_json=$(az acr credential show --name "${acr_name}" --resource-group "${resource_group}" 2>/dev/null) + + if [ $? -ne 0 ]; then + log_error "ACR credential 조회 실패" + exit 1 + fi + + local username + local password + + username=$(echo "${credential_json}" | jq -r '.username') + password=$(echo "${credential_json}" | jq -r '.passwords[0].value') + + if [ -z "${username}" ] || [ -z "${password}" ] || [ "${username}" == "null" ] || [ "${password}" == "null" ]; then + log_error "ACR credential 파싱 실패" + exit 1 + fi + + echo "${password}" | docker login "${REGISTRY}" -u "${username}" --password-stdin + + if [ $? -eq 0 ]; then + log_success "ACR 로그인 성공!" + return 0 + else + log_error "ACR 로그인 실패" + exit 1 + fi +} + +# 파라미터 검증 +if [ "$1" == "--help" ] || [ "$1" == "-h" ]; then + show_usage + exit 0 +fi + +if [ -n "${ACR_NAME}" ] && [ -z "${RESOURCE_GROUP}" ]; then + log_error "ACR_NAME이 제공된 경우 RESOURCE_GROUP도 필요합니다." + echo "" + show_usage + exit 1 +fi + +# 필수 파일 확인 +log_info "필수 파일 확인 중..." + +if [ ! -f "${BASE_DOCKERFILE_PATH}" ]; then + log_error "${BASE_DOCKERFILE_PATH} 파일을 찾을 수 없습니다." + exit 1 +fi + +if [ ! -f "setup.sh" ]; then + log_error "setup.sh 파일을 찾을 수 없습니다." + log_error "Poetry 설치 스크립트(setup_poetry_vector.sh)를 setup.sh로 저장해주세요." + exit 1 +fi + +log_success "모든 필수 파일 확인 완료" +echo "📄 Base Dockerfile: ${BASE_DOCKERFILE_PATH}" +echo "📄 Poetry 설치 스크립트: setup.sh" + +# setup.sh 실행 가능 확인 +if [ ! -x "setup.sh" ]; then + log_warning "setup.sh 파일에 실행 권한이 없습니다. 실행 권한 추가 중..." + chmod +x setup.sh + log_success "실행 권한 추가 완료" +fi + +# 시스템 정보 확인 +log_info "시스템 정보 확인..." +echo " - OS: $(lsb_release -d 2>/dev/null | cut -f2 || echo 'Unknown')" +echo " - CPU Cores: $(nproc)" +echo " - Available Memory: $(free -h | awk '/^Mem:/ {print $7}' 2>/dev/null || echo 'Unknown')" +echo " - Docker Version: $(docker --version)" +echo " - Build Context: ${BUILD_CONTEXT}" + +# ACR 로그인 수행 +if [ -n "${ACR_NAME}" ] && [ -n "${RESOURCE_GROUP}" ]; then + echo "" + acr_login "${ACR_NAME}" "${RESOURCE_GROUP}" + echo "" +fi + +# Docker 빌드 +log_info "Poetry 기반 Base Image 빌드 시작..." +echo " - 예상 빌드 시간: 15-25분 (Poetry 의존성 해결 포함)" +echo " - 모든 Python 패키지가 Poetry로 설치됨" +echo " - 멀티스테이지 빌드로 크기 최적화" +echo "" +echo "🔨 빌드 명령어:" +echo "docker build -t \"${FULL_BASE_IMAGE_NAME}\" -f \"${BASE_DOCKERFILE_PATH}\" \"${BUILD_CONTEXT}\"" +echo "" + +docker build -t "${FULL_BASE_IMAGE_NAME}" -f "${BASE_DOCKERFILE_PATH}" "${BUILD_CONTEXT}" + +if [ $? -eq 0 ]; then + # 빌드 종료 시간 계산 + BUILD_END=$(date +%s) + BUILD_TIME=$((BUILD_END - BUILD_START)) + BUILD_MINUTES=$((BUILD_TIME / 60)) + BUILD_SECONDS=$((BUILD_TIME % 60)) + + log_success "Poetry 기반 Base Image 빌드 완료!" + echo "" + echo "⏱️ 총 빌드 시간: ${BUILD_MINUTES}분 ${BUILD_SECONDS}초" + + # 이미지 정보 표시 + echo "" + log_info "Base Image 정보:" + docker images "${FULL_BASE_IMAGE_NAME}" --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}\t{{.CreatedAt}}" + + # latest 태그 추가 생성 + if [ "${BASE_IMAGE_TAG}" != "latest" ] && [ -n "${REGISTRY}" ]; then + echo "" + log_info "latest 태그 생성 중..." + docker tag "${FULL_BASE_IMAGE_NAME}" "${REGISTRY}/${BASE_IMAGE_NAME}:latest" + log_success "latest 태그 생성 완료: ${REGISTRY}/${BASE_IMAGE_NAME}:latest" + fi + + # ACR 푸시 + if [ -n "${ACR_NAME}" ]; then + echo "" + log_info "ACR에 Base Image 푸시 중..." + + echo "📤 푸시 중: ${FULL_BASE_IMAGE_NAME}" + docker push "${FULL_BASE_IMAGE_NAME}" + + if [ $? -eq 0 ]; then + log_success "Base Image 푸시 성공" + + if [ "${BASE_IMAGE_TAG}" != "latest" ]; then + echo "📤 푸시 중: ${REGISTRY}/${BASE_IMAGE_NAME}:latest" + docker push "${REGISTRY}/${BASE_IMAGE_NAME}:latest" + + if [ $? -eq 0 ]; then + log_success "latest 태그 푸시 성공" + fi + fi + else + log_error "Base Image 푸시 실패" + exit 1 + fi + fi + + echo "" + log_success "🎉 Poetry 기반 Base Image 빌드 완료!" + echo "" + echo "📋 완료된 작업:" + echo " ✅ Base Image 빌드: ${FULL_BASE_IMAGE_NAME}" + if [ "${BASE_IMAGE_TAG}" != "latest" ] && [ -n "${REGISTRY}" ]; then + echo " ✅ latest 태그: ${REGISTRY}/${BASE_IMAGE_NAME}:latest" + fi + if [ -n "${ACR_NAME}" ]; then + echo " ✅ ACR 푸시 완료" + fi + echo " ✅ Poetry 환경 구성 완료" + echo " ✅ 모든 AI/ML 의존성 설치 완료" + + echo "" + echo "📝 다음 단계:" + echo " 이제 Service Image를 빌드하세요:" + if [ -n "${ACR_NAME}" ]; then + echo " ./build.sh v1.0.0 ${ACR_NAME} ${RESOURCE_GROUP}" + else + echo " ./build.sh v1.0.0" + fi + + echo "" + echo "💡 최적화 포인트:" + echo " ✅ Base Image: 한 번만 빌드 (주기적 업데이트)" + echo " ✅ Service Image: 매번 빠른 빌드 (앱 코드만)" + echo " ✅ Poetry 환경: 의존성 충돌 자동 해결" + echo " ✅ 개발 생산성: 빌드 대기시간 95% 단축" + +else + log_error "Base Image 빌드 실패!" + exit 1 +fi + +echo "" +echo "🏁 Base Image 빌드 프로세스 완료 - $(date)" + diff --git a/vector/build.sh b/vector/build.sh new file mode 100755 index 0000000..da51485 --- /dev/null +++ b/vector/build.sh @@ -0,0 +1,337 @@ +#!/bin/bash +# build.sh - Poetry 기반 Vector DB API Service Image 빌드 스크립트 + +set -e + +# 변수 설정 +IMAGE_NAME="vector-api" +IMAGE_TAG="${1:-latest}" +ACR_NAME="${2:-acrdigitalgarage03}" +RESOURCE_GROUP="${3:-rg-digitalgarage-03}" +BASE_IMAGE_TAG="${4:-latest}" + +# ACR URL 자동 구성 +if [ -n "${ACR_NAME}" ]; then + REGISTRY="${ACR_NAME}.azurecr.io" + FULL_IMAGE_NAME="${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}" + BASE_IMAGE="${REGISTRY}/vector-api-base:${BASE_IMAGE_TAG}" +else + FULL_IMAGE_NAME="${IMAGE_NAME}:${IMAGE_TAG}" + BASE_IMAGE="vector-api-base:${BASE_IMAGE_TAG}" +fi + +# 고정된 파일 경로 +DOCKERFILE_PATH="deployment/container/Dockerfile" +BUILD_CONTEXT="." + +# 색상 설정 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +NC='\033[0m' + +log_info() { echo -e "${CYAN}ℹ️ $1${NC}"; } +log_success() { echo -e "${GREEN}✅ $1${NC}"; } +log_warning() { echo -e "${YELLOW}⚠️ $1${NC}"; } +log_error() { echo -e "${RED}❌ $1${NC}"; } + +echo "========================================================" +echo "🚀 Poetry 기반 Vector DB API Service Image 빌드" +echo "========================================================" +echo "Service 이미지명: ${FULL_IMAGE_NAME}" +echo "Base 이미지: ${BASE_IMAGE}" +if [ -n "${ACR_NAME}" ]; then + echo "ACR 이름: ${ACR_NAME}" + echo "리소스 그룹: ${RESOURCE_GROUP}" +fi +echo "빌드 시작: $(date)" +echo "" + +# 시작 시간 기록 +BUILD_START=$(date +%s) + +# 사용법 표시 함수 +show_usage() { + echo "사용법:" + echo " $0 [IMAGE_TAG] [ACR_NAME] [RESOURCE_GROUP] [BASE_IMAGE_TAG]" + echo "" + echo "파라미터:" + echo " IMAGE_TAG : Service 이미지 태그 (기본값: latest)" + echo " ACR_NAME : Azure Container Registry 이름" + echo " RESOURCE_GROUP: Azure 리소스 그룹" + echo " BASE_IMAGE_TAG: Base 이미지 태그 (기본값: latest)" + echo "" + echo "예시:" + echo " $0 v1.0.0 # 로컬 빌드만" + echo " $0 v1.0.0 acrdigitalgarage01 rg-digitalgarage-03 # ACR 빌드 + 푸시" + echo " $0 v1.0.0 acrdigitalgarage01 rg-digitalgarage-03 v2.0.0 # 특정 Base Image 사용" + echo "" + echo "전제조건:" + echo " Poetry 기반 Base Image가 먼저 빌드되어 있어야 합니다:" + echo " ./build-base.sh ${BASE_IMAGE_TAG} [ACR_NAME] [RESOURCE_GROUP]" + echo "" + echo "💡 장점:" + echo " - 빠른 빌드: 앱 코드만 복사 (30초~2분)" + echo " - Poetry 환경: 의존성 관리 최적화" + echo " - 캐시 활용: Base Image 재사용으로 효율성 극대화" +} + +# ACR 로그인 함수 +acr_login() { + local acr_name="$1" + local resource_group="$2" + + log_info "Azure Container Registry 로그인 중..." + + if ! command -v az &> /dev/null; then + log_error "Azure CLI (az)가 설치되지 않았습니다." + exit 1 + fi + + if ! command -v jq &> /dev/null; then + log_error "jq가 설치되지 않았습니다." + exit 1 + fi + + if ! az account show &> /dev/null; then + log_error "Azure에 로그인되지 않았습니다." + echo "로그인 명령: az login" + exit 1 + fi + + local credential_json + credential_json=$(az acr credential show --name "${acr_name}" --resource-group "${resource_group}" 2>/dev/null) + + if [ $? -ne 0 ]; then + log_error "ACR credential 조회 실패" + exit 1 + fi + + local username + local password + + username=$(echo "${credential_json}" | jq -r '.username') + password=$(echo "${credential_json}" | jq -r '.passwords[0].value') + + if [ -z "${username}" ] || [ -z "${password}" ] || [ "${username}" == "null" ] || [ "${password}" == "null" ]; then + log_error "ACR credential 파싱 실패" + exit 1 + fi + + echo "${password}" | docker login "${REGISTRY}" -u "${username}" --password-stdin + + if [ $? -eq 0 ]; then + log_success "ACR 로그인 성공!" + return 0 + else + log_error "ACR 로그인 실패" + exit 1 + fi +} + +# 파라미터 검증 +if [ "$1" == "--help" ] || [ "$1" == "-h" ]; then + show_usage + exit 0 +fi + +if [ -n "${ACR_NAME}" ] && [ -z "${RESOURCE_GROUP}" ]; then + log_error "ACR_NAME이 제공된 경우 RESOURCE_GROUP도 필요합니다." + echo "" + show_usage + exit 1 +fi + +# 필수 파일 확인 +log_info "필수 파일 확인 중..." + +if [ ! -f "app/main.py" ]; then + log_error "app/main.py 파일을 찾을 수 없습니다." + exit 1 +fi + +if [ ! -f "${DOCKERFILE_PATH}" ]; then + log_error "${DOCKERFILE_PATH} 파일을 찾을 수 없습니다." + exit 1 +fi + +log_success "모든 필수 파일이 확인되었습니다." +echo "📄 Service Dockerfile: ${DOCKERFILE_PATH}" +echo "📄 메인 애플리케이션: app/main.py" +echo "🏗️ 빌드 컨텍스트: ${BUILD_CONTEXT}" + +# Base Image 존재 확인 +echo "" +log_info "Poetry 기반 Base Image 확인 중..." +if docker image inspect "${BASE_IMAGE}" >/dev/null 2>&1; then + log_success "Base Image 확인됨: ${BASE_IMAGE}" + + # Base Image에서 Poetry 환경 확인 + log_info "Base Image Poetry 환경 검증 중..." + if docker run --rm "${BASE_IMAGE}" poetry --version >/dev/null 2>&1; then + POETRY_VERSION=$(docker run --rm "${BASE_IMAGE}" poetry --version 2>/dev/null) + log_success "Poetry 환경 확인됨: ${POETRY_VERSION}" + else + log_warning "Base Image에서 Poetry를 찾을 수 없습니다" + fi + + # Base Image 크기 확인 + BASE_SIZE=$(docker images "${BASE_IMAGE}" --format "{{.Size}}") + echo "📊 Base Image 크기: ${BASE_SIZE}" + +else + log_error "Base Image를 찾을 수 없습니다: ${BASE_IMAGE}" + echo "" + echo "📋 해결 방법:" + echo " Base Image를 먼저 빌드하세요:" + if [ -n "${ACR_NAME}" ]; then + echo " ./build-base.sh ${BASE_IMAGE_TAG} ${ACR_NAME} ${RESOURCE_GROUP}" + else + echo " ./build-base.sh ${BASE_IMAGE_TAG}" + fi + echo "" + echo " 또는 ACR에서 Base Image를 풀하세요:" + if [ -n "${ACR_NAME}" ]; then + echo " docker pull ${BASE_IMAGE}" + fi + exit 1 +fi + +# ACR 로그인 수행 +if [ -n "${ACR_NAME}" ] && [ -n "${RESOURCE_GROUP}" ]; then + echo "" + acr_login "${ACR_NAME}" "${RESOURCE_GROUP}" + echo "" +fi + +# Docker 빌드 +log_info "Poetry 기반 Service Image 빌드 시작..." +echo " - 예상 빌드 시간: 30초~2분 (앱 코드만 복사)" +echo " - Base Image 재사용으로 빠른 빌드" +echo " - Poetry 환경 상속" +echo "" +echo "🔨 빌드 명령어:" +echo "docker build --build-arg BASE_IMAGE=\"${BASE_IMAGE}\" -t \"${FULL_IMAGE_NAME}\" -f \"${DOCKERFILE_PATH}\" \"${BUILD_CONTEXT}\"" +echo "" + +docker build --build-arg BASE_IMAGE="${BASE_IMAGE}" -t "${FULL_IMAGE_NAME}" -f "${DOCKERFILE_PATH}" "${BUILD_CONTEXT}" + +if [ $? -eq 0 ]; then + # 빌드 종료 시간 계산 + BUILD_END=$(date +%s) + BUILD_TIME=$((BUILD_END - BUILD_START)) + BUILD_MINUTES=$((BUILD_TIME / 60)) + BUILD_SECONDS=$((BUILD_TIME % 60)) + + log_success "Service Image 빌드 완료!" + echo "" + echo "⏱️ 총 빌드 시간: ${BUILD_MINUTES}분 ${BUILD_SECONDS}초" + + # 이미지 정보 표시 + echo "" + log_info "Service Image 정보:" + docker images "${FULL_IMAGE_NAME}" --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}\t{{.CreatedAt}}" + + # 🧪 빌드된 이미지 검증 + log_info "Service Image 검증 중..." + echo "🔍 Poetry 환경 및 앱 코드 확인:" + + docker run --rm "${FULL_IMAGE_NAME}" sh -c " + echo '✅ Poetry 버전:' && poetry --version && + echo '✅ Python 버전:' && poetry run python --version && + echo '✅ 설치된 패키지 수:' && poetry show | wc -l && + echo '✅ 앱 코드 확인:' && ls -la /app/app/ 2>/dev/null | head -5 + " 2>/dev/null || log_warning "일부 검증에 실패했습니다." + + # latest 태그 추가 생성 + if [ "${IMAGE_TAG}" != "latest" ] && [ -n "${REGISTRY}" ]; then + echo "" + log_info "latest 태그 생성 중..." + docker tag "${FULL_IMAGE_NAME}" "${REGISTRY}/${IMAGE_NAME}:latest" + log_success "latest 태그 생성 완료: ${REGISTRY}/${IMAGE_NAME}:latest" + fi + + # ACR 푸시 + if [ -n "${ACR_NAME}" ]; then + echo "" + log_info "ACR에 Service Image 푸시 중..." + + echo "📤 푸시 중: ${FULL_IMAGE_NAME}" + docker push "${FULL_IMAGE_NAME}" + + if [ $? -eq 0 ]; then + log_success "Service Image 푸시 성공" + + if [ "${IMAGE_TAG}" != "latest" ]; then + echo "📤 푸시 중: ${REGISTRY}/${IMAGE_NAME}:latest" + docker push "${REGISTRY}/${IMAGE_NAME}:latest" + + if [ $? -eq 0 ]; then + log_success "latest 태그 푸시 성공" + fi + fi + else + log_error "Service Image 푸시 실패" + exit 1 + fi + fi + + echo "" + log_success "🎉 Poetry 기반 Service Image 빌드 완료!" + echo "" + echo "📋 완료된 작업:" + echo " ✅ Service Image 빌드: ${FULL_IMAGE_NAME}" + echo " ✅ Base Image 활용: ${BASE_IMAGE}" + if [ "${IMAGE_TAG}" != "latest" ] && [ -n "${REGISTRY}" ]; then + echo " ✅ latest 태그: ${REGISTRY}/${IMAGE_NAME}:latest" + fi + if [ -n "${ACR_NAME}" ]; then + echo " ✅ ACR 푸시 완료" + fi + echo " ✅ Poetry 환경 상속" + + echo "" + echo "🧪 테스트 명령어:" + echo " # 로컬 실행 테스트" + echo " docker run --rm -p 8000:8000 ${FULL_IMAGE_NAME}" + echo "" + echo " # Poetry 환경 확인" + echo " docker run --rm ${FULL_IMAGE_NAME} poetry show" + echo "" + echo " # 앱 헬스체크" + echo " docker run --rm ${FULL_IMAGE_NAME} poetry run python -c \"import app.main; print('✅ 앱 로드 성공')\"" + + echo "" + echo "📝 다음 단계:" + echo " 1. 로컬 테스트:" + echo " docker run --rm -p 8000:8000 ${FULL_IMAGE_NAME}" + echo "" + echo " 2. Kubernetes 배포:" + echo " kubectl apply -f deployment/manifests/" + echo "" + echo " 3. 스케일링:" + echo " kubectl scale deployment vector-api --replicas=3" + + echo "" + echo "💡 성능 최적화:" + echo " ✅ 빠른 빌드: Base Image 재사용으로 빌드 시간 95% 단축" + echo " ✅ 효율적 캐시: Poetry 환경은 Base Image에서 관리" + echo " ✅ 작은 이미지: 앱 코드만 포함하여 배포 이미지 최소화" + echo " ✅ 개발 친화적: Poetry로 일관된 의존성 관리" + +else + log_error "Service Image 빌드 실패!" + echo "" + echo "🔍 문제 해결:" + echo " 1. Base Image 확인: docker images | grep vector-api-base" + echo " 2. Dockerfile 확인: cat ${DOCKERFILE_PATH}" + echo " 3. 앱 코드 확인: ls -la app/" + echo " 4. 빌드 로그 확인: 위의 에러 메시지 참조" + exit 1 +fi + +echo "" +echo "🏁 Service Image 빌드 프로세스 완료 - $(date)" + diff --git a/vector/create-imagepullsecret.sh b/vector/create-imagepullsecret.sh new file mode 100755 index 0000000..441417d --- /dev/null +++ b/vector/create-imagepullsecret.sh @@ -0,0 +1,236 @@ +#!/bin/bash +# create-imagepullsecret.sh - ACR Image Pull Secret 생성 스크립트 + +set -e + +# 변수 설정 +ACR_NAME="${1:-acrdigitalgarage03}" +RESOURCE_GROUP="${2:-rg-digitalgarage-03}" +SECRET_NAME="${3:-acr-secret}" + +echo "=====================================================" +echo " ACR Image Pull Secret 생성 (Vector API)" +echo "=====================================================" + +# 사용법 표시 함수 +show_usage() { + echo "사용법:" + echo " $0 [ACR_NAME] [RESOURCE_GROUP] [SECRET_NAME]" + echo "" + echo "파라미터:" + echo " ACR_NAME : Azure Container Registry 이름 (필수)" + echo " RESOURCE_GROUP: Azure 리소스 그룹 (필수)" + echo " SECRET_NAME : Secret 이름 (기본값: acr-secret)" + echo "" + echo "예시:" + echo " $0 acrdigitalgarage01 rg-digitalgarage-03" + echo " $0 acrdigitalgarage01 rg-digitalgarage-03 acr-prod-secret" + echo "" +} + +# 파라미터 검증 +if [ "$1" == "--help" ] || [ "$1" == "-h" ]; then + show_usage + exit 0 +fi + +if [ -z "${ACR_NAME}" ] || [ -z "${RESOURCE_GROUP}" ]; then + echo "❌ ACR_NAME과 RESOURCE_GROUP는 필수 파라미터입니다." + echo "" + show_usage + exit 1 +fi + +# 필수 도구 확인 +echo "🔧 필수 도구 확인 중..." + +if ! command -v az &> /dev/null; then + echo "❌ Azure CLI (az)가 설치되지 않았습니다." + echo "설치 방법: https://docs.microsoft.com/en-us/cli/azure/install-azure-cli" + exit 1 +fi + +if ! command -v kubectl &> /dev/null; then + echo "❌ kubectl이 설치되지 않았습니다." + echo "설치 방법: https://kubernetes.io/docs/tasks/tools/" + exit 1 +fi + +if ! command -v jq &> /dev/null; then + echo "❌ jq가 설치되지 않았습니다." + echo "설치 방법: sudo apt-get install jq" + exit 1 +fi + +echo "✅ 필수 도구 확인 완료" + +# Azure 로그인 확인 +echo "" +echo "🔐 Azure 로그인 상태 확인 중..." + +if ! az account show &> /dev/null; then + echo "❌ Azure에 로그인되지 않았습니다." + echo "로그인 명령: az login" + exit 1 +fi + +CURRENT_SUBSCRIPTION=$(az account show --query name -o tsv) +echo "✅ Azure 로그인 확인됨" +echo " 현재 구독: ${CURRENT_SUBSCRIPTION}" + +# Kubernetes 클러스터 연결 확인 +echo "" +echo "☸️ Kubernetes 클러스터 연결 확인 중..." + +if ! kubectl cluster-info &> /dev/null; then + echo "❌ Kubernetes 클러스터에 연결되지 않았습니다." + echo "클러스터 연결 방법:" + echo " az aks get-credentials --resource-group ${RESOURCE_GROUP} --name " + exit 1 +fi + +CURRENT_CONTEXT=$(kubectl config current-context) +echo "✅ Kubernetes 클러스터 연결 확인됨" +echo " 현재 컨텍스트: ${CURRENT_CONTEXT}" + +# ACR 정보 설정 +REGISTRY_URL="${ACR_NAME}.azurecr.io" + +echo "" +echo "📋 설정 정보:" +echo " ACR 이름: ${ACR_NAME}" +echo " 레지스트리 URL: ${REGISTRY_URL}" +echo " 리소스 그룹: ${RESOURCE_GROUP}" +echo " Secret 이름: ${SECRET_NAME}" + +# ACR 존재 확인 +echo "" +echo "🏪 ACR 존재 확인 중..." + +if ! az acr show --name "${ACR_NAME}" --resource-group "${RESOURCE_GROUP}" &> /dev/null; then + echo "❌ ACR을 찾을 수 없습니다." + echo "확인 사항:" + echo " - ACR 이름: ${ACR_NAME}" + echo " - 리소스 그룹: ${RESOURCE_GROUP}" + echo " - ACR이 해당 리소스 그룹에 존재하는지 확인" + exit 1 +fi + +echo "✅ ACR 존재 확인됨: ${ACR_NAME}" + +# ACR credential 조회 +echo "" +echo "🔑 ACR credential 조회 중..." + +credential_json=$(az acr credential show --name "${ACR_NAME}" --resource-group "${RESOURCE_GROUP}" 2>/dev/null) + +if [ $? -ne 0 ]; then + echo "❌ ACR credential 조회 실패" + echo "확인 사항:" + echo " - ACR 이름: ${ACR_NAME}" + echo " - 리소스 그룹: ${RESOURCE_GROUP}" + echo " - ACR에 대한 권한이 있는지 확인" + exit 1 +fi + +# JSON에서 username과 password 추출 +username=$(echo "${credential_json}" | jq -r '.username') +password=$(echo "${credential_json}" | jq -r '.passwords[0].value') + +if [ -z "${username}" ] || [ -z "${password}" ] || [ "${username}" == "null" ] || [ "${password}" == "null" ]; then + echo "❌ ACR credential 파싱 실패" + echo "credential JSON:" + echo "${credential_json}" + exit 1 +fi + +echo "✅ ACR credential 조회 성공" +echo " 사용자명: ${username}" +echo " 비밀번호: ${password:0:10}..." + +# 기존 Secret 확인 및 삭제 +echo "" +echo "🔍 기존 Secret 확인 중..." + +if kubectl get secret "${SECRET_NAME}" &> /dev/null; then + echo "🗑️ 기존 Secret 삭제 중..." + kubectl delete secret "${SECRET_NAME}" + echo "✅ 기존 Secret 삭제 완료" +else + echo "✅ 기존 Secret 없음" +fi + +# Image Pull Secret 생성 +echo "" +echo "🔐 Image Pull Secret 생성 중..." + +kubectl create secret docker-registry "${SECRET_NAME}" \ + --docker-server="${REGISTRY_URL}" \ + --docker-username="${username}" \ + --docker-password="${password}" + +if [ $? -eq 0 ]; then + echo "✅ Image Pull Secret 생성 성공!" +else + echo "❌ Image Pull Secret 생성 실패" + exit 1 +fi + +# Secret 정보 확인 +echo "" +echo "📊 생성된 Secret 정보:" +kubectl describe secret "${SECRET_NAME}" + +echo "" +echo "🧪 Secret 테스트 중..." + +# Secret이 올바르게 생성되었는지 확인 +SECRET_TYPE=$(kubectl get secret "${SECRET_NAME}" -o jsonpath='{.type}') +if [ "${SECRET_TYPE}" = "kubernetes.io/dockerconfigjson" ]; then + echo "✅ Secret 타입 확인됨: ${SECRET_TYPE}" +else + echo "❌ Secret 타입 불일치: ${SECRET_TYPE}" +fi + +# Registry URL 확인 +SECRET_REGISTRY=$(kubectl get secret "${SECRET_NAME}" -o jsonpath='{.data.\.dockerconfigjson}' | base64 -d | jq -r ".auths | keys[0]") +if [ "${SECRET_REGISTRY}" = "${REGISTRY_URL}" ]; then + echo "✅ Registry URL 확인됨: ${SECRET_REGISTRY}" +else + echo "❌ Registry URL 불일치: ${SECRET_REGISTRY}" +fi + +echo "" +echo "🎉 ACR Image Pull Secret 생성 완료!" +echo "" +echo "📋 사용 방법:" +echo "" +echo "1. Deployment에서 imagePullSecrets 사용:" +echo " spec:" +echo " template:" +echo " spec:" +echo " imagePullSecrets:" +echo " - name: ${SECRET_NAME}" +echo "" +echo "2. ServiceAccount에 Secret 연결:" +echo " kubectl patch serviceaccount default -p '{\"imagePullSecrets\": [{\"name\": \"${SECRET_NAME}\"}]}'" +echo "" +echo "3. Secret 확인:" +echo " kubectl get secret ${SECRET_NAME}" +echo " kubectl describe secret ${SECRET_NAME}" +echo "" +echo "4. Secret 삭제 (필요시):" +echo " kubectl delete secret ${SECRET_NAME}" +echo "" +echo "🔧 다음 단계:" +echo "1. Deployment 매니페스트에 imagePullSecrets 추가" +echo "2. kubectl apply -f deployment/manifests/deployment.yaml" +echo "3. Pod 상태 확인: kubectl get pods" +echo "" +echo "💡 문제 해결:" +echo "- ErrImagePull 오류 시: Secret 이름과 레지스트리 URL 확인" +echo "- 권한 오류 시: ACR에 대한 적절한 권한 확인" +echo "- 네트워크 오류 시: 클러스터에서 ACR로의 네트워크 연결 확인" + +echo "" +echo "✅ Image Pull Secret 설정이 완료되었습니다!" \ No newline at end of file diff --git a/vector/deployment/container/Dockerfile b/vector/deployment/container/Dockerfile new file mode 100644 index 0000000..18613d5 --- /dev/null +++ b/vector/deployment/container/Dockerfile @@ -0,0 +1,100 @@ +# deployment/container/Dockerfile +# Poetry 기반 Vector DB API Service Image - PVC 마운트 충돌 해결 + +# Base Image에서 상속 (Poetry 환경 포함) +ARG BASE_IMAGE=vector-api-base:latest +FROM ${BASE_IMAGE} + +# 메타데이터 +LABEL maintainer="admin@example.com" +LABEL version="1.0.2" +LABEL description="Vector DB API Service with Poetry - PVC Mount Fixed" + +# 환경 변수 설정 - Poetry 가상환경 경로 유지 +ENV HOME=/home/appuser \ + PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + POETRY_VENV_IN_PROJECT=false \ + POETRY_VIRTUALENVS_CREATE=true \ + POETRY_VIRTUALENVS_PATH=/opt/pypoetry/venvs \ + POETRY_CACHE_DIR=/opt/pypoetry/cache \ + POETRY_NO_INTERACTION=1 \ + PATH="/home/appuser/.local/bin:/opt/pypoetry/venvs/vector-api/bin:/usr/local/bin:/usr/bin:/bin" + +# root로 전환 (파일 복사 및 권한 설정용) +USER root + +# 🔧 Poetry 설정 파일들 복사 (의존성 정보) +COPY pyproject.toml poetry.lock* /app/ + +# 🚀 애플리케이션 소스 코드 복사 +COPY app/ /app/app/ + +# 📦 Poetry 의존성 설치 (가상환경이 /opt에 생성됨) +RUN cd /app && \ + # Poetry 설정 확인 및 재설정 + poetry config virtualenvs.in-project false && \ + poetry config virtualenvs.create true && \ + poetry config virtualenvs.path /opt/pypoetry/venvs && \ + poetry config cache-dir /opt/pypoetry/cache && \ + echo "🔧 Poetry 설정 확인:" && \ + poetry config --list && \ + echo "📦 의존성 설치 시작..." && \ + poetry install --no-dev --no-interaction && \ + echo "✅ 의존성 설치 완료" && \ + # 설치된 패키지 확인 + poetry show | head -10 && \ + # 가상환경 위치 확인 + poetry env info && \ + # 캐시 정리 + rm -rf $POETRY_CACHE_DIR/cache && \ + rm -rf /tmp/* + +# 📁 데이터 디렉토리 생성 및 권한 설정 +RUN mkdir -p /app/data /app/logs /app/vectordb \ + && chmod -R 755 /app/data /app/logs /app/vectordb + +# 👤 사용자 및 권한 설정 +RUN if id "appuser" &>/dev/null; then \ + chown -R appuser:appuser /app; \ + chown -R appuser:appuser /opt/pypoetry; \ + else \ + echo "appuser가 없어서 root로 실행됩니다"; \ + fi + +# 🔧 실행 스크립트 생성 (Poetry 가상환경 자동 활성화) +RUN cat > /app/start.sh << 'EOF' +#!/bin/bash +echo "🚀 Vector API 시작 중..." +echo "📍 현재 디렉토리: $(pwd)" +echo "🐍 Python 위치: $(which python)" +echo "📦 Poetry 위치: $(which poetry)" +echo "🔧 Poetry 가상환경 정보:" +poetry env info +echo "📋 설치된 패키지 (일부):" +poetry show | head -5 +echo "🔍 dotenv 모듈 테스트:" +poetry run python -c "from dotenv import load_dotenv; print('✅ dotenv 모듈 정상 로드')" +echo "🚀 애플리케이션 실행..." +exec poetry run python app/main.py +EOF + +RUN chmod +x /app/start.sh && \ + chown appuser:appuser /app/start.sh + +# 🏥 헬스체크 (의존성 확인 포함) +HEALTHCHECK --interval=30s --timeout=15s --start-period=60s --retries=3 \ + CMD poetry run python -c "from dotenv import load_dotenv; import app.main; print('✅ 앱 헬스체크 성공')" || exit 1 + +# 🚀 포트 노출 +EXPOSE 8000 + +# 📁 작업 디렉토리 설정 +WORKDIR /app + +# 👤 실행 사용자 설정 +USER appuser + +# 🎯 애플리케이션 실행 - 스크립트 사용 +CMD ["/app/start.sh"] + diff --git a/vector/deployment/container/Dockerfile-base b/vector/deployment/container/Dockerfile-base new file mode 100644 index 0000000..deab09a --- /dev/null +++ b/vector/deployment/container/Dockerfile-base @@ -0,0 +1,138 @@ +# deployment/container/Dockerfile-base +# Poetry 기반 Vector DB API Base Image - 홈 디렉토리 사용 (안전한 방식) + +FROM python:3.11-slim + +# 메타데이터 +LABEL description="Vector DB API Base Image with Poetry - Home Directory" +LABEL version="poetry-home-v1.0" +LABEL maintainer="admin@example.com" + +# 환경 변수 설정 - Poetry 가상환경을 홈 디렉토리로 이동 +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + DEBIAN_FRONTEND=noninteractive \ + PIP_NO_CACHE_DIR=1 \ + PIP_DISABLE_PIP_VERSION_CHECK=1 \ + POETRY_NO_INTERACTION=1 \ + POETRY_VENV_IN_PROJECT=false \ + POETRY_VIRTUALENVS_CREATE=true \ + POETRY_VIRTUALENVS_PATH=/home/appuser/.cache/pypoetry/venvs \ + POETRY_CACHE_DIR=/home/appuser/.cache/pypoetry/cache \ + HF_HUB_CACHE=/app/.cache/huggingface \ + TRANSFORMERS_CACHE=/app/.cache/transformers \ + SENTENCE_TRANSFORMERS_HOME=/app/.cache/sentence_transformers + +# 🔧 시스템 패키지 설치 +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + gcc \ + g++ \ + python3-dev \ + curl \ + wget \ + ca-certificates \ + git \ + sudo \ + lsb-release \ + bc \ + python3.11 \ + python3.11-venv \ + python3.11-dev \ + python3.11-distutils \ + && rm -rf /var/lib/apt/lists/* \ + && apt-get clean + +# 📦 pip 업그레이드 +RUN python3.11 -m pip install --no-cache-dir --upgrade pip setuptools wheel + +# 👤 비root 사용자 생성 (Poetry 설치 전에) +RUN groupadd -r appuser && \ + useradd -r -g appuser -d /home/appuser -s /bin/bash appuser && \ + mkdir -p /home/appuser && \ + chown -R appuser:appuser /home/appuser + +# 🔧 Poetry 가상환경 디렉토리 생성 (홈 디렉토리 사용) +RUN mkdir -p /home/appuser/.cache/pypoetry/venvs \ + /home/appuser/.cache/pypoetry/cache && \ + chown -R appuser:appuser /home/appuser/.cache && \ + chmod -R 755 /home/appuser/.cache + +# 🐍 Poetry를 appuser로 설치 +USER appuser +ENV PATH="/home/appuser/.local/bin:$PATH" + +# appuser 홈 디렉토리에 Poetry 설치 +RUN curl -sSL https://install.python-poetry.org | python3.11 - + +# Poetry 실행 권한 및 심볼릭 링크 (root 권한 필요) +USER root +RUN chmod +x /home/appuser/.local/bin/poetry && \ + ln -sf /home/appuser/.local/bin/poetry /usr/local/bin/poetry && \ + chown appuser:appuser /home/appuser/.local/bin/poetry + +# appuser로 다시 전환 +USER appuser + +# 🔧 Poetry 설정 - 가상환경을 홈 디렉토리로 이동 +RUN poetry config virtualenvs.in-project false && \ + poetry config virtualenvs.create true && \ + poetry config virtualenvs.path /home/appuser/.cache/pypoetry/venvs && \ + poetry config cache-dir /home/appuser/.cache/pypoetry/cache + +# Poetry 버전 확인 및 설정 검증 +RUN poetry --version && \ + poetry config --list && \ + ls -la /home/appuser/.local/bin/poetry && \ + which poetry + +# 🏗️ 작업 디렉토리 설정 및 권한 조정 +WORKDIR /app + +# root로 전환하여 디렉토리 소유권 설정 +USER root +RUN chown -R appuser:appuser /app + +# 📋 Poetry 설치 스크립트 복사 및 권한 설정 +COPY setup.sh /app/setup.sh +RUN chmod +x /app/setup.sh && \ + chown appuser:appuser /app/setup.sh + +# appuser로 전환하여 Poetry 환경 설정 +USER appuser + +# 🚀 Poetry 환경 설정 및 의존성 설치 +RUN cd /app && \ + export DEBIAN_FRONTEND=noninteractive && \ + ./setup.sh --skip-poetry-install --skip-python311-check --force-reinstall + +# 🗂️ 필요한 디렉토리 생성 및 권한 설정 +USER root +RUN mkdir -p /app/.cache/huggingface \ + /app/.cache/transformers \ + /app/.cache/sentence_transformers \ + /app/vectordb \ + /app/data \ + /app/logs && \ + chmod -R 755 /app/.cache /app/vectordb /app/data /app/logs && \ + chown -R appuser:appuser /app && \ + # Poetry 가상환경 디렉토리 권한 재확인 + chown -R appuser:appuser /home/appuser/.cache && \ + chmod -R 755 /home/appuser/.cache + +# 🧹 캐시 정리 +RUN rm -rf /tmp/* /var/tmp/* + +# 🚀 포트 노출 +EXPOSE 8000 + +# 🏥 간단한 헬스체크 (appuser 권한으로 실행) +HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \ + CMD su -c "poetry --version && poetry config virtualenvs.path" appuser || exit 1 + +# 👤 최종 사용자 설정 +USER appuser + +# 🎯 기본 명령어 +CMD ["poetry", "--version"] + diff --git a/vector/deployment/manifest/configmap.yaml b/vector/deployment/manifest/configmap.yaml new file mode 100644 index 0000000..1eea584 --- /dev/null +++ b/vector/deployment/manifest/configmap.yaml @@ -0,0 +1,63 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: vector-api-config +data: + # 🔧 기존 애플리케이션 설정 (유지) + APP_TITLE: "음식점 Vector DB 구축 서비스" + APP_VERSION: "1.0.0" + APP_DESCRIPTION: "소상공인을 위한 AI 기반 경쟁업체 분석 및 액션 추천 시스템" + + # 🔧 기존 서버 설정 (유지) + HOST: "0.0.0.0" + PORT: "8000" + LOG_LEVEL: "debug" # 디버깅을 위해 debug로 변경 + + # 🔧 기존 Restaurant API 설정 (K8s 환경, 유지) + RESTAURANT_API_HOST: "restaurant-api-service" + RESTAURANT_API_PORT: "80" + + # 🔧 기존 Review API 설정 (K8s 환경, 유지) + REVIEW_API_HOST: "kakao-review-api-service" + REVIEW_API_PORT: "80" + + # 🔧 기존 Claude API 설정 (유지) + CLAUDE_MODEL: "claude-sonnet-4-20250514" + + # 🔧 기존 Vector DB 설정 (유지) + VECTOR_DB_PATH: "/app/vectordb" + VECTOR_DB_COLLECTION: "restaurant_reviews" + EMBEDDING_MODEL: "sentence-transformers/all-MiniLM-L6-v2" + + # 🔧 기존 데이터 수집 설정 (유지) + MAX_RESTAURANTS_PER_CATEGORY: "50" + MAX_REVIEWS_PER_RESTAURANT: "100" + REQUEST_DELAY: "0.1" + REQUEST_TIMEOUT: "600" + + # 🆕 ChromaDB 최신 버전 호환 설정 추가 + CHROMA_DB_IMPL: "duckdb+parquet" # SQLite 대신 DuckDB 사용 + ALLOW_RESET: "True" + ANONYMIZED_TELEMETRY: "False" + + # 🆕 Python 최적화 설정 + PYTHONUNBUFFERED: "1" + PYTHONDONTWRITEBYTECODE: "1" + + # 🆕 캐시 디렉토리 설정 + HF_HUB_CACHE: "/app/.cache/huggingface" + TRANSFORMERS_CACHE: "/app/.cache/transformers" + + # 🆕 FastAPI 설정 + FASTAPI_ENV: "production" + + # 🆕 Uvicorn 설정 + UVICORN_HOST: "0.0.0.0" + UVICORN_PORT: "8000" + UVICORN_LOG_LEVEL: "debug" + UVICORN_ACCESS_LOG: "true" + + # 🆕 타임아웃 설정 + STARTUP_TIMEOUT: "300" # 5분 + SHUTDOWN_TIMEOUT: "30" # 30초 + diff --git a/vector/deployment/manifest/deployment.yaml b/vector/deployment/manifest/deployment.yaml new file mode 100644 index 0000000..7306d73 --- /dev/null +++ b/vector/deployment/manifest/deployment.yaml @@ -0,0 +1,164 @@ +# deployment/manifests/deployment.yaml.fixed +apiVersion: apps/v1 +kind: Deployment +metadata: + name: vector-api + labels: + app: vector-api +spec: + replicas: 1 + selector: + matchLabels: + app: vector-api + template: + metadata: + labels: + app: vector-api + spec: + # 🔧 볼륨 권한 설정을 위한 initContainer + initContainers: + - name: volume-permissions + image: busybox:1.35 + command: + - /bin/sh + - -c + - | + echo "=== 볼륨 권한 설정 시작 ===" + mkdir -p /app/vectordb + chown -R 1000:1000 /app/vectordb + chmod -R 755 /app/vectordb + echo "=== 볼륨 권한 설정 완료 ===" + volumeMounts: + - name: vector-db-storage + mountPath: /app/vectordb + securityContext: + runAsUser: 0 + + containers: + - name: vector-api + image: acrdigitalgarage03.azurecr.io/vector-api:latest + imagePullPolicy: Always + ports: + - containerPort: 8000 + + # 🔧 보안 컨텍스트 + securityContext: + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + allowPrivilegeEscalation: false + readOnlyRootFilesystem: false + + # 🔧 리소스 설정 + resources: + requests: + memory: "4Gi" + cpu: "1000m" + limits: + memory: "8Gi" + cpu: "2000m" + + # 🏥 헬스체크 설정 + livenessProbe: + httpGet: + path: /health + port: 8000 + initialDelaySeconds: 120 + periodSeconds: 30 + timeoutSeconds: 15 + failureThreshold: 3 + + readinessProbe: + httpGet: + path: /health + port: 8000 + initialDelaySeconds: 60 + periodSeconds: 10 + timeoutSeconds: 10 + failureThreshold: 3 + + # 📂 볼륨 마운트 + volumeMounts: + - name: vector-db-storage + mountPath: /app/vectordb + + # ConfigMap 환경 변수 + envFrom: + - configMapRef: + name: vector-api-config + + # 🌍 환경변수 설정 (인증 필드 제거) + env: + - name: PYTHONUNBUFFERED + value: "1" + - name: PYTHONDONTWRITEBYTECODE + value: "1" + # 🔧 ChromaDB 기본 설정 (인증 필드 제거) + - name: ANONYMIZED_TELEMETRY + value: "False" + - name: CHROMA_DB_IMPL + value: "duckdb+parquet" + - name: ALLOW_RESET + value: "True" + # 🔧 로그 레벨 + - name: LOG_LEVEL + value: "info" + # 🔧 Claude API (ConfigMap에서 가져오기) + - name: CLAUDE_API_KEY + valueFrom: + secretKeyRef: + name: vector-api-secret + key: CLAUDE_API_KEY + - name: CLAUDE_MODEL + valueFrom: + configMapKeyRef: + name: vector-api-config + key: CLAUDE_MODEL + # 🔧 기타 설정 (ConfigMap에서 가져오기) + - name: APP_TITLE + valueFrom: + configMapKeyRef: + name: vector-api-config + key: APP_TITLE + - name: APP_VERSION + valueFrom: + configMapKeyRef: + name: vector-api-config + key: APP_VERSION + + # 📦 볼륨 설정 + volumes: + - name: vector-db-storage + persistentVolumeClaim: + claimName: vector-db-pvc + + # 🔐 이미지 Pull Secret + imagePullSecrets: + - name: acr-secret + + # 🎯 노드 선택 및 배치 설정 + nodeSelector: + agentpool: aipool + + tolerations: + - key: "dedicated" + operator: "Equal" + value: "aipool" + effect: "NoSchedule" + + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + labelSelector: + matchExpressions: + - key: app + operator: In + values: + - vector-api + topologyKey: kubernetes.io/hostname + + restartPolicy: Always + dnsPolicy: ClusterFirst + diff --git a/vector/deployment/manifest/ingress.yaml b/vector/deployment/manifest/ingress.yaml new file mode 100644 index 0000000..dd30ec8 --- /dev/null +++ b/vector/deployment/manifest/ingress.yaml @@ -0,0 +1,39 @@ +# deployment/manifests/ingress.yaml +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: vector-api-ingress + annotations: + nginx.ingress.kubernetes.io/rewrite-target: / + nginx.ingress.kubernetes.io/ssl-redirect: "false" + nginx.ingress.kubernetes.io/proxy-body-size: "10m" + # Vector DB 구축 시간을 고려한 긴 타임아웃 설정 + nginx.ingress.kubernetes.io/proxy-read-timeout: "1800" + nginx.ingress.kubernetes.io/proxy-send-timeout: "1800" + nginx.ingress.kubernetes.io/client-body-timeout: "1800" + nginx.ingress.kubernetes.io/proxy-connect-timeout: "60" + # CORS 설정 + nginx.ingress.kubernetes.io/enable-cors: "true" + nginx.ingress.kubernetes.io/cors-allow-origin: "*" + nginx.ingress.kubernetes.io/cors-allow-methods: "GET, POST, OPTIONS" + nginx.ingress.kubernetes.io/cors-allow-headers: "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization" +spec: + ingressClassName: nginx + rules: + # 환경에 맞게 호스트명 수정 필요 + - host: vector-api.20.249.191.180.nip.io + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: vector-api-service + port: + number: 80 + # TLS 설정 (HTTPS 필요시 주석 해제) + # tls: + # - hosts: + # - vector-api.example.com + # secretName: vector-api-tls + \ No newline at end of file diff --git a/vector/deployment/manifest/pvc.yaml b/vector/deployment/manifest/pvc.yaml new file mode 100644 index 0000000..430d88f --- /dev/null +++ b/vector/deployment/manifest/pvc.yaml @@ -0,0 +1,18 @@ +# deployment/manifests/pvc.yaml +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: vector-db-pvc + labels: + app: vector-api + component: storage +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 10Gi + storageClassName: managed + # 선택적: 특정 PV에 바인딩하려는 경우 + # volumeName: vector-db-pv + \ No newline at end of file diff --git a/vector/deployment/manifest/secret.yaml b/vector/deployment/manifest/secret.yaml new file mode 100644 index 0000000..13ab1ed --- /dev/null +++ b/vector/deployment/manifest/secret.yaml @@ -0,0 +1,11 @@ +# deployment/manifests/secret.yaml +apiVersion: v1 +kind: Secret +metadata: + name: vector-api-secret +type: Opaque +data: + # Claude API 키 (Base64 인코딩 필요) + # echo -n "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" | base64 + CLAUDE_API_KEY: c2stYW50LWFwaTAzLUVGM1ZocXJJUkVmY3hOa1V3ZkdENDluZ0k1SGZhcTUwd3c4WGZMd0psckR6akczdzNPSHRYTzFBZEltczJuRng2cmc4bk84cWhnMnFwUU01WFJnLTQ1SDdIQUFB + \ No newline at end of file diff --git a/vector/deployment/manifest/service.yaml b/vector/deployment/manifest/service.yaml new file mode 100644 index 0000000..1b26db3 --- /dev/null +++ b/vector/deployment/manifest/service.yaml @@ -0,0 +1,17 @@ +# deployment/manifests/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: vector-api-service + labels: + app: vector-api +spec: + type: ClusterIP + ports: + - port: 80 + targetPort: 8000 + protocol: TCP + name: http + selector: + app: vector-api + \ No newline at end of file diff --git a/vector/poetry.lock b/vector/poetry.lock new file mode 100644 index 0000000..6b970c0 --- /dev/null +++ b/vector/poetry.lock @@ -0,0 +1,4274 @@ +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. + +[[package]] +name = "aiohappyeyeballs" +version = "2.6.1" +description = "Happy Eyeballs for asyncio" +optional = false +python-versions = ">=3.9" +files = [ + {file = "aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8"}, + {file = "aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558"}, +] + +[[package]] +name = "aiohttp" +version = "3.12.13" +description = "Async http client/server framework (asyncio)" +optional = false +python-versions = ">=3.9" +files = [ + {file = "aiohttp-3.12.13-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5421af8f22a98f640261ee48aae3a37f0c41371e99412d55eaf2f8a46d5dad29"}, + {file = "aiohttp-3.12.13-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0fcda86f6cb318ba36ed8f1396a6a4a3fd8f856f84d426584392083d10da4de0"}, + {file = "aiohttp-3.12.13-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cd71c9fb92aceb5a23c4c39d8ecc80389c178eba9feab77f19274843eb9412d"}, + {file = "aiohttp-3.12.13-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34ebf1aca12845066c963016655dac897651e1544f22a34c9b461ac3b4b1d3aa"}, + {file = "aiohttp-3.12.13-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:893a4639694c5b7edd4bdd8141be296042b6806e27cc1d794e585c43010cc294"}, + {file = "aiohttp-3.12.13-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:663d8ee3ffb3494502ebcccb49078faddbb84c1d870f9c1dd5a29e85d1f747ce"}, + {file = "aiohttp-3.12.13-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0f8f6a85a0006ae2709aa4ce05749ba2cdcb4b43d6c21a16c8517c16593aabe"}, + {file = "aiohttp-3.12.13-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1582745eb63df267c92d8b61ca655a0ce62105ef62542c00a74590f306be8cb5"}, + {file = "aiohttp-3.12.13-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d59227776ee2aa64226f7e086638baa645f4b044f2947dbf85c76ab11dcba073"}, + {file = "aiohttp-3.12.13-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:06b07c418bde1c8e737d8fa67741072bd3f5b0fb66cf8c0655172188c17e5fa6"}, + {file = "aiohttp-3.12.13-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:9445c1842680efac0f81d272fd8db7163acfcc2b1436e3f420f4c9a9c5a50795"}, + {file = "aiohttp-3.12.13-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:09c4767af0b0b98c724f5d47f2bf33395c8986995b0a9dab0575ca81a554a8c0"}, + {file = "aiohttp-3.12.13-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f3854fbde7a465318ad8d3fc5bef8f059e6d0a87e71a0d3360bb56c0bf87b18a"}, + {file = "aiohttp-3.12.13-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2332b4c361c05ecd381edb99e2a33733f3db906739a83a483974b3df70a51b40"}, + {file = "aiohttp-3.12.13-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1561db63fa1b658cd94325d303933553ea7d89ae09ff21cc3bcd41b8521fbbb6"}, + {file = "aiohttp-3.12.13-cp310-cp310-win32.whl", hash = "sha256:a0be857f0b35177ba09d7c472825d1b711d11c6d0e8a2052804e3b93166de1ad"}, + {file = "aiohttp-3.12.13-cp310-cp310-win_amd64.whl", hash = "sha256:fcc30ad4fb5cb41a33953292d45f54ef4066746d625992aeac33b8c681173178"}, + {file = "aiohttp-3.12.13-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7c229b1437aa2576b99384e4be668af1db84b31a45305d02f61f5497cfa6f60c"}, + {file = "aiohttp-3.12.13-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:04076d8c63471e51e3689c93940775dc3d12d855c0c80d18ac5a1c68f0904358"}, + {file = "aiohttp-3.12.13-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:55683615813ce3601640cfaa1041174dc956d28ba0511c8cbd75273eb0587014"}, + {file = "aiohttp-3.12.13-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:921bc91e602d7506d37643e77819cb0b840d4ebb5f8d6408423af3d3bf79a7b7"}, + {file = "aiohttp-3.12.13-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e72d17fe0974ddeae8ed86db297e23dba39c7ac36d84acdbb53df2e18505a013"}, + {file = "aiohttp-3.12.13-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0653d15587909a52e024a261943cf1c5bdc69acb71f411b0dd5966d065a51a47"}, + {file = "aiohttp-3.12.13-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a77b48997c66722c65e157c06c74332cdf9c7ad00494b85ec43f324e5c5a9b9a"}, + {file = "aiohttp-3.12.13-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d6946bae55fd36cfb8e4092c921075cde029c71c7cb571d72f1079d1e4e013bc"}, + {file = "aiohttp-3.12.13-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f95db8c8b219bcf294a53742c7bda49b80ceb9d577c8e7aa075612b7f39ffb7"}, + {file = "aiohttp-3.12.13-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:03d5eb3cfb4949ab4c74822fb3326cd9655c2b9fe22e4257e2100d44215b2e2b"}, + {file = "aiohttp-3.12.13-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:6383dd0ffa15515283c26cbf41ac8e6705aab54b4cbb77bdb8935a713a89bee9"}, + {file = "aiohttp-3.12.13-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6548a411bc8219b45ba2577716493aa63b12803d1e5dc70508c539d0db8dbf5a"}, + {file = "aiohttp-3.12.13-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:81b0fcbfe59a4ca41dc8f635c2a4a71e63f75168cc91026c61be665945739e2d"}, + {file = "aiohttp-3.12.13-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:6a83797a0174e7995e5edce9dcecc517c642eb43bc3cba296d4512edf346eee2"}, + {file = "aiohttp-3.12.13-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a5734d8469a5633a4e9ffdf9983ff7cdb512524645c7a3d4bc8a3de45b935ac3"}, + {file = "aiohttp-3.12.13-cp311-cp311-win32.whl", hash = "sha256:fef8d50dfa482925bb6b4c208b40d8e9fa54cecba923dc65b825a72eed9a5dbd"}, + {file = "aiohttp-3.12.13-cp311-cp311-win_amd64.whl", hash = "sha256:9a27da9c3b5ed9d04c36ad2df65b38a96a37e9cfba6f1381b842d05d98e6afe9"}, + {file = "aiohttp-3.12.13-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0aa580cf80558557285b49452151b9c69f2fa3ad94c5c9e76e684719a8791b73"}, + {file = "aiohttp-3.12.13-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b103a7e414b57e6939cc4dece8e282cfb22043efd0c7298044f6594cf83ab347"}, + {file = "aiohttp-3.12.13-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78f64e748e9e741d2eccff9597d09fb3cd962210e5b5716047cbb646dc8fe06f"}, + {file = "aiohttp-3.12.13-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c955989bf4c696d2ededc6b0ccb85a73623ae6e112439398935362bacfaaf6"}, + {file = "aiohttp-3.12.13-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d640191016763fab76072c87d8854a19e8e65d7a6fcfcbf017926bdbbb30a7e5"}, + {file = "aiohttp-3.12.13-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4dc507481266b410dede95dd9f26c8d6f5a14315372cc48a6e43eac652237d9b"}, + {file = "aiohttp-3.12.13-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8a94daa873465d518db073bd95d75f14302e0208a08e8c942b2f3f1c07288a75"}, + {file = "aiohttp-3.12.13-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:177f52420cde4ce0bb9425a375d95577fe082cb5721ecb61da3049b55189e4e6"}, + {file = "aiohttp-3.12.13-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f7df1f620ec40f1a7fbcb99ea17d7326ea6996715e78f71a1c9a021e31b96b8"}, + {file = "aiohttp-3.12.13-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3062d4ad53b36e17796dce1c0d6da0ad27a015c321e663657ba1cc7659cfc710"}, + {file = "aiohttp-3.12.13-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:8605e22d2a86b8e51ffb5253d9045ea73683d92d47c0b1438e11a359bdb94462"}, + {file = "aiohttp-3.12.13-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:54fbbe6beafc2820de71ece2198458a711e224e116efefa01b7969f3e2b3ddae"}, + {file = "aiohttp-3.12.13-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:050bd277dfc3768b606fd4eae79dd58ceda67d8b0b3c565656a89ae34525d15e"}, + {file = "aiohttp-3.12.13-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2637a60910b58f50f22379b6797466c3aa6ae28a6ab6404e09175ce4955b4e6a"}, + {file = "aiohttp-3.12.13-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e986067357550d1aaa21cfe9897fa19e680110551518a5a7cf44e6c5638cb8b5"}, + {file = "aiohttp-3.12.13-cp312-cp312-win32.whl", hash = "sha256:ac941a80aeea2aaae2875c9500861a3ba356f9ff17b9cb2dbfb5cbf91baaf5bf"}, + {file = "aiohttp-3.12.13-cp312-cp312-win_amd64.whl", hash = "sha256:671f41e6146a749b6c81cb7fd07f5a8356d46febdaaaf07b0e774ff04830461e"}, + {file = "aiohttp-3.12.13-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d4a18e61f271127465bdb0e8ff36e8f02ac4a32a80d8927aa52371e93cd87938"}, + {file = "aiohttp-3.12.13-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:532542cb48691179455fab429cdb0d558b5e5290b033b87478f2aa6af5d20ace"}, + {file = "aiohttp-3.12.13-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d7eea18b52f23c050ae9db5d01f3d264ab08f09e7356d6f68e3f3ac2de9dfabb"}, + {file = "aiohttp-3.12.13-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad7c8e5c25f2a26842a7c239de3f7b6bfb92304593ef997c04ac49fb703ff4d7"}, + {file = "aiohttp-3.12.13-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6af355b483e3fe9d7336d84539fef460120c2f6e50e06c658fe2907c69262d6b"}, + {file = "aiohttp-3.12.13-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a95cf9f097498f35c88e3609f55bb47b28a5ef67f6888f4390b3d73e2bac6177"}, + {file = "aiohttp-3.12.13-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8ed8c38a1c584fe99a475a8f60eefc0b682ea413a84c6ce769bb19a7ff1c5ef"}, + {file = "aiohttp-3.12.13-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a0b9170d5d800126b5bc89d3053a2363406d6e327afb6afaeda2d19ee8bb103"}, + {file = "aiohttp-3.12.13-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:372feeace612ef8eb41f05ae014a92121a512bd5067db8f25101dd88a8db11da"}, + {file = "aiohttp-3.12.13-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a946d3702f7965d81f7af7ea8fb03bb33fe53d311df48a46eeca17e9e0beed2d"}, + {file = "aiohttp-3.12.13-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a0c4725fae86555bbb1d4082129e21de7264f4ab14baf735278c974785cd2041"}, + {file = "aiohttp-3.12.13-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9b28ea2f708234f0a5c44eb6c7d9eb63a148ce3252ba0140d050b091b6e842d1"}, + {file = "aiohttp-3.12.13-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d4f5becd2a5791829f79608c6f3dc745388162376f310eb9c142c985f9441cc1"}, + {file = "aiohttp-3.12.13-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:60f2ce6b944e97649051d5f5cc0f439360690b73909230e107fd45a359d3e911"}, + {file = "aiohttp-3.12.13-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:69fc1909857401b67bf599c793f2183fbc4804717388b0b888f27f9929aa41f3"}, + {file = "aiohttp-3.12.13-cp313-cp313-win32.whl", hash = "sha256:7d7e68787a2046b0e44ba5587aa723ce05d711e3a3665b6b7545328ac8e3c0dd"}, + {file = "aiohttp-3.12.13-cp313-cp313-win_amd64.whl", hash = "sha256:5a178390ca90419bfd41419a809688c368e63c86bd725e1186dd97f6b89c2706"}, + {file = "aiohttp-3.12.13-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:36f6c973e003dc9b0bb4e8492a643641ea8ef0e97ff7aaa5c0f53d68839357b4"}, + {file = "aiohttp-3.12.13-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6cbfc73179bd67c229eb171e2e3745d2afd5c711ccd1e40a68b90427f282eab1"}, + {file = "aiohttp-3.12.13-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1e8b27b2d414f7e3205aa23bb4a692e935ef877e3a71f40d1884f6e04fd7fa74"}, + {file = "aiohttp-3.12.13-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eabded0c2b2ef56243289112c48556c395d70150ce4220d9008e6b4b3dd15690"}, + {file = "aiohttp-3.12.13-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:003038e83f1a3ff97409999995ec02fe3008a1d675478949643281141f54751d"}, + {file = "aiohttp-3.12.13-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b6f46613031dbc92bdcaad9c4c22c7209236ec501f9c0c5f5f0b6a689bf50f3"}, + {file = "aiohttp-3.12.13-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c332c6bb04650d59fb94ed96491f43812549a3ba6e7a16a218e612f99f04145e"}, + {file = "aiohttp-3.12.13-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3fea41a2c931fb582cb15dc86a3037329e7b941df52b487a9f8b5aa960153cbd"}, + {file = "aiohttp-3.12.13-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:846104f45d18fb390efd9b422b27d8f3cf8853f1218c537f36e71a385758c896"}, + {file = "aiohttp-3.12.13-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d6c85ac7dd350f8da2520bac8205ce99df4435b399fa7f4dc4a70407073e390"}, + {file = "aiohttp-3.12.13-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5a1ecce0ed281bec7da8550da052a6b89552db14d0a0a45554156f085a912f48"}, + {file = "aiohttp-3.12.13-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:5304d74867028cca8f64f1cc1215eb365388033c5a691ea7aa6b0dc47412f495"}, + {file = "aiohttp-3.12.13-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:64d1f24ee95a2d1e094a4cd7a9b7d34d08db1bbcb8aa9fb717046b0a884ac294"}, + {file = "aiohttp-3.12.13-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:119c79922a7001ca6a9e253228eb39b793ea994fd2eccb79481c64b5f9d2a055"}, + {file = "aiohttp-3.12.13-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:bb18f00396d22e2f10cd8825d671d9f9a3ba968d708a559c02a627536b36d91c"}, + {file = "aiohttp-3.12.13-cp39-cp39-win32.whl", hash = "sha256:0022de47ef63fd06b065d430ac79c6b0bd24cdae7feaf0e8c6bac23b805a23a8"}, + {file = "aiohttp-3.12.13-cp39-cp39-win_amd64.whl", hash = "sha256:29e08111ccf81b2734ae03f1ad1cb03b9615e7d8f616764f22f71209c094f122"}, + {file = "aiohttp-3.12.13.tar.gz", hash = "sha256:47e2da578528264a12e4e3dd8dd72a7289e5f812758fe086473fab037a10fcce"}, +] + +[package.dependencies] +aiohappyeyeballs = ">=2.5.0" +aiosignal = ">=1.1.2" +attrs = ">=17.3.0" +frozenlist = ">=1.1.1" +multidict = ">=4.5,<7.0" +propcache = ">=0.2.0" +yarl = ">=1.17.0,<2.0" + +[package.extras] +speedups = ["Brotli", "aiodns (>=3.3.0)", "brotlicffi"] + +[[package]] +name = "aiosignal" +version = "1.3.2" +description = "aiosignal: a list of registered asynchronous callbacks" +optional = false +python-versions = ">=3.9" +files = [ + {file = "aiosignal-1.3.2-py2.py3-none-any.whl", hash = "sha256:45cde58e409a301715980c2b01d0c28bdde3770d8290b5eb2173759d9acb31a5"}, + {file = "aiosignal-1.3.2.tar.gz", hash = "sha256:a8c255c66fafb1e499c9351d0bf32ff2d8a0321595ebac3b93713656d2436f54"}, +] + +[package.dependencies] +frozenlist = ">=1.1.0" + +[[package]] +name = "annotated-types" +version = "0.7.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +files = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] + +[[package]] +name = "anthropic" +version = "0.54.0" +description = "The official Python library for the anthropic API" +optional = false +python-versions = ">=3.8" +files = [ + {file = "anthropic-0.54.0-py3-none-any.whl", hash = "sha256:c1062a0a905daeec17ca9c06c401e4b3f24cb0495841d29d752568a1d4018d56"}, + {file = "anthropic-0.54.0.tar.gz", hash = "sha256:5e6f997d97ce8e70eac603c3ec2e7f23addeff953fbbb76b19430562bb6ba815"}, +] + +[package.dependencies] +anyio = ">=3.5.0,<5" +distro = ">=1.7.0,<2" +httpx = ">=0.25.0,<1" +jiter = ">=0.4.0,<1" +pydantic = ">=1.9.0,<3" +sniffio = "*" +typing-extensions = ">=4.10,<5" + +[package.extras] +bedrock = ["boto3 (>=1.28.57)", "botocore (>=1.31.57)"] +vertex = ["google-auth[requests] (>=2,<3)"] + +[[package]] +name = "anyio" +version = "4.9.0" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +optional = false +python-versions = ">=3.9" +files = [ + {file = "anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c"}, + {file = "anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028"}, +] + +[package.dependencies] +idna = ">=2.8" +sniffio = ">=1.1" +typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} + +[package.extras] +doc = ["Sphinx (>=8.2,<9.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"] +test = ["anyio[trio]", "blockbuster (>=1.5.23)", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"] +trio = ["trio (>=0.26.1)"] + +[[package]] +name = "asgiref" +version = "3.8.1" +description = "ASGI specs, helper code, and adapters" +optional = false +python-versions = ">=3.8" +files = [ + {file = "asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47"}, + {file = "asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590"}, +] + +[package.extras] +tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] + +[[package]] +name = "attrs" +version = "25.3.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.8" +files = [ + {file = "attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3"}, + {file = "attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b"}, +] + +[package.extras] +benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier"] +tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] + +[[package]] +name = "backoff" +version = "2.2.1" +description = "Function decoration for backoff and retry" +optional = false +python-versions = ">=3.7,<4.0" +files = [ + {file = "backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8"}, + {file = "backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba"}, +] + +[[package]] +name = "bcrypt" +version = "4.3.0" +description = "Modern password hashing for your software and your servers" +optional = false +python-versions = ">=3.8" +files = [ + {file = "bcrypt-4.3.0-cp313-cp313t-macosx_10_12_universal2.whl", hash = "sha256:f01e060f14b6b57bbb72fc5b4a83ac21c443c9a2ee708e04a10e9192f90a6281"}, + {file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5eeac541cefd0bb887a371ef73c62c3cd78535e4887b310626036a7c0a817bb"}, + {file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59e1aa0e2cd871b08ca146ed08445038f42ff75968c7ae50d2fdd7860ade2180"}, + {file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:0042b2e342e9ae3d2ed22727c1262f76cc4f345683b5c1715f0250cf4277294f"}, + {file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74a8d21a09f5e025a9a23e7c0fd2c7fe8e7503e4d356c0a2c1486ba010619f09"}, + {file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:0142b2cb84a009f8452c8c5a33ace5e3dfec4159e7735f5afe9a4d50a8ea722d"}, + {file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_34_aarch64.whl", hash = "sha256:12fa6ce40cde3f0b899729dbd7d5e8811cb892d31b6f7d0334a1f37748b789fd"}, + {file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_34_x86_64.whl", hash = "sha256:5bd3cca1f2aa5dbcf39e2aa13dd094ea181f48959e1071265de49cc2b82525af"}, + {file = "bcrypt-4.3.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:335a420cfd63fc5bc27308e929bee231c15c85cc4c496610ffb17923abf7f231"}, + {file = "bcrypt-4.3.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:0e30e5e67aed0187a1764911af023043b4542e70a7461ad20e837e94d23e1d6c"}, + {file = "bcrypt-4.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b8d62290ebefd49ee0b3ce7500f5dbdcf13b81402c05f6dafab9a1e1b27212f"}, + {file = "bcrypt-4.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2ef6630e0ec01376f59a006dc72918b1bf436c3b571b80fa1968d775fa02fe7d"}, + {file = "bcrypt-4.3.0-cp313-cp313t-win32.whl", hash = "sha256:7a4be4cbf241afee43f1c3969b9103a41b40bcb3a3f467ab19f891d9bc4642e4"}, + {file = "bcrypt-4.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5c1949bf259a388863ced887c7861da1df681cb2388645766c89fdfd9004c669"}, + {file = "bcrypt-4.3.0-cp38-abi3-macosx_10_12_universal2.whl", hash = "sha256:f81b0ed2639568bf14749112298f9e4e2b28853dab50a8b357e31798686a036d"}, + {file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:864f8f19adbe13b7de11ba15d85d4a428c7e2f344bac110f667676a0ff84924b"}, + {file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e36506d001e93bffe59754397572f21bb5dc7c83f54454c990c74a468cd589e"}, + {file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:842d08d75d9fe9fb94b18b071090220697f9f184d4547179b60734846461ed59"}, + {file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7c03296b85cb87db865d91da79bf63d5609284fc0cab9472fdd8367bbd830753"}, + {file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:62f26585e8b219cdc909b6a0069efc5e4267e25d4a3770a364ac58024f62a761"}, + {file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:beeefe437218a65322fbd0069eb437e7c98137e08f22c4660ac2dc795c31f8bb"}, + {file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:97eea7408db3a5bcce4a55d13245ab3fa566e23b4c67cd227062bb49e26c585d"}, + {file = "bcrypt-4.3.0-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:191354ebfe305e84f344c5964c7cd5f924a3bfc5d405c75ad07f232b6dffb49f"}, + {file = "bcrypt-4.3.0-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:41261d64150858eeb5ff43c753c4b216991e0ae16614a308a15d909503617732"}, + {file = "bcrypt-4.3.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:33752b1ba962ee793fa2b6321404bf20011fe45b9afd2a842139de3011898fef"}, + {file = "bcrypt-4.3.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:50e6e80a4bfd23a25f5c05b90167c19030cf9f87930f7cb2eacb99f45d1c3304"}, + {file = "bcrypt-4.3.0-cp38-abi3-win32.whl", hash = "sha256:67a561c4d9fb9465ec866177e7aebcad08fe23aaf6fbd692a6fab69088abfc51"}, + {file = "bcrypt-4.3.0-cp38-abi3-win_amd64.whl", hash = "sha256:584027857bc2843772114717a7490a37f68da563b3620f78a849bcb54dc11e62"}, + {file = "bcrypt-4.3.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0d3efb1157edebfd9128e4e46e2ac1a64e0c1fe46fb023158a407c7892b0f8c3"}, + {file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08bacc884fd302b611226c01014eca277d48f0a05187666bca23aac0dad6fe24"}, + {file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6746e6fec103fcd509b96bacdfdaa2fbde9a553245dbada284435173a6f1aef"}, + {file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:afe327968aaf13fc143a56a3360cb27d4ad0345e34da12c7290f1b00b8fe9a8b"}, + {file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d9af79d322e735b1fc33404b5765108ae0ff232d4b54666d46730f8ac1a43676"}, + {file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f1e3ffa1365e8702dc48c8b360fef8d7afeca482809c5e45e653af82ccd088c1"}, + {file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:3004df1b323d10021fda07a813fd33e0fd57bef0e9a480bb143877f6cba996fe"}, + {file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:531457e5c839d8caea9b589a1bcfe3756b0547d7814e9ce3d437f17da75c32b0"}, + {file = "bcrypt-4.3.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:17a854d9a7a476a89dcef6c8bd119ad23e0f82557afbd2c442777a16408e614f"}, + {file = "bcrypt-4.3.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6fb1fd3ab08c0cbc6826a2e0447610c6f09e983a281b919ed721ad32236b8b23"}, + {file = "bcrypt-4.3.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e965a9c1e9a393b8005031ff52583cedc15b7884fce7deb8b0346388837d6cfe"}, + {file = "bcrypt-4.3.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:79e70b8342a33b52b55d93b3a59223a844962bef479f6a0ea318ebbcadf71505"}, + {file = "bcrypt-4.3.0-cp39-abi3-win32.whl", hash = "sha256:b4d4e57f0a63fd0b358eb765063ff661328f69a04494427265950c71b992a39a"}, + {file = "bcrypt-4.3.0-cp39-abi3-win_amd64.whl", hash = "sha256:e53e074b120f2877a35cc6c736b8eb161377caae8925c17688bd46ba56daaa5b"}, + {file = "bcrypt-4.3.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c950d682f0952bafcceaf709761da0a32a942272fad381081b51096ffa46cea1"}, + {file = "bcrypt-4.3.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:107d53b5c67e0bbc3f03ebf5b030e0403d24dda980f8e244795335ba7b4a027d"}, + {file = "bcrypt-4.3.0-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:b693dbb82b3c27a1604a3dff5bfc5418a7e6a781bb795288141e5f80cf3a3492"}, + {file = "bcrypt-4.3.0-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:b6354d3760fcd31994a14c89659dee887f1351a06e5dac3c1142307172a79f90"}, + {file = "bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a839320bf27d474e52ef8cb16449bb2ce0ba03ca9f44daba6d93fa1d8828e48a"}, + {file = "bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:bdc6a24e754a555d7316fa4774e64c6c3997d27ed2d1964d55920c7c227bc4ce"}, + {file = "bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:55a935b8e9a1d2def0626c4269db3fcd26728cbff1e84f0341465c31c4ee56d8"}, + {file = "bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:57967b7a28d855313a963aaea51bf6df89f833db4320da458e5b3c5ab6d4c938"}, + {file = "bcrypt-4.3.0.tar.gz", hash = "sha256:3a3fd2204178b6d2adcf09cb4f6426ffef54762577a7c9b54c159008cb288c18"}, +] + +[package.extras] +tests = ["pytest (>=3.2.1,!=3.3.0)"] +typecheck = ["mypy"] + +[[package]] +name = "build" +version = "1.2.2.post1" +description = "A simple, correct Python build frontend" +optional = false +python-versions = ">=3.8" +files = [ + {file = "build-1.2.2.post1-py3-none-any.whl", hash = "sha256:1d61c0887fa860c01971625baae8bdd338e517b836a2f70dd1f7aa3a6b2fc5b5"}, + {file = "build-1.2.2.post1.tar.gz", hash = "sha256:b36993e92ca9375a219c99e606a122ff365a760a2d4bba0caa09bd5278b608b7"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "os_name == \"nt\""} +packaging = ">=19.1" +pyproject_hooks = "*" + +[package.extras] +docs = ["furo (>=2023.08.17)", "sphinx (>=7.0,<8.0)", "sphinx-argparse-cli (>=1.5)", "sphinx-autodoc-typehints (>=1.10)", "sphinx-issues (>=3.0.0)"] +test = ["build[uv,virtualenv]", "filelock (>=3)", "pytest (>=6.2.4)", "pytest-cov (>=2.12)", "pytest-mock (>=2)", "pytest-rerunfailures (>=9.1)", "pytest-xdist (>=1.34)", "setuptools (>=42.0.0)", "setuptools (>=56.0.0)", "setuptools (>=56.0.0)", "setuptools (>=67.8.0)", "wheel (>=0.36.0)"] +typing = ["build[uv]", "importlib-metadata (>=5.1)", "mypy (>=1.9.0,<1.10.0)", "tomli", "typing-extensions (>=3.7.4.3)"] +uv = ["uv (>=0.1.18)"] +virtualenv = ["virtualenv (>=20.0.35)"] + +[[package]] +name = "cachetools" +version = "5.5.2" +description = "Extensible memoizing collections and decorators" +optional = false +python-versions = ">=3.7" +files = [ + {file = "cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a"}, + {file = "cachetools-5.5.2.tar.gz", hash = "sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4"}, +] + +[[package]] +name = "certifi" +version = "2025.6.15" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.7" +files = [ + {file = "certifi-2025.6.15-py3-none-any.whl", hash = "sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057"}, + {file = "certifi-2025.6.15.tar.gz", hash = "sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.2" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7" +files = [ + {file = "charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cad5f45b3146325bb38d6855642f6fd609c3f7cad4dbaf75549bf3b904d3184"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2680962a4848b3c4f155dc2ee64505a9c57186d0d56b43123b17ca3de18f0fa"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:36b31da18b8890a76ec181c3cf44326bf2c48e36d393ca1b72b3f484113ea344"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4074c5a429281bf056ddd4c5d3b740ebca4d43ffffe2ef4bf4d2d05114299da"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9e36a97bee9b86ef9a1cf7bb96747eb7a15c2f22bdb5b516434b00f2a599f02"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:1b1bde144d98e446b056ef98e59c256e9294f6b74d7af6846bf5ffdafd687a7d"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:915f3849a011c1f593ab99092f3cecfcb4d65d8feb4a64cf1bf2d22074dc0ec4"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:fb707f3e15060adf5b7ada797624a6c6e0138e2a26baa089df64c68ee98e040f"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:25a23ea5c7edc53e0f29bae2c44fcb5a1aa10591aae107f2a2b2583a9c5cbc64"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:770cab594ecf99ae64c236bc9ee3439c3f46be49796e265ce0cc8bc17b10294f"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-win32.whl", hash = "sha256:6a0289e4589e8bdfef02a80478f1dfcb14f0ab696b5a00e1f4b8a14a307a3c58"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6fc1f5b51fa4cecaa18f2bd7a003f3dd039dd615cd69a2afd6d3b19aed6775f2"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:76af085e67e56c8816c3ccf256ebd136def2ed9654525348cfa744b6802b69eb"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e45ba65510e2647721e35323d6ef54c7974959f6081b58d4ef5d87c60c84919a"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:046595208aae0120559a67693ecc65dd75d46f7bf687f159127046628178dc45"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75d10d37a47afee94919c4fab4c22b9bc2a8bf7d4f46f87363bcf0573f3ff4f5"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6333b3aa5a12c26b2a4d4e7335a28f1475e0e5e17d69d55141ee3cab736f66d1"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8323a9b031aa0393768b87f04b4164a40037fb2a3c11ac06a03ffecd3618027"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:24498ba8ed6c2e0b56d4acbf83f2d989720a93b41d712ebd4f4979660db4417b"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:844da2b5728b5ce0e32d863af26f32b5ce61bc4273a9c720a9f3aa9df73b1455"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:65c981bdbd3f57670af8b59777cbfae75364b483fa8a9f420f08094531d54a01"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:3c21d4fca343c805a52c0c78edc01e3477f6dd1ad7c47653241cf2a206d4fc58"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dc7039885fa1baf9be153a0626e337aa7ec8bf96b0128605fb0d77788ddc1681"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-win32.whl", hash = "sha256:8272b73e1c5603666618805fe821edba66892e2870058c94c53147602eab29c7"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:70f7172939fdf8790425ba31915bfbe8335030f05b9913d7ae00a87d4395620a"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-win32.whl", hash = "sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e"}, + {file = "charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0"}, + {file = "charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63"}, +] + +[[package]] +name = "chromadb" +version = "1.0.12" +description = "Chroma." +optional = false +python-versions = ">=3.9" +files = [ + {file = "chromadb-1.0.12-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:b98956f5881f2ec6842946d0f968da6925ccd5019125935efdd49e2b61c6dafe"}, + {file = "chromadb-1.0.12-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:3a245d34dcd9d6ef095bb10c688a8ec6a2aacf13e777d410a291324d29428039"}, + {file = "chromadb-1.0.12-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e5c7d1912d12aeb21a598c9f97c2540548f1f3e47ccca1973ad774441740d0"}, + {file = "chromadb-1.0.12-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff82c806846301bb8ffbddd04abeb2413b13d1a1e7b1bb4328dd831ad01f1b1d"}, + {file = "chromadb-1.0.12-cp39-abi3-win_amd64.whl", hash = "sha256:6e7993a903b27b184468c80e094259645c458f494dac6380f7156e5accb9b1cb"}, + {file = "chromadb-1.0.12.tar.gz", hash = "sha256:d3d2d4bb5eff3cb3ae72a713959dda3c8209131c2d16c3d788bd0189eba8b51e"}, +] + +[package.dependencies] +bcrypt = ">=4.0.1" +build = ">=1.0.3" +fastapi = "0.115.9" +grpcio = ">=1.58.0" +httpx = ">=0.27.0" +importlib-resources = "*" +jsonschema = ">=4.19.0" +kubernetes = ">=28.1.0" +mmh3 = ">=4.0.1" +numpy = ">=1.22.5" +onnxruntime = ">=1.14.1" +opentelemetry-api = ">=1.2.0" +opentelemetry-exporter-otlp-proto-grpc = ">=1.2.0" +opentelemetry-instrumentation-fastapi = ">=0.41b0" +opentelemetry-sdk = ">=1.2.0" +orjson = ">=3.9.12" +overrides = ">=7.3.1" +posthog = ">=2.4.0" +pydantic = ">=1.9" +pypika = ">=0.48.9" +pyyaml = ">=6.0.0" +rich = ">=10.11.0" +tenacity = ">=8.2.3" +tokenizers = ">=0.13.2" +tqdm = ">=4.65.0" +typer = ">=0.9.0" +typing-extensions = ">=4.5.0" +uvicorn = {version = ">=0.18.3", extras = ["standard"]} + +[package.extras] +dev = ["chroma-hnswlib (==0.7.6)"] + +[[package]] +name = "click" +version = "8.2.1" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.10" +files = [ + {file = "click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b"}, + {file = "click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "coloredlogs" +version = "15.0.1" +description = "Colored terminal output for Python's logging module" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934"}, + {file = "coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0"}, +] + +[package.dependencies] +humanfriendly = ">=9.1" + +[package.extras] +cron = ["capturer (>=2.4)"] + +[[package]] +name = "distro" +version = "1.9.0" +description = "Distro - an OS platform information API" +optional = false +python-versions = ">=3.6" +files = [ + {file = "distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2"}, + {file = "distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed"}, +] + +[[package]] +name = "duckdb" +version = "1.3.0" +description = "DuckDB in-process database" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "duckdb-1.3.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:fc65c1e97aa010359c43c0342ea423e6efa3cb8c8e3f133b0765451ce674e3db"}, + {file = "duckdb-1.3.0-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:8fc91b629646679e33806342510335ccbbeaf2b823186f0ae829fd48e7a63c66"}, + {file = "duckdb-1.3.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:1a69b970553fd015c557238d427ef00be3c8ed58c3bc3641aef987e33f8bf614"}, + {file = "duckdb-1.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1003e84c07b84680cee6d06e4795b6e861892474704f7972058594a52c7473cf"}, + {file = "duckdb-1.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:992239b54ca6f015ad0ed0d80f3492c065313c4641df0a226183b8860cb7f5b0"}, + {file = "duckdb-1.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0ba1c5af59e8147216149b814b1970b8f7e3c240494a9688171390db3c504b29"}, + {file = "duckdb-1.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:57b794ca28e22b23bd170506cb1d4704a3608e67f0fe33273db9777b69bdf26a"}, + {file = "duckdb-1.3.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:60a58b85929754abb21db1e739d2f53eaef63e6015e62ba58eae3425030e7935"}, + {file = "duckdb-1.3.0-cp311-cp311-macosx_12_0_universal2.whl", hash = "sha256:1d46b5a20f078b1b2284243e02a1fde7e12cbb8d205fce62e4700bcfe6a09881"}, + {file = "duckdb-1.3.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:0044e5ffb2d46308099640a92f99980a44e12bb68642aa9e6b08acbf300d64a1"}, + {file = "duckdb-1.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cb813de2ca2f5e7c77392a67bdcaa174bfd69ebbfdfc983024af270c77a0447"}, + {file = "duckdb-1.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7a0c993eb6df2b30b189ad747f3aea1b0b87b78ab7f80c6e7c57117b6e8dbfb0"}, + {file = "duckdb-1.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6728e209570d36ece66dd7249e5d6055326321137cd807f26300733283930cd4"}, + {file = "duckdb-1.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:7e652b7c8dbdb91a94fd7d543d3e115d24a25aa0791a373a852e20cb7bb21154"}, + {file = "duckdb-1.3.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:f24038fe9b83dcbaeafb1ed76ec3b3f38943c1c8d27ab464ad384db8a6658b61"}, + {file = "duckdb-1.3.0-cp312-cp312-macosx_12_0_universal2.whl", hash = "sha256:956c85842841bef68f4a5388c6b225b933151a7c06d568390fc895fc44607913"}, + {file = "duckdb-1.3.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:efe883d822ed56fcfbb6a7b397c13f6a0d2eaeb3bc4ef4510f84fadb3dfe416d"}, + {file = "duckdb-1.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3872a3a1b80ffba5264ea236a3754d0c41d3c7b01bdf8cdcb1c180fc1b8dc8e2"}, + {file = "duckdb-1.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:30bf45ad78a5a997f378863e036e917b481d18d685e5c977cd0a3faf2e31fbaf"}, + {file = "duckdb-1.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:85cbd8e1d65df8a0780023baf5045d3033fabd154799bc9ea6d9ab5728f41eb3"}, + {file = "duckdb-1.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:8754c40dac0f26d9fb0363bbb5df02f7a61ce6a6728d5efc02c3bc925d7c89c3"}, + {file = "duckdb-1.3.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:176b9818d940c52ac7f31c64a98cf172d7c19d2a006017c9c4e9c06c246e36bf"}, + {file = "duckdb-1.3.0-cp313-cp313-macosx_12_0_universal2.whl", hash = "sha256:03981f7e8793f07a4a9a2ba387640e71d0a99ebcaf8693ab09f96d59e628b713"}, + {file = "duckdb-1.3.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:a177d55a38a62fdf79b59a0eaa32531a1dbb443265f6d67f64992cc1e82b755c"}, + {file = "duckdb-1.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b1c30e3749823147d5578bc3f01f35d1a0433a1c768908d946056ec8d6e1757e"}, + {file = "duckdb-1.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5855f3a564baf22eeeab70c120b51f5a11914f1f1634f03382daeb6b1dea4c62"}, + {file = "duckdb-1.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9b1fac15a48056f7c2739cf8800873063ba2f691e91a9b2fc167658a401ca76a"}, + {file = "duckdb-1.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:fbdfc1c0b83b90f780ae74038187ee696bb56ab727a289752372d7ec42dda65b"}, + {file = "duckdb-1.3.0-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:5f6b5d725546ad30abc125a6813734b493fea694bc3123e991c480744573c2f1"}, + {file = "duckdb-1.3.0-cp39-cp39-macosx_12_0_universal2.whl", hash = "sha256:fcbcc9b956b06cf5ee94629438ecab88de89b08b5620fcda93665c222ab18cd4"}, + {file = "duckdb-1.3.0-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:2d32f2d44105e1705d8a0fb6d6d246fd69aff82c80ad23293266244b66b69012"}, + {file = "duckdb-1.3.0-cp39-cp39-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0aa7a5c0dcb780850e6da1227fb1d552af8e1a5091e02667ab6ace61ab49ce6c"}, + {file = "duckdb-1.3.0-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7cb254fd5405f3edbd7d962ba39c72e4ab90b37cb4d0e34846089796c8078419"}, + {file = "duckdb-1.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a7d337b58c59fd2cd9faae531b05d940f8d92bdc2e14cb6e9a5a37675ad2742d"}, + {file = "duckdb-1.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3cea3a345755c7dbcb58403dbab8befd499c82f0d27f893a4c1d4b8cf56ec54"}, + {file = "duckdb-1.3.0.tar.gz", hash = "sha256:09aaa4b1dca24f4d1f231e7ae66b6413e317b7e04e2753541d42df6c8113fac7"}, +] + +[[package]] +name = "durationpy" +version = "0.10" +description = "Module for converting between datetime.timedelta and Go's Duration strings." +optional = false +python-versions = "*" +files = [ + {file = "durationpy-0.10-py3-none-any.whl", hash = "sha256:3b41e1b601234296b4fb368338fdcd3e13e0b4fb5b67345948f4f2bf9868b286"}, + {file = "durationpy-0.10.tar.gz", hash = "sha256:1fa6893409a6e739c9c72334fc65cca1f355dbdd93405d30f726deb5bde42fba"}, +] + +[[package]] +name = "fastapi" +version = "0.115.9" +description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" +optional = false +python-versions = ">=3.8" +files = [ + {file = "fastapi-0.115.9-py3-none-any.whl", hash = "sha256:4a439d7923e4de796bcc88b64e9754340fcd1574673cbd865ba8a99fe0d28c56"}, + {file = "fastapi-0.115.9.tar.gz", hash = "sha256:9d7da3b196c5eed049bc769f9475cd55509a112fbe031c0ef2f53768ae68d13f"}, +] + +[package.dependencies] +pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" +starlette = ">=0.40.0,<0.46.0" +typing-extensions = ">=4.8.0" + +[package.extras] +all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=3.1.5)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] +standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"] + +[[package]] +name = "filelock" +version = "3.18.0" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.9" +files = [ + {file = "filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de"}, + {file = "filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2"}, +] + +[package.extras] +docs = ["furo (>=2024.8.6)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.6.10)", "diff-cover (>=9.2.1)", "pytest (>=8.3.4)", "pytest-asyncio (>=0.25.2)", "pytest-cov (>=6)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.28.1)"] +typing = ["typing-extensions (>=4.12.2)"] + +[[package]] +name = "flatbuffers" +version = "25.2.10" +description = "The FlatBuffers serialization format for Python" +optional = false +python-versions = "*" +files = [ + {file = "flatbuffers-25.2.10-py2.py3-none-any.whl", hash = "sha256:ebba5f4d5ea615af3f7fd70fc310636fbb2bbd1f566ac0a23d98dd412de50051"}, + {file = "flatbuffers-25.2.10.tar.gz", hash = "sha256:97e451377a41262f8d9bd4295cc836133415cc03d8cb966410a4af92eb00d26e"}, +] + +[[package]] +name = "frozenlist" +version = "1.7.0" +description = "A list-like structure which implements collections.abc.MutableSequence" +optional = false +python-versions = ">=3.9" +files = [ + {file = "frozenlist-1.7.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cc4df77d638aa2ed703b878dd093725b72a824c3c546c076e8fdf276f78ee84a"}, + {file = "frozenlist-1.7.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:716a9973a2cc963160394f701964fe25012600f3d311f60c790400b00e568b61"}, + {file = "frozenlist-1.7.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a0fd1bad056a3600047fb9462cff4c5322cebc59ebf5d0a3725e0ee78955001d"}, + {file = "frozenlist-1.7.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3789ebc19cb811163e70fe2bd354cea097254ce6e707ae42e56f45e31e96cb8e"}, + {file = "frozenlist-1.7.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:af369aa35ee34f132fcfad5be45fbfcde0e3a5f6a1ec0712857f286b7d20cca9"}, + {file = "frozenlist-1.7.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac64b6478722eeb7a3313d494f8342ef3478dff539d17002f849101b212ef97c"}, + {file = "frozenlist-1.7.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f89f65d85774f1797239693cef07ad4c97fdd0639544bad9ac4b869782eb1981"}, + {file = "frozenlist-1.7.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1073557c941395fdfcfac13eb2456cb8aad89f9de27bae29fabca8e563b12615"}, + {file = "frozenlist-1.7.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ed8d2fa095aae4bdc7fdd80351009a48d286635edffee66bf865e37a9125c50"}, + {file = "frozenlist-1.7.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:24c34bea555fe42d9f928ba0a740c553088500377448febecaa82cc3e88aa1fa"}, + {file = "frozenlist-1.7.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:69cac419ac6a6baad202c85aaf467b65ac860ac2e7f2ac1686dc40dbb52f6577"}, + {file = "frozenlist-1.7.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:960d67d0611f4c87da7e2ae2eacf7ea81a5be967861e0c63cf205215afbfac59"}, + {file = "frozenlist-1.7.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:41be2964bd4b15bf575e5daee5a5ce7ed3115320fb3c2b71fca05582ffa4dc9e"}, + {file = "frozenlist-1.7.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:46d84d49e00c9429238a7ce02dc0be8f6d7cd0cd405abd1bebdc991bf27c15bd"}, + {file = "frozenlist-1.7.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:15900082e886edb37480335d9d518cec978afc69ccbc30bd18610b7c1b22a718"}, + {file = "frozenlist-1.7.0-cp310-cp310-win32.whl", hash = "sha256:400ddd24ab4e55014bba442d917203c73b2846391dd42ca5e38ff52bb18c3c5e"}, + {file = "frozenlist-1.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:6eb93efb8101ef39d32d50bce242c84bcbddb4f7e9febfa7b524532a239b4464"}, + {file = "frozenlist-1.7.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:aa51e147a66b2d74de1e6e2cf5921890de6b0f4820b257465101d7f37b49fb5a"}, + {file = "frozenlist-1.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9b35db7ce1cd71d36ba24f80f0c9e7cff73a28d7a74e91fe83e23d27c7828750"}, + {file = "frozenlist-1.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:34a69a85e34ff37791e94542065c8416c1afbf820b68f720452f636d5fb990cd"}, + {file = "frozenlist-1.7.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a646531fa8d82c87fe4bb2e596f23173caec9185bfbca5d583b4ccfb95183e2"}, + {file = "frozenlist-1.7.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:79b2ffbba483f4ed36a0f236ccb85fbb16e670c9238313709638167670ba235f"}, + {file = "frozenlist-1.7.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a26f205c9ca5829cbf82bb2a84b5c36f7184c4316617d7ef1b271a56720d6b30"}, + {file = "frozenlist-1.7.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bcacfad3185a623fa11ea0e0634aac7b691aa925d50a440f39b458e41c561d98"}, + {file = "frozenlist-1.7.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:72c1b0fe8fe451b34f12dce46445ddf14bd2a5bcad7e324987194dc8e3a74c86"}, + {file = "frozenlist-1.7.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61d1a5baeaac6c0798ff6edfaeaa00e0e412d49946c53fae8d4b8e8b3566c4ae"}, + {file = "frozenlist-1.7.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7edf5c043c062462f09b6820de9854bf28cc6cc5b6714b383149745e287181a8"}, + {file = "frozenlist-1.7.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:d50ac7627b3a1bd2dcef6f9da89a772694ec04d9a61b66cf87f7d9446b4a0c31"}, + {file = "frozenlist-1.7.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ce48b2fece5aeb45265bb7a58259f45027db0abff478e3077e12b05b17fb9da7"}, + {file = "frozenlist-1.7.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:fe2365ae915a1fafd982c146754e1de6ab3478def8a59c86e1f7242d794f97d5"}, + {file = "frozenlist-1.7.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:45a6f2fdbd10e074e8814eb98b05292f27bad7d1883afbe009d96abdcf3bc898"}, + {file = "frozenlist-1.7.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:21884e23cffabb157a9dd7e353779077bf5b8f9a58e9b262c6caad2ef5f80a56"}, + {file = "frozenlist-1.7.0-cp311-cp311-win32.whl", hash = "sha256:284d233a8953d7b24f9159b8a3496fc1ddc00f4db99c324bd5fb5f22d8698ea7"}, + {file = "frozenlist-1.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:387cbfdcde2f2353f19c2f66bbb52406d06ed77519ac7ee21be0232147c2592d"}, + {file = "frozenlist-1.7.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3dbf9952c4bb0e90e98aec1bd992b3318685005702656bc6f67c1a32b76787f2"}, + {file = "frozenlist-1.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1f5906d3359300b8a9bb194239491122e6cf1444c2efb88865426f170c262cdb"}, + {file = "frozenlist-1.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3dabd5a8f84573c8d10d8859a50ea2dec01eea372031929871368c09fa103478"}, + {file = "frozenlist-1.7.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa57daa5917f1738064f302bf2626281a1cb01920c32f711fbc7bc36111058a8"}, + {file = "frozenlist-1.7.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c193dda2b6d49f4c4398962810fa7d7c78f032bf45572b3e04dd5249dff27e08"}, + {file = "frozenlist-1.7.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfe2b675cf0aaa6d61bf8fbffd3c274b3c9b7b1623beb3809df8a81399a4a9c4"}, + {file = "frozenlist-1.7.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8fc5d5cda37f62b262405cf9652cf0856839c4be8ee41be0afe8858f17f4c94b"}, + {file = "frozenlist-1.7.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0d5ce521d1dd7d620198829b87ea002956e4319002ef0bc8d3e6d045cb4646e"}, + {file = "frozenlist-1.7.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:488d0a7d6a0008ca0db273c542098a0fa9e7dfaa7e57f70acef43f32b3f69dca"}, + {file = "frozenlist-1.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:15a7eaba63983d22c54d255b854e8108e7e5f3e89f647fc854bd77a237e767df"}, + {file = "frozenlist-1.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1eaa7e9c6d15df825bf255649e05bd8a74b04a4d2baa1ae46d9c2d00b2ca2cb5"}, + {file = "frozenlist-1.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e4389e06714cfa9d47ab87f784a7c5be91d3934cd6e9a7b85beef808297cc025"}, + {file = "frozenlist-1.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:73bd45e1488c40b63fe5a7df892baf9e2a4d4bb6409a2b3b78ac1c6236178e01"}, + {file = "frozenlist-1.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99886d98e1643269760e5fe0df31e5ae7050788dd288947f7f007209b8c33f08"}, + {file = "frozenlist-1.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:290a172aae5a4c278c6da8a96222e6337744cd9c77313efe33d5670b9f65fc43"}, + {file = "frozenlist-1.7.0-cp312-cp312-win32.whl", hash = "sha256:426c7bc70e07cfebc178bc4c2bf2d861d720c4fff172181eeb4a4c41d4ca2ad3"}, + {file = "frozenlist-1.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:563b72efe5da92e02eb68c59cb37205457c977aa7a449ed1b37e6939e5c47c6a"}, + {file = "frozenlist-1.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee80eeda5e2a4e660651370ebffd1286542b67e268aa1ac8d6dbe973120ef7ee"}, + {file = "frozenlist-1.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d1a81c85417b914139e3a9b995d4a1c84559afc839a93cf2cb7f15e6e5f6ed2d"}, + {file = "frozenlist-1.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cbb65198a9132ebc334f237d7b0df163e4de83fb4f2bdfe46c1e654bdb0c5d43"}, + {file = "frozenlist-1.7.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dab46c723eeb2c255a64f9dc05b8dd601fde66d6b19cdb82b2e09cc6ff8d8b5d"}, + {file = "frozenlist-1.7.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6aeac207a759d0dedd2e40745575ae32ab30926ff4fa49b1635def65806fddee"}, + {file = "frozenlist-1.7.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bd8c4e58ad14b4fa7802b8be49d47993182fdd4023393899632c88fd8cd994eb"}, + {file = "frozenlist-1.7.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04fb24d104f425da3540ed83cbfc31388a586a7696142004c577fa61c6298c3f"}, + {file = "frozenlist-1.7.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a5c505156368e4ea6b53b5ac23c92d7edc864537ff911d2fb24c140bb175e60"}, + {file = "frozenlist-1.7.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8bd7eb96a675f18aa5c553eb7ddc24a43c8c18f22e1f9925528128c052cdbe00"}, + {file = "frozenlist-1.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:05579bf020096fe05a764f1f84cd104a12f78eaab68842d036772dc6d4870b4b"}, + {file = "frozenlist-1.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:376b6222d114e97eeec13d46c486facd41d4f43bab626b7c3f6a8b4e81a5192c"}, + {file = "frozenlist-1.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0aa7e176ebe115379b5b1c95b4096fb1c17cce0847402e227e712c27bdb5a949"}, + {file = "frozenlist-1.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3fbba20e662b9c2130dc771e332a99eff5da078b2b2648153a40669a6d0e36ca"}, + {file = "frozenlist-1.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f3f4410a0a601d349dd406b5713fec59b4cee7e71678d5b17edda7f4655a940b"}, + {file = "frozenlist-1.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e2cdfaaec6a2f9327bf43c933c0319a7c429058e8537c508964a133dffee412e"}, + {file = "frozenlist-1.7.0-cp313-cp313-win32.whl", hash = "sha256:5fc4df05a6591c7768459caba1b342d9ec23fa16195e744939ba5914596ae3e1"}, + {file = "frozenlist-1.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:52109052b9791a3e6b5d1b65f4b909703984b770694d3eb64fad124c835d7cba"}, + {file = "frozenlist-1.7.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a6f86e4193bb0e235ef6ce3dde5cbabed887e0b11f516ce8a0f4d3b33078ec2d"}, + {file = "frozenlist-1.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:82d664628865abeb32d90ae497fb93df398a69bb3434463d172b80fc25b0dd7d"}, + {file = "frozenlist-1.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:912a7e8375a1c9a68325a902f3953191b7b292aa3c3fb0d71a216221deca460b"}, + {file = "frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9537c2777167488d539bc5de2ad262efc44388230e5118868e172dd4a552b146"}, + {file = "frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f34560fb1b4c3e30ba35fa9a13894ba39e5acfc5f60f57d8accde65f46cc5e74"}, + {file = "frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:acd03d224b0175f5a850edc104ac19040d35419eddad04e7cf2d5986d98427f1"}, + {file = "frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2038310bc582f3d6a09b3816ab01737d60bf7b1ec70f5356b09e84fb7408ab1"}, + {file = "frozenlist-1.7.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b8c05e4c8e5f36e5e088caa1bf78a687528f83c043706640a92cb76cd6999384"}, + {file = "frozenlist-1.7.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:765bb588c86e47d0b68f23c1bee323d4b703218037765dcf3f25c838c6fecceb"}, + {file = "frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:32dc2e08c67d86d0969714dd484fd60ff08ff81d1a1e40a77dd34a387e6ebc0c"}, + {file = "frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:c0303e597eb5a5321b4de9c68e9845ac8f290d2ab3f3e2c864437d3c5a30cd65"}, + {file = "frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:a47f2abb4e29b3a8d0b530f7c3598badc6b134562b1a5caee867f7c62fee51e3"}, + {file = "frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:3d688126c242a6fabbd92e02633414d40f50bb6002fa4cf995a1d18051525657"}, + {file = "frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:4e7e9652b3d367c7bd449a727dc79d5043f48b88d0cbfd4f9f1060cf2b414104"}, + {file = "frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1a85e345b4c43db8b842cab1feb41be5cc0b10a1830e6295b69d7310f99becaf"}, + {file = "frozenlist-1.7.0-cp313-cp313t-win32.whl", hash = "sha256:3a14027124ddb70dfcee5148979998066897e79f89f64b13328595c4bdf77c81"}, + {file = "frozenlist-1.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3bf8010d71d4507775f658e9823210b7427be36625b387221642725b515dcf3e"}, + {file = "frozenlist-1.7.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:cea3dbd15aea1341ea2de490574a4a37ca080b2ae24e4b4f4b51b9057b4c3630"}, + {file = "frozenlist-1.7.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7d536ee086b23fecc36c2073c371572374ff50ef4db515e4e503925361c24f71"}, + {file = "frozenlist-1.7.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dfcebf56f703cb2e346315431699f00db126d158455e513bd14089d992101e44"}, + {file = "frozenlist-1.7.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:974c5336e61d6e7eb1ea5b929cb645e882aadab0095c5a6974a111e6479f8878"}, + {file = "frozenlist-1.7.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c70db4a0ab5ab20878432c40563573229a7ed9241506181bba12f6b7d0dc41cb"}, + {file = "frozenlist-1.7.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1137b78384eebaf70560a36b7b229f752fb64d463d38d1304939984d5cb887b6"}, + {file = "frozenlist-1.7.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e793a9f01b3e8b5c0bc646fb59140ce0efcc580d22a3468d70766091beb81b35"}, + {file = "frozenlist-1.7.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:74739ba8e4e38221d2c5c03d90a7e542cb8ad681915f4ca8f68d04f810ee0a87"}, + {file = "frozenlist-1.7.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e63344c4e929b1a01e29bc184bbb5fd82954869033765bfe8d65d09e336a677"}, + {file = "frozenlist-1.7.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2ea2a7369eb76de2217a842f22087913cdf75f63cf1307b9024ab82dfb525938"}, + {file = "frozenlist-1.7.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:836b42f472a0e006e02499cef9352ce8097f33df43baaba3e0a28a964c26c7d2"}, + {file = "frozenlist-1.7.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:e22b9a99741294b2571667c07d9f8cceec07cb92aae5ccda39ea1b6052ed4319"}, + {file = "frozenlist-1.7.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:9a19e85cc503d958abe5218953df722748d87172f71b73cf3c9257a91b999890"}, + {file = "frozenlist-1.7.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:f22dac33bb3ee8fe3e013aa7b91dc12f60d61d05b7fe32191ffa84c3aafe77bd"}, + {file = "frozenlist-1.7.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9ccec739a99e4ccf664ea0775149f2749b8a6418eb5b8384b4dc0a7d15d304cb"}, + {file = "frozenlist-1.7.0-cp39-cp39-win32.whl", hash = "sha256:b3950f11058310008a87757f3eee16a8e1ca97979833239439586857bc25482e"}, + {file = "frozenlist-1.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:43a82fce6769c70f2f5a06248b614a7d268080a9d20f7457ef10ecee5af82b63"}, + {file = "frozenlist-1.7.0-py3-none-any.whl", hash = "sha256:9a5af342e34f7e97caf8c995864c7a396418ae2859cc6fdf1b1073020d516a7e"}, + {file = "frozenlist-1.7.0.tar.gz", hash = "sha256:2e310d81923c2437ea8670467121cc3e9b0f76d3043cc1d2331d56c7fb7a3a8f"}, +] + +[[package]] +name = "fsspec" +version = "2025.5.1" +description = "File-system specification" +optional = false +python-versions = ">=3.9" +files = [ + {file = "fsspec-2025.5.1-py3-none-any.whl", hash = "sha256:24d3a2e663d5fc735ab256263c4075f374a174c3410c0b25e5bd1970bceaa462"}, + {file = "fsspec-2025.5.1.tar.gz", hash = "sha256:2e55e47a540b91843b755e83ded97c6e897fa0942b11490113f09e9c443c2475"}, +] + +[package.extras] +abfs = ["adlfs"] +adl = ["adlfs"] +arrow = ["pyarrow (>=1)"] +dask = ["dask", "distributed"] +dev = ["pre-commit", "ruff"] +doc = ["numpydoc", "sphinx", "sphinx-design", "sphinx-rtd-theme", "yarl"] +dropbox = ["dropbox", "dropboxdrivefs", "requests"] +full = ["adlfs", "aiohttp (!=4.0.0a0,!=4.0.0a1)", "dask", "distributed", "dropbox", "dropboxdrivefs", "fusepy", "gcsfs", "libarchive-c", "ocifs", "panel", "paramiko", "pyarrow (>=1)", "pygit2", "requests", "s3fs", "smbprotocol", "tqdm"] +fuse = ["fusepy"] +gcs = ["gcsfs"] +git = ["pygit2"] +github = ["requests"] +gs = ["gcsfs"] +gui = ["panel"] +hdfs = ["pyarrow (>=1)"] +http = ["aiohttp (!=4.0.0a0,!=4.0.0a1)"] +libarchive = ["libarchive-c"] +oci = ["ocifs"] +s3 = ["s3fs"] +sftp = ["paramiko"] +smb = ["smbprotocol"] +ssh = ["paramiko"] +test = ["aiohttp (!=4.0.0a0,!=4.0.0a1)", "numpy", "pytest", "pytest-asyncio (!=0.22.0)", "pytest-benchmark", "pytest-cov", "pytest-mock", "pytest-recording", "pytest-rerunfailures", "requests"] +test-downstream = ["aiobotocore (>=2.5.4,<3.0.0)", "dask[dataframe,test]", "moto[server] (>4,<5)", "pytest-timeout", "xarray"] +test-full = ["adlfs", "aiohttp (!=4.0.0a0,!=4.0.0a1)", "cloudpickle", "dask", "distributed", "dropbox", "dropboxdrivefs", "fastparquet", "fusepy", "gcsfs", "jinja2", "kerchunk", "libarchive-c", "lz4", "notebook", "numpy", "ocifs", "pandas", "panel", "paramiko", "pyarrow", "pyarrow (>=1)", "pyftpdlib", "pygit2", "pytest", "pytest-asyncio (!=0.22.0)", "pytest-benchmark", "pytest-cov", "pytest-mock", "pytest-recording", "pytest-rerunfailures", "python-snappy", "requests", "smbprotocol", "tqdm", "urllib3", "zarr", "zstandard"] +tqdm = ["tqdm"] + +[[package]] +name = "google-auth" +version = "2.40.3" +description = "Google Authentication Library" +optional = false +python-versions = ">=3.7" +files = [ + {file = "google_auth-2.40.3-py2.py3-none-any.whl", hash = "sha256:1370d4593e86213563547f97a92752fc658456fe4514c809544f330fed45a7ca"}, + {file = "google_auth-2.40.3.tar.gz", hash = "sha256:500c3a29adedeb36ea9cf24b8d10858e152f2412e3ca37829b3fa18e33d63b77"}, +] + +[package.dependencies] +cachetools = ">=2.0.0,<6.0" +pyasn1-modules = ">=0.2.1" +rsa = ">=3.1.4,<5" + +[package.extras] +aiohttp = ["aiohttp (>=3.6.2,<4.0.0)", "requests (>=2.20.0,<3.0.0)"] +enterprise-cert = ["cryptography", "pyopenssl"] +pyjwt = ["cryptography (<39.0.0)", "cryptography (>=38.0.3)", "pyjwt (>=2.0)"] +pyopenssl = ["cryptography (<39.0.0)", "cryptography (>=38.0.3)", "pyopenssl (>=20.0.0)"] +reauth = ["pyu2f (>=0.1.5)"] +requests = ["requests (>=2.20.0,<3.0.0)"] +testing = ["aiohttp (<3.10.0)", "aiohttp (>=3.6.2,<4.0.0)", "aioresponses", "cryptography (<39.0.0)", "cryptography (>=38.0.3)", "flask", "freezegun", "grpcio", "mock", "oauth2client", "packaging", "pyjwt (>=2.0)", "pyopenssl (<24.3.0)", "pyopenssl (>=20.0.0)", "pytest", "pytest-asyncio", "pytest-cov", "pytest-localserver", "pyu2f (>=0.1.5)", "requests (>=2.20.0,<3.0.0)", "responses", "urllib3"] +urllib3 = ["packaging", "urllib3"] + +[[package]] +name = "googleapis-common-protos" +version = "1.70.0" +description = "Common protobufs used in Google APIs" +optional = false +python-versions = ">=3.7" +files = [ + {file = "googleapis_common_protos-1.70.0-py3-none-any.whl", hash = "sha256:b8bfcca8c25a2bb253e0e0b0adaf8c00773e5e6af6fd92397576680b807e0fd8"}, + {file = "googleapis_common_protos-1.70.0.tar.gz", hash = "sha256:0e1b44e0ea153e6594f9f394fef15193a68aaaea2d843f83e2742717ca753257"}, +] + +[package.dependencies] +protobuf = ">=3.20.2,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0" + +[package.extras] +grpc = ["grpcio (>=1.44.0,<2.0.0)"] + +[[package]] +name = "greenlet" +version = "3.2.3" +description = "Lightweight in-process concurrent programming" +optional = false +python-versions = ">=3.9" +files = [ + {file = "greenlet-3.2.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:1afd685acd5597349ee6d7a88a8bec83ce13c106ac78c196ee9dde7c04fe87be"}, + {file = "greenlet-3.2.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:761917cac215c61e9dc7324b2606107b3b292a8349bdebb31503ab4de3f559ac"}, + {file = "greenlet-3.2.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:a433dbc54e4a37e4fff90ef34f25a8c00aed99b06856f0119dcf09fbafa16392"}, + {file = "greenlet-3.2.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:72e77ed69312bab0434d7292316d5afd6896192ac4327d44f3d613ecb85b037c"}, + {file = "greenlet-3.2.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:68671180e3849b963649254a882cd544a3c75bfcd2c527346ad8bb53494444db"}, + {file = "greenlet-3.2.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:49c8cfb18fb419b3d08e011228ef8a25882397f3a859b9fe1436946140b6756b"}, + {file = "greenlet-3.2.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:efc6dc8a792243c31f2f5674b670b3a95d46fa1c6a912b8e310d6f542e7b0712"}, + {file = "greenlet-3.2.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:731e154aba8e757aedd0781d4b240f1225b075b4409f1bb83b05ff410582cf00"}, + {file = "greenlet-3.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:96c20252c2f792defe9a115d3287e14811036d51e78b3aaddbee23b69b216302"}, + {file = "greenlet-3.2.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:784ae58bba89fa1fa5733d170d42486580cab9decda3484779f4759345b29822"}, + {file = "greenlet-3.2.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0921ac4ea42a5315d3446120ad48f90c3a6b9bb93dd9b3cf4e4d84a66e42de83"}, + {file = "greenlet-3.2.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:d2971d93bb99e05f8c2c0c2f4aa9484a18d98c4c3bd3c62b65b7e6ae33dfcfaf"}, + {file = "greenlet-3.2.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c667c0bf9d406b77a15c924ef3285e1e05250948001220368e039b6aa5b5034b"}, + {file = "greenlet-3.2.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:592c12fb1165be74592f5de0d70f82bc5ba552ac44800d632214b76089945147"}, + {file = "greenlet-3.2.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29e184536ba333003540790ba29829ac14bb645514fbd7e32af331e8202a62a5"}, + {file = "greenlet-3.2.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:93c0bb79844a367782ec4f429d07589417052e621aa39a5ac1fb99c5aa308edc"}, + {file = "greenlet-3.2.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:751261fc5ad7b6705f5f76726567375bb2104a059454e0226e1eef6c756748ba"}, + {file = "greenlet-3.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:83a8761c75312361aa2b5b903b79da97f13f556164a7dd2d5448655425bd4c34"}, + {file = "greenlet-3.2.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:25ad29caed5783d4bd7a85c9251c651696164622494c00802a139c00d639242d"}, + {file = "greenlet-3.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88cd97bf37fe24a6710ec6a3a7799f3f81d9cd33317dcf565ff9950c83f55e0b"}, + {file = "greenlet-3.2.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:baeedccca94880d2f5666b4fa16fc20ef50ba1ee353ee2d7092b383a243b0b0d"}, + {file = "greenlet-3.2.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:be52af4b6292baecfa0f397f3edb3c6092ce071b499dd6fe292c9ac9f2c8f264"}, + {file = "greenlet-3.2.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0cc73378150b8b78b0c9fe2ce56e166695e67478550769536a6742dca3651688"}, + {file = "greenlet-3.2.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:706d016a03e78df129f68c4c9b4c4f963f7d73534e48a24f5f5a7101ed13dbbb"}, + {file = "greenlet-3.2.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:419e60f80709510c343c57b4bb5a339d8767bf9aef9b8ce43f4f143240f88b7c"}, + {file = "greenlet-3.2.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:93d48533fade144203816783373f27a97e4193177ebaaf0fc396db19e5d61163"}, + {file = "greenlet-3.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:7454d37c740bb27bdeddfc3f358f26956a07d5220818ceb467a483197d84f849"}, + {file = "greenlet-3.2.3-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:500b8689aa9dd1ab26872a34084503aeddefcb438e2e7317b89b11eaea1901ad"}, + {file = "greenlet-3.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a07d3472c2a93117af3b0136f246b2833fdc0b542d4a9799ae5f41c28323faef"}, + {file = "greenlet-3.2.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:8704b3768d2f51150626962f4b9a9e4a17d2e37c8a8d9867bbd9fa4eb938d3b3"}, + {file = "greenlet-3.2.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:5035d77a27b7c62db6cf41cf786cfe2242644a7a337a0e155c80960598baab95"}, + {file = "greenlet-3.2.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2d8aa5423cd4a396792f6d4580f88bdc6efcb9205891c9d40d20f6e670992efb"}, + {file = "greenlet-3.2.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2c724620a101f8170065d7dded3f962a2aea7a7dae133a009cada42847e04a7b"}, + {file = "greenlet-3.2.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:873abe55f134c48e1f2a6f53f7d1419192a3d1a4e873bace00499a4e45ea6af0"}, + {file = "greenlet-3.2.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:024571bbce5f2c1cfff08bf3fbaa43bbc7444f580ae13b0099e95d0e6e67ed36"}, + {file = "greenlet-3.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:5195fb1e75e592dd04ce79881c8a22becdfa3e6f500e7feb059b1e6fdd54d3e3"}, + {file = "greenlet-3.2.3-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:3d04332dddb10b4a211b68111dabaee2e1a073663d117dc10247b5b1642bac86"}, + {file = "greenlet-3.2.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8186162dffde068a465deab08fc72c767196895c39db26ab1c17c0b77a6d8b97"}, + {file = "greenlet-3.2.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f4bfbaa6096b1b7a200024784217defedf46a07c2eee1a498e94a1b5f8ec5728"}, + {file = "greenlet-3.2.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:ed6cfa9200484d234d8394c70f5492f144b20d4533f69262d530a1a082f6ee9a"}, + {file = "greenlet-3.2.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:02b0df6f63cd15012bed5401b47829cfd2e97052dc89da3cfaf2c779124eb892"}, + {file = "greenlet-3.2.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:86c2d68e87107c1792e2e8d5399acec2487a4e993ab76c792408e59394d52141"}, + {file = "greenlet-3.2.3-cp314-cp314-win_amd64.whl", hash = "sha256:8c47aae8fbbfcf82cc13327ae802ba13c9c36753b67e760023fd116bc124a62a"}, + {file = "greenlet-3.2.3-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:42efc522c0bd75ffa11a71e09cd8a399d83fafe36db250a87cf1dacfaa15dc64"}, + {file = "greenlet-3.2.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d760f9bdfe79bff803bad32b4d8ffb2c1d2ce906313fc10a83976ffb73d64ca7"}, + {file = "greenlet-3.2.3-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:8324319cbd7b35b97990090808fdc99c27fe5338f87db50514959f8059999805"}, + {file = "greenlet-3.2.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:8c37ef5b3787567d322331d5250e44e42b58c8c713859b8a04c6065f27efbf72"}, + {file = "greenlet-3.2.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ce539fb52fb774d0802175d37fcff5c723e2c7d249c65916257f0a940cee8904"}, + {file = "greenlet-3.2.3-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:003c930e0e074db83559edc8705f3a2d066d4aa8c2f198aff1e454946efd0f26"}, + {file = "greenlet-3.2.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7e70ea4384b81ef9e84192e8a77fb87573138aa5d4feee541d8014e452b434da"}, + {file = "greenlet-3.2.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:22eb5ba839c4b2156f18f76768233fe44b23a31decd9cc0d4cc8141c211fd1b4"}, + {file = "greenlet-3.2.3-cp39-cp39-win32.whl", hash = "sha256:4532f0d25df67f896d137431b13f4cdce89f7e3d4a96387a41290910df4d3a57"}, + {file = "greenlet-3.2.3-cp39-cp39-win_amd64.whl", hash = "sha256:aaa7aae1e7f75eaa3ae400ad98f8644bb81e1dc6ba47ce8a93d3f17274e08322"}, + {file = "greenlet-3.2.3.tar.gz", hash = "sha256:8b0dd8ae4c0d6f5e54ee55ba935eeb3d735a9b58a8a1e5b5cbab64e01a39f365"}, +] + +[package.extras] +docs = ["Sphinx", "furo"] +test = ["objgraph", "psutil"] + +[[package]] +name = "grpcio" +version = "1.73.0" +description = "HTTP/2-based RPC framework" +optional = false +python-versions = ">=3.9" +files = [ + {file = "grpcio-1.73.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:d050197eeed50f858ef6c51ab09514856f957dba7b1f7812698260fc9cc417f6"}, + {file = "grpcio-1.73.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:ebb8d5f4b0200916fb292a964a4d41210de92aba9007e33d8551d85800ea16cb"}, + {file = "grpcio-1.73.0-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:c0811331b469e3f15dda5f90ab71bcd9681189a83944fd6dc908e2c9249041ef"}, + {file = "grpcio-1.73.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:12787c791c3993d0ea1cc8bf90393647e9a586066b3b322949365d2772ba965b"}, + {file = "grpcio-1.73.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c17771e884fddf152f2a0df12478e8d02853e5b602a10a9a9f1f52fa02b1d32"}, + {file = "grpcio-1.73.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:275e23d4c428c26b51857bbd95fcb8e528783597207ec592571e4372b300a29f"}, + {file = "grpcio-1.73.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:9ffc972b530bf73ef0f948f799482a1bf12d9b6f33406a8e6387c0ca2098a833"}, + {file = "grpcio-1.73.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ebd8d269df64aff092b2cec5e015d8ae09c7e90888b5c35c24fdca719a2c9f35"}, + {file = "grpcio-1.73.0-cp310-cp310-win32.whl", hash = "sha256:072d8154b8f74300ed362c01d54af8b93200c1a9077aeaea79828d48598514f1"}, + {file = "grpcio-1.73.0-cp310-cp310-win_amd64.whl", hash = "sha256:ce953d9d2100e1078a76a9dc2b7338d5415924dc59c69a15bf6e734db8a0f1ca"}, + {file = "grpcio-1.73.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:51036f641f171eebe5fa7aaca5abbd6150f0c338dab3a58f9111354240fe36ec"}, + {file = "grpcio-1.73.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:d12bbb88381ea00bdd92c55aff3da3391fd85bc902c41275c8447b86f036ce0f"}, + {file = "grpcio-1.73.0-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:483c507c2328ed0e01bc1adb13d1eada05cc737ec301d8e5a8f4a90f387f1790"}, + {file = "grpcio-1.73.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c201a34aa960c962d0ce23fe5f423f97e9d4b518ad605eae6d0a82171809caaa"}, + {file = "grpcio-1.73.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:859f70c8e435e8e1fa060e04297c6818ffc81ca9ebd4940e180490958229a45a"}, + {file = "grpcio-1.73.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e2459a27c6886e7e687e4e407778425f3c6a971fa17a16420227bda39574d64b"}, + {file = "grpcio-1.73.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:e0084d4559ee3dbdcce9395e1bc90fdd0262529b32c417a39ecbc18da8074ac7"}, + {file = "grpcio-1.73.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ef5fff73d5f724755693a464d444ee0a448c6cdfd3c1616a9223f736c622617d"}, + {file = "grpcio-1.73.0-cp311-cp311-win32.whl", hash = "sha256:965a16b71a8eeef91fc4df1dc40dc39c344887249174053814f8a8e18449c4c3"}, + {file = "grpcio-1.73.0-cp311-cp311-win_amd64.whl", hash = "sha256:b71a7b4483d1f753bbc11089ff0f6fa63b49c97a9cc20552cded3fcad466d23b"}, + {file = "grpcio-1.73.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:fb9d7c27089d9ba3746f18d2109eb530ef2a37452d2ff50f5a6696cd39167d3b"}, + {file = "grpcio-1.73.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:128ba2ebdac41e41554d492b82c34586a90ebd0766f8ebd72160c0e3a57b9155"}, + {file = "grpcio-1.73.0-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:068ecc415f79408d57a7f146f54cdf9f0acb4b301a52a9e563973dc981e82f3d"}, + {file = "grpcio-1.73.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ddc1cfb2240f84d35d559ade18f69dcd4257dbaa5ba0de1a565d903aaab2968"}, + {file = "grpcio-1.73.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e53007f70d9783f53b41b4cf38ed39a8e348011437e4c287eee7dd1d39d54b2f"}, + {file = "grpcio-1.73.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4dd8d8d092efede7d6f48d695ba2592046acd04ccf421436dd7ed52677a9ad29"}, + {file = "grpcio-1.73.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:70176093d0a95b44d24baa9c034bb67bfe2b6b5f7ebc2836f4093c97010e17fd"}, + {file = "grpcio-1.73.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:085ebe876373ca095e24ced95c8f440495ed0b574c491f7f4f714ff794bbcd10"}, + {file = "grpcio-1.73.0-cp312-cp312-win32.whl", hash = "sha256:cfc556c1d6aef02c727ec7d0016827a73bfe67193e47c546f7cadd3ee6bf1a60"}, + {file = "grpcio-1.73.0-cp312-cp312-win_amd64.whl", hash = "sha256:bbf45d59d090bf69f1e4e1594832aaf40aa84b31659af3c5e2c3f6a35202791a"}, + {file = "grpcio-1.73.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:da1d677018ef423202aca6d73a8d3b2cb245699eb7f50eb5f74cae15a8e1f724"}, + {file = "grpcio-1.73.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:36bf93f6a657f37c131d9dd2c391b867abf1426a86727c3575393e9e11dadb0d"}, + {file = "grpcio-1.73.0-cp313-cp313-manylinux_2_17_aarch64.whl", hash = "sha256:d84000367508ade791d90c2bafbd905574b5ced8056397027a77a215d601ba15"}, + {file = "grpcio-1.73.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c98ba1d928a178ce33f3425ff823318040a2b7ef875d30a0073565e5ceb058d9"}, + {file = "grpcio-1.73.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a73c72922dfd30b396a5f25bb3a4590195ee45ecde7ee068acb0892d2900cf07"}, + {file = "grpcio-1.73.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:10e8edc035724aba0346a432060fd192b42bd03675d083c01553cab071a28da5"}, + {file = "grpcio-1.73.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:f5cdc332b503c33b1643b12ea933582c7b081957c8bc2ea4cc4bc58054a09288"}, + {file = "grpcio-1.73.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:07ad7c57233c2109e4ac999cb9c2710c3b8e3f491a73b058b0ce431f31ed8145"}, + {file = "grpcio-1.73.0-cp313-cp313-win32.whl", hash = "sha256:0eb5df4f41ea10bda99a802b2a292d85be28958ede2a50f2beb8c7fc9a738419"}, + {file = "grpcio-1.73.0-cp313-cp313-win_amd64.whl", hash = "sha256:38cf518cc54cd0c47c9539cefa8888549fcc067db0b0c66a46535ca8032020c4"}, + {file = "grpcio-1.73.0-cp39-cp39-linux_armv7l.whl", hash = "sha256:1284850607901cfe1475852d808e5a102133461ec9380bc3fc9ebc0686ee8e32"}, + {file = "grpcio-1.73.0-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:0e092a4b28eefb63eec00d09ef33291cd4c3a0875cde29aec4d11d74434d222c"}, + {file = "grpcio-1.73.0-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:33577fe7febffe8ebad458744cfee8914e0c10b09f0ff073a6b149a84df8ab8f"}, + {file = "grpcio-1.73.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:60813d8a16420d01fa0da1fc7ebfaaa49a7e5051b0337cd48f4f950eb249a08e"}, + {file = "grpcio-1.73.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a9c957dc65e5d474378d7bcc557e9184576605d4b4539e8ead6e351d7ccce20"}, + {file = "grpcio-1.73.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3902b71407d021163ea93c70c8531551f71ae742db15b66826cf8825707d2908"}, + {file = "grpcio-1.73.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:1dd7fa7276dcf061e2d5f9316604499eea06b1b23e34a9380572d74fe59915a8"}, + {file = "grpcio-1.73.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2d1510c4ea473110cb46a010555f2c1a279d1c256edb276e17fa571ba1e8927c"}, + {file = "grpcio-1.73.0-cp39-cp39-win32.whl", hash = "sha256:d0a1517b2005ba1235a1190b98509264bf72e231215dfeef8db9a5a92868789e"}, + {file = "grpcio-1.73.0-cp39-cp39-win_amd64.whl", hash = "sha256:6228f7eb6d9f785f38b589d49957fca5df3d5b5349e77d2d89b14e390165344c"}, + {file = "grpcio-1.73.0.tar.gz", hash = "sha256:3af4c30918a7f0d39de500d11255f8d9da4f30e94a2033e70fe2a720e184bd8e"}, +] + +[package.extras] +protobuf = ["grpcio-tools (>=1.73.0)"] + +[[package]] +name = "h11" +version = "0.16.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.8" +files = [ + {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, + {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, +] + +[[package]] +name = "hf-xet" +version = "1.1.3" +description = "Fast transfer of large files with the Hugging Face Hub." +optional = false +python-versions = ">=3.8" +files = [ + {file = "hf_xet-1.1.3-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c3b508b5f583a75641aebf732853deb058953370ce8184f5dabc49f803b0819b"}, + {file = "hf_xet-1.1.3-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:b788a61977fbe6b5186e66239e2a329a3f0b7e7ff50dad38984c0c74f44aeca1"}, + {file = "hf_xet-1.1.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd2da210856444a34aad8ada2fc12f70dabed7cc20f37e90754d1d9b43bc0534"}, + {file = "hf_xet-1.1.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:8203f52827e3df65981984936654a5b390566336956f65765a8aa58c362bb841"}, + {file = "hf_xet-1.1.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:30c575a5306f8e6fda37edb866762140a435037365eba7a17ce7bd0bc0216a8b"}, + {file = "hf_xet-1.1.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7c1a6aa6abed1f696f8099aa9796ca04c9ee778a58728a115607de9cc4638ff1"}, + {file = "hf_xet-1.1.3-cp37-abi3-win_amd64.whl", hash = "sha256:b578ae5ac9c056296bb0df9d018e597c8dc6390c5266f35b5c44696003cde9f3"}, + {file = "hf_xet-1.1.3.tar.gz", hash = "sha256:a5f09b1dd24e6ff6bcedb4b0ddab2d81824098bb002cf8b4ffa780545fa348c3"}, +] + +[package.extras] +tests = ["pytest"] + +[[package]] +name = "hnswlib" +version = "0.8.0" +description = "hnswlib" +optional = false +python-versions = "*" +files = [ + {file = "hnswlib-0.8.0.tar.gz", hash = "sha256:cb6d037eedebb34a7134e7dc78966441dfd04c9cf5ee93911be911ced951c44c"}, +] + +[package.dependencies] +numpy = "*" + +[[package]] +name = "httpcore" +version = "1.0.9" +description = "A minimal low-level HTTP client." +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}, + {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"}, +] + +[package.dependencies] +certifi = "*" +h11 = ">=0.16" + +[package.extras] +asyncio = ["anyio (>=4.0,<5.0)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +trio = ["trio (>=0.22.0,<1.0)"] + +[[package]] +name = "httptools" +version = "0.6.4" +description = "A collection of framework independent HTTP protocol utils." +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "httptools-0.6.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3c73ce323711a6ffb0d247dcd5a550b8babf0f757e86a52558fe5b86d6fefcc0"}, + {file = "httptools-0.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:345c288418f0944a6fe67be8e6afa9262b18c7626c3ef3c28adc5eabc06a68da"}, + {file = "httptools-0.6.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:deee0e3343f98ee8047e9f4c5bc7cedbf69f5734454a94c38ee829fb2d5fa3c1"}, + {file = "httptools-0.6.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca80b7485c76f768a3bc83ea58373f8db7b015551117375e4918e2aa77ea9b50"}, + {file = "httptools-0.6.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:90d96a385fa941283ebd231464045187a31ad932ebfa541be8edf5b3c2328959"}, + {file = "httptools-0.6.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:59e724f8b332319e2875efd360e61ac07f33b492889284a3e05e6d13746876f4"}, + {file = "httptools-0.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:c26f313951f6e26147833fc923f78f95604bbec812a43e5ee37f26dc9e5a686c"}, + {file = "httptools-0.6.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f47f8ed67cc0ff862b84a1189831d1d33c963fb3ce1ee0c65d3b0cbe7b711069"}, + {file = "httptools-0.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0614154d5454c21b6410fdf5262b4a3ddb0f53f1e1721cfd59d55f32138c578a"}, + {file = "httptools-0.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8787367fbdfccae38e35abf7641dafc5310310a5987b689f4c32cc8cc3ee975"}, + {file = "httptools-0.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40b0f7fe4fd38e6a507bdb751db0379df1e99120c65fbdc8ee6c1d044897a636"}, + {file = "httptools-0.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40a5ec98d3f49904b9fe36827dcf1aadfef3b89e2bd05b0e35e94f97c2b14721"}, + {file = "httptools-0.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dacdd3d10ea1b4ca9df97a0a303cbacafc04b5cd375fa98732678151643d4988"}, + {file = "httptools-0.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:288cd628406cc53f9a541cfaf06041b4c71d751856bab45e3702191f931ccd17"}, + {file = "httptools-0.6.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:df017d6c780287d5c80601dafa31f17bddb170232d85c066604d8558683711a2"}, + {file = "httptools-0.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:85071a1e8c2d051b507161f6c3e26155b5c790e4e28d7f236422dbacc2a9cc44"}, + {file = "httptools-0.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69422b7f458c5af875922cdb5bd586cc1f1033295aa9ff63ee196a87519ac8e1"}, + {file = "httptools-0.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16e603a3bff50db08cd578d54f07032ca1631450ceb972c2f834c2b860c28ea2"}, + {file = "httptools-0.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec4f178901fa1834d4a060320d2f3abc5c9e39766953d038f1458cb885f47e81"}, + {file = "httptools-0.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f9eb89ecf8b290f2e293325c646a211ff1c2493222798bb80a530c5e7502494f"}, + {file = "httptools-0.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:db78cb9ca56b59b016e64b6031eda5653be0589dba2b1b43453f6e8b405a0970"}, + {file = "httptools-0.6.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ade273d7e767d5fae13fa637f4d53b6e961fb7fd93c7797562663f0171c26660"}, + {file = "httptools-0.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:856f4bc0478ae143bad54a4242fccb1f3f86a6e1be5548fecfd4102061b3a083"}, + {file = "httptools-0.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:322d20ea9cdd1fa98bd6a74b77e2ec5b818abdc3d36695ab402a0de8ef2865a3"}, + {file = "httptools-0.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d87b29bd4486c0093fc64dea80231f7c7f7eb4dc70ae394d70a495ab8436071"}, + {file = "httptools-0.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:342dd6946aa6bda4b8f18c734576106b8a31f2fe31492881a9a160ec84ff4bd5"}, + {file = "httptools-0.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b36913ba52008249223042dca46e69967985fb4051951f94357ea681e1f5dc0"}, + {file = "httptools-0.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:28908df1b9bb8187393d5b5db91435ccc9c8e891657f9cbb42a2541b44c82fc8"}, + {file = "httptools-0.6.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:d3f0d369e7ffbe59c4b6116a44d6a8eb4783aae027f2c0b366cf0aa964185dba"}, + {file = "httptools-0.6.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:94978a49b8f4569ad607cd4946b759d90b285e39c0d4640c6b36ca7a3ddf2efc"}, + {file = "httptools-0.6.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40dc6a8e399e15ea525305a2ddba998b0af5caa2566bcd79dcbe8948181eeaff"}, + {file = "httptools-0.6.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab9ba8dcf59de5181f6be44a77458e45a578fc99c31510b8c65b7d5acc3cf490"}, + {file = "httptools-0.6.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:fc411e1c0a7dcd2f902c7c48cf079947a7e65b5485dea9decb82b9105ca71a43"}, + {file = "httptools-0.6.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:d54efd20338ac52ba31e7da78e4a72570cf729fac82bc31ff9199bedf1dc7440"}, + {file = "httptools-0.6.4-cp38-cp38-win_amd64.whl", hash = "sha256:df959752a0c2748a65ab5387d08287abf6779ae9165916fe053e68ae1fbdc47f"}, + {file = "httptools-0.6.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:85797e37e8eeaa5439d33e556662cc370e474445d5fab24dcadc65a8ffb04003"}, + {file = "httptools-0.6.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:db353d22843cf1028f43c3651581e4bb49374d85692a85f95f7b9a130e1b2cab"}, + {file = "httptools-0.6.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1ffd262a73d7c28424252381a5b854c19d9de5f56f075445d33919a637e3547"}, + {file = "httptools-0.6.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:703c346571fa50d2e9856a37d7cd9435a25e7fd15e236c397bf224afaa355fe9"}, + {file = "httptools-0.6.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:aafe0f1918ed07b67c1e838f950b1c1fabc683030477e60b335649b8020e1076"}, + {file = "httptools-0.6.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:0e563e54979e97b6d13f1bbc05a96109923e76b901f786a5eae36e99c01237bd"}, + {file = "httptools-0.6.4-cp39-cp39-win_amd64.whl", hash = "sha256:b799de31416ecc589ad79dd85a0b2657a8fe39327944998dea368c1d4c9e55e6"}, + {file = "httptools-0.6.4.tar.gz", hash = "sha256:4e93eee4add6493b59a5c514da98c939b244fce4a0d8879cd3f466562f4b7d5c"}, +] + +[package.extras] +test = ["Cython (>=0.29.24)"] + +[[package]] +name = "httpx" +version = "0.28.1" +description = "The next generation HTTP client." +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, + {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, +] + +[package.dependencies] +anyio = "*" +certifi = "*" +httpcore = "==1.*" +idna = "*" + +[package.extras] +brotli = ["brotli", "brotlicffi"] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "huggingface-hub" +version = "0.33.0" +description = "Client library to download and publish models, datasets and other repos on the huggingface.co hub" +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "huggingface_hub-0.33.0-py3-none-any.whl", hash = "sha256:e8668875b40c68f9929150d99727d39e5ebb8a05a98e4191b908dc7ded9074b3"}, + {file = "huggingface_hub-0.33.0.tar.gz", hash = "sha256:aa31f70d29439d00ff7a33837c03f1f9dd83971ce4e29ad664d63ffb17d3bb97"}, +] + +[package.dependencies] +filelock = "*" +fsspec = ">=2023.5.0" +hf-xet = {version = ">=1.1.2,<2.0.0", markers = "platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"arm64\" or platform_machine == \"aarch64\""} +packaging = ">=20.9" +pyyaml = ">=5.1" +requests = "*" +tqdm = ">=4.42.1" +typing-extensions = ">=3.7.4.3" + +[package.extras] +all = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "authlib (>=1.3.2)", "fastapi", "gradio (>=4.0.0)", "httpx", "itsdangerous", "jedi", "libcst (==1.4.0)", "mypy (==1.15.0)", "mypy (>=1.14.1,<1.15.0)", "numpy", "pytest (>=8.1.1,<8.2.2)", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-mock", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "ruff (>=0.9.0)", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"] +cli = ["InquirerPy (==0.3.4)"] +dev = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "authlib (>=1.3.2)", "fastapi", "gradio (>=4.0.0)", "httpx", "itsdangerous", "jedi", "libcst (==1.4.0)", "mypy (==1.15.0)", "mypy (>=1.14.1,<1.15.0)", "numpy", "pytest (>=8.1.1,<8.2.2)", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-mock", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "ruff (>=0.9.0)", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"] +fastai = ["fastai (>=2.4)", "fastcore (>=1.3.27)", "toml"] +hf-transfer = ["hf-transfer (>=0.1.4)"] +hf-xet = ["hf-xet (>=1.1.2,<2.0.0)"] +inference = ["aiohttp"] +mcp = ["aiohttp", "mcp (>=1.8.0)", "typer"] +oauth = ["authlib (>=1.3.2)", "fastapi", "httpx", "itsdangerous"] +quality = ["libcst (==1.4.0)", "mypy (==1.15.0)", "mypy (>=1.14.1,<1.15.0)", "ruff (>=0.9.0)"] +tensorflow = ["graphviz", "pydot", "tensorflow"] +tensorflow-testing = ["keras (<3.0)", "tensorflow"] +testing = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "authlib (>=1.3.2)", "fastapi", "gradio (>=4.0.0)", "httpx", "itsdangerous", "jedi", "numpy", "pytest (>=8.1.1,<8.2.2)", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-mock", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "soundfile", "urllib3 (<2.0)"] +torch = ["safetensors[torch]", "torch"] +typing = ["types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)"] + +[[package]] +name = "humanfriendly" +version = "10.0" +description = "Human friendly output for text interfaces using Python" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477"}, + {file = "humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc"}, +] + +[package.dependencies] +pyreadline3 = {version = "*", markers = "sys_platform == \"win32\" and python_version >= \"3.8\""} + +[[package]] +name = "idna" +version = "3.10" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.6" +files = [ + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, +] + +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + +[[package]] +name = "importlib-metadata" +version = "8.7.0" +description = "Read metadata from Python packages" +optional = false +python-versions = ">=3.9" +files = [ + {file = "importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd"}, + {file = "importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000"}, +] + +[package.dependencies] +zipp = ">=3.20" + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] +perf = ["ipython"] +test = ["flufl.flake8", "importlib_resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] +type = ["pytest-mypy"] + +[[package]] +name = "importlib-resources" +version = "6.5.2" +description = "Read resources from Python packages" +optional = false +python-versions = ">=3.9" +files = [ + {file = "importlib_resources-6.5.2-py3-none-any.whl", hash = "sha256:789cfdc3ed28c78b67a06acb8126751ced69a3d5f79c095a98298cd8a760ccec"}, + {file = "importlib_resources-6.5.2.tar.gz", hash = "sha256:185f87adef5bcc288449d98fb4fba07cea78bc036455dd44c5fc4a2fe78fed2c"}, +] + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["jaraco.test (>=5.4)", "pytest (>=6,!=8.1.*)", "zipp (>=3.17)"] +type = ["pytest-mypy"] + +[[package]] +name = "jinja2" +version = "3.1.6" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +files = [ + {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, + {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "jiter" +version = "0.10.0" +description = "Fast iterable JSON parser." +optional = false +python-versions = ">=3.9" +files = [ + {file = "jiter-0.10.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:cd2fb72b02478f06a900a5782de2ef47e0396b3e1f7d5aba30daeb1fce66f303"}, + {file = "jiter-0.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:32bb468e3af278f095d3fa5b90314728a6916d89ba3d0ffb726dd9bf7367285e"}, + {file = "jiter-0.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa8b3e0068c26ddedc7abc6fac37da2d0af16b921e288a5a613f4b86f050354f"}, + {file = "jiter-0.10.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:286299b74cc49e25cd42eea19b72aa82c515d2f2ee12d11392c56d8701f52224"}, + {file = "jiter-0.10.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6ed5649ceeaeffc28d87fb012d25a4cd356dcd53eff5acff1f0466b831dda2a7"}, + {file = "jiter-0.10.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2ab0051160cb758a70716448908ef14ad476c3774bd03ddce075f3c1f90a3d6"}, + {file = "jiter-0.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03997d2f37f6b67d2f5c475da4412be584e1cec273c1cfc03d642c46db43f8cf"}, + {file = "jiter-0.10.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c404a99352d839fed80d6afd6c1d66071f3bacaaa5c4268983fc10f769112e90"}, + {file = "jiter-0.10.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:66e989410b6666d3ddb27a74c7e50d0829704ede652fd4c858e91f8d64b403d0"}, + {file = "jiter-0.10.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b532d3af9ef4f6374609a3bcb5e05a1951d3bf6190dc6b176fdb277c9bbf15ee"}, + {file = "jiter-0.10.0-cp310-cp310-win32.whl", hash = "sha256:da9be20b333970e28b72edc4dff63d4fec3398e05770fb3205f7fb460eb48dd4"}, + {file = "jiter-0.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:f59e533afed0c5b0ac3eba20d2548c4a550336d8282ee69eb07b37ea526ee4e5"}, + {file = "jiter-0.10.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:3bebe0c558e19902c96e99217e0b8e8b17d570906e72ed8a87170bc290b1e978"}, + {file = "jiter-0.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:558cc7e44fd8e507a236bee6a02fa17199ba752874400a0ca6cd6e2196cdb7dc"}, + {file = "jiter-0.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d613e4b379a07d7c8453c5712ce7014e86c6ac93d990a0b8e7377e18505e98d"}, + {file = "jiter-0.10.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f62cf8ba0618eda841b9bf61797f21c5ebd15a7a1e19daab76e4e4b498d515b2"}, + {file = "jiter-0.10.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:919d139cdfa8ae8945112398511cb7fca58a77382617d279556b344867a37e61"}, + {file = "jiter-0.10.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:13ddbc6ae311175a3b03bd8994881bc4635c923754932918e18da841632349db"}, + {file = "jiter-0.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c440ea003ad10927a30521a9062ce10b5479592e8a70da27f21eeb457b4a9c5"}, + {file = "jiter-0.10.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dc347c87944983481e138dea467c0551080c86b9d21de6ea9306efb12ca8f606"}, + {file = "jiter-0.10.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:13252b58c1f4d8c5b63ab103c03d909e8e1e7842d302473f482915d95fefd605"}, + {file = "jiter-0.10.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7d1bbf3c465de4a24ab12fb7766a0003f6f9bce48b8b6a886158c4d569452dc5"}, + {file = "jiter-0.10.0-cp311-cp311-win32.whl", hash = "sha256:db16e4848b7e826edca4ccdd5b145939758dadf0dc06e7007ad0e9cfb5928ae7"}, + {file = "jiter-0.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:9c9c1d5f10e18909e993f9641f12fe1c77b3e9b533ee94ffa970acc14ded3812"}, + {file = "jiter-0.10.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:1e274728e4a5345a6dde2d343c8da018b9d4bd4350f5a472fa91f66fda44911b"}, + {file = "jiter-0.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7202ae396446c988cb2a5feb33a543ab2165b786ac97f53b59aafb803fef0744"}, + {file = "jiter-0.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23ba7722d6748b6920ed02a8f1726fb4b33e0fd2f3f621816a8b486c66410ab2"}, + {file = "jiter-0.10.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:371eab43c0a288537d30e1f0b193bc4eca90439fc08a022dd83e5e07500ed026"}, + {file = "jiter-0.10.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c675736059020365cebc845a820214765162728b51ab1e03a1b7b3abb70f74c"}, + {file = "jiter-0.10.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0c5867d40ab716e4684858e4887489685968a47e3ba222e44cde6e4a2154f959"}, + {file = "jiter-0.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395bb9a26111b60141757d874d27fdea01b17e8fac958b91c20128ba8f4acc8a"}, + {file = "jiter-0.10.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6842184aed5cdb07e0c7e20e5bdcfafe33515ee1741a6835353bb45fe5d1bd95"}, + {file = "jiter-0.10.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:62755d1bcea9876770d4df713d82606c8c1a3dca88ff39046b85a048566d56ea"}, + {file = "jiter-0.10.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:533efbce2cacec78d5ba73a41756beff8431dfa1694b6346ce7af3a12c42202b"}, + {file = "jiter-0.10.0-cp312-cp312-win32.whl", hash = "sha256:8be921f0cadd245e981b964dfbcd6fd4bc4e254cdc069490416dd7a2632ecc01"}, + {file = "jiter-0.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:a7c7d785ae9dda68c2678532a5a1581347e9c15362ae9f6e68f3fdbfb64f2e49"}, + {file = "jiter-0.10.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e0588107ec8e11b6f5ef0e0d656fb2803ac6cf94a96b2b9fc675c0e3ab5e8644"}, + {file = "jiter-0.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cafc4628b616dc32530c20ee53d71589816cf385dd9449633e910d596b1f5c8a"}, + {file = "jiter-0.10.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:520ef6d981172693786a49ff5b09eda72a42e539f14788124a07530f785c3ad6"}, + {file = "jiter-0.10.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:554dedfd05937f8fc45d17ebdf298fe7e0c77458232bcb73d9fbbf4c6455f5b3"}, + {file = "jiter-0.10.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5bc299da7789deacf95f64052d97f75c16d4fc8c4c214a22bf8d859a4288a1c2"}, + {file = "jiter-0.10.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5161e201172de298a8a1baad95eb85db4fb90e902353b1f6a41d64ea64644e25"}, + {file = "jiter-0.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e2227db6ba93cb3e2bf67c87e594adde0609f146344e8207e8730364db27041"}, + {file = "jiter-0.10.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:15acb267ea5e2c64515574b06a8bf393fbfee6a50eb1673614aa45f4613c0cca"}, + {file = "jiter-0.10.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:901b92f2e2947dc6dfcb52fd624453862e16665ea909a08398dde19c0731b7f4"}, + {file = "jiter-0.10.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d0cb9a125d5a3ec971a094a845eadde2db0de85b33c9f13eb94a0c63d463879e"}, + {file = "jiter-0.10.0-cp313-cp313-win32.whl", hash = "sha256:48a403277ad1ee208fb930bdf91745e4d2d6e47253eedc96e2559d1e6527006d"}, + {file = "jiter-0.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:75f9eb72ecb640619c29bf714e78c9c46c9c4eaafd644bf78577ede459f330d4"}, + {file = "jiter-0.10.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:28ed2a4c05a1f32ef0e1d24c2611330219fed727dae01789f4a335617634b1ca"}, + {file = "jiter-0.10.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14a4c418b1ec86a195f1ca69da8b23e8926c752b685af665ce30777233dfe070"}, + {file = "jiter-0.10.0-cp313-cp313t-win_amd64.whl", hash = "sha256:d7bfed2fe1fe0e4dda6ef682cee888ba444b21e7a6553e03252e4feb6cf0adca"}, + {file = "jiter-0.10.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:5e9251a5e83fab8d87799d3e1a46cb4b7f2919b895c6f4483629ed2446f66522"}, + {file = "jiter-0.10.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:023aa0204126fe5b87ccbcd75c8a0d0261b9abdbbf46d55e7ae9f8e22424eeb8"}, + {file = "jiter-0.10.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c189c4f1779c05f75fc17c0c1267594ed918996a231593a21a5ca5438445216"}, + {file = "jiter-0.10.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:15720084d90d1098ca0229352607cd68256c76991f6b374af96f36920eae13c4"}, + {file = "jiter-0.10.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e4f2fb68e5f1cfee30e2b2a09549a00683e0fde4c6a2ab88c94072fc33cb7426"}, + {file = "jiter-0.10.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce541693355fc6da424c08b7edf39a2895f58d6ea17d92cc2b168d20907dee12"}, + {file = "jiter-0.10.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31c50c40272e189d50006ad5c73883caabb73d4e9748a688b216e85a9a9ca3b9"}, + {file = "jiter-0.10.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fa3402a2ff9815960e0372a47b75c76979d74402448509ccd49a275fa983ef8a"}, + {file = "jiter-0.10.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:1956f934dca32d7bb647ea21d06d93ca40868b505c228556d3373cbd255ce853"}, + {file = "jiter-0.10.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:fcedb049bdfc555e261d6f65a6abe1d5ad68825b7202ccb9692636c70fcced86"}, + {file = "jiter-0.10.0-cp314-cp314-win32.whl", hash = "sha256:ac509f7eccca54b2a29daeb516fb95b6f0bd0d0d8084efaf8ed5dfc7b9f0b357"}, + {file = "jiter-0.10.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5ed975b83a2b8639356151cef5c0d597c68376fc4922b45d0eb384ac058cfa00"}, + {file = "jiter-0.10.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3aa96f2abba33dc77f79b4cf791840230375f9534e5fac927ccceb58c5e604a5"}, + {file = "jiter-0.10.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:bd6292a43c0fc09ce7c154ec0fa646a536b877d1e8f2f96c19707f65355b5a4d"}, + {file = "jiter-0.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:39de429dcaeb6808d75ffe9effefe96a4903c6a4b376b2f6d08d77c1aaee2f18"}, + {file = "jiter-0.10.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52ce124f13a7a616fad3bb723f2bfb537d78239d1f7f219566dc52b6f2a9e48d"}, + {file = "jiter-0.10.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:166f3606f11920f9a1746b2eea84fa2c0a5d50fd313c38bdea4edc072000b0af"}, + {file = "jiter-0.10.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:28dcecbb4ba402916034fc14eba7709f250c4d24b0c43fc94d187ee0580af181"}, + {file = "jiter-0.10.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86c5aa6910f9bebcc7bc4f8bc461aff68504388b43bfe5e5c0bd21efa33b52f4"}, + {file = "jiter-0.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ceeb52d242b315d7f1f74b441b6a167f78cea801ad7c11c36da77ff2d42e8a28"}, + {file = "jiter-0.10.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ff76d8887c8c8ee1e772274fcf8cc1071c2c58590d13e33bd12d02dc9a560397"}, + {file = "jiter-0.10.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a9be4d0fa2b79f7222a88aa488bd89e2ae0a0a5b189462a12def6ece2faa45f1"}, + {file = "jiter-0.10.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9ab7fd8738094139b6c1ab1822d6f2000ebe41515c537235fd45dabe13ec9324"}, + {file = "jiter-0.10.0-cp39-cp39-win32.whl", hash = "sha256:5f51e048540dd27f204ff4a87f5d79294ea0aa3aa552aca34934588cf27023cf"}, + {file = "jiter-0.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:1b28302349dc65703a9e4ead16f163b1c339efffbe1049c30a44b001a2a4fff9"}, + {file = "jiter-0.10.0.tar.gz", hash = "sha256:07a7142c38aacc85194391108dc91b5b57093c978a9932bd86a36862759d9500"}, +] + +[[package]] +name = "joblib" +version = "1.5.1" +description = "Lightweight pipelining with Python functions" +optional = false +python-versions = ">=3.9" +files = [ + {file = "joblib-1.5.1-py3-none-any.whl", hash = "sha256:4719a31f054c7d766948dcd83e9613686b27114f190f717cec7eaa2084f8a74a"}, + {file = "joblib-1.5.1.tar.gz", hash = "sha256:f4f86e351f39fe3d0d32a9f2c3d8af1ee4cec285aafcb27003dda5205576b444"}, +] + +[[package]] +name = "jsonschema" +version = "4.24.0" +description = "An implementation of JSON Schema validation for Python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "jsonschema-4.24.0-py3-none-any.whl", hash = "sha256:a462455f19f5faf404a7902952b6f0e3ce868f3ee09a359b05eca6673bd8412d"}, + {file = "jsonschema-4.24.0.tar.gz", hash = "sha256:0b4e8069eb12aedfa881333004bccaec24ecef5a8a6a4b6df142b2cc9599d196"}, +] + +[package.dependencies] +attrs = ">=22.2.0" +jsonschema-specifications = ">=2023.03.6" +referencing = ">=0.28.4" +rpds-py = ">=0.7.1" + +[package.extras] +format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] +format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=24.6.0)"] + +[[package]] +name = "jsonschema-specifications" +version = "2025.4.1" +description = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" +optional = false +python-versions = ">=3.9" +files = [ + {file = "jsonschema_specifications-2025.4.1-py3-none-any.whl", hash = "sha256:4653bffbd6584f7de83a67e0d620ef16900b390ddc7939d56684d6c81e33f1af"}, + {file = "jsonschema_specifications-2025.4.1.tar.gz", hash = "sha256:630159c9f4dbea161a6a2205c3011cc4f18ff381b189fff48bb39b9bf26ae608"}, +] + +[package.dependencies] +referencing = ">=0.31.0" + +[[package]] +name = "kubernetes" +version = "33.1.0" +description = "Kubernetes python client" +optional = false +python-versions = ">=3.6" +files = [ + {file = "kubernetes-33.1.0-py2.py3-none-any.whl", hash = "sha256:544de42b24b64287f7e0aa9513c93cb503f7f40eea39b20f66810011a86eabc5"}, + {file = "kubernetes-33.1.0.tar.gz", hash = "sha256:f64d829843a54c251061a8e7a14523b521f2dc5c896cf6d65ccf348648a88993"}, +] + +[package.dependencies] +certifi = ">=14.05.14" +durationpy = ">=0.7" +google-auth = ">=1.0.1" +oauthlib = ">=3.2.2" +python-dateutil = ">=2.5.3" +pyyaml = ">=5.4.1" +requests = "*" +requests-oauthlib = "*" +six = ">=1.9.0" +urllib3 = ">=1.24.2" +websocket-client = ">=0.32.0,<0.40.0 || >0.40.0,<0.41.dev0 || >=0.43.dev0" + +[package.extras] +adal = ["adal (>=1.0.2)"] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +optional = false +python-versions = ">=3.8" +files = [ + {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, + {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, +] + +[package.dependencies] +mdurl = ">=0.1,<1.0" + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +code-style = ["pre-commit (>=3.0,<4.0)"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] +plugins = ["mdit-py-plugins"] +profiling = ["gprof2dot"] +rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] + +[[package]] +name = "markupsafe" +version = "3.0.2" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.9" +files = [ + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, + {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +optional = false +python-versions = ">=3.7" +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + +[[package]] +name = "mmh3" +version = "5.1.0" +description = "Python extension for MurmurHash (MurmurHash3), a set of fast and robust hash functions." +optional = false +python-versions = ">=3.9" +files = [ + {file = "mmh3-5.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:eaf4ac5c6ee18ca9232238364d7f2a213278ae5ca97897cafaa123fcc7bb8bec"}, + {file = "mmh3-5.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:48f9aa8ccb9ad1d577a16104834ac44ff640d8de8c0caed09a2300df7ce8460a"}, + {file = "mmh3-5.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d4ba8cac21e1f2d4e436ce03a82a7f87cda80378691f760e9ea55045ec480a3d"}, + {file = "mmh3-5.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d69281c281cb01994f054d862a6bb02a2e7acfe64917795c58934b0872b9ece4"}, + {file = "mmh3-5.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d05ed3962312fbda2a1589b97359d2467f677166952f6bd410d8c916a55febf"}, + {file = "mmh3-5.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78ae6a03f4cff4aa92ddd690611168856f8c33a141bd3e5a1e0a85521dc21ea0"}, + {file = "mmh3-5.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:95f983535b39795d9fb7336438faae117424c6798f763d67c6624f6caf2c4c01"}, + {file = "mmh3-5.1.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d46fdd80d4c7ecadd9faa6181e92ccc6fe91c50991c9af0e371fdf8b8a7a6150"}, + {file = "mmh3-5.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0f16e976af7365ea3b5c425124b2a7f0147eed97fdbb36d99857f173c8d8e096"}, + {file = "mmh3-5.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:6fa97f7d1e1f74ad1565127229d510f3fd65d931fdedd707c1e15100bc9e5ebb"}, + {file = "mmh3-5.1.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4052fa4a8561bd62648e9eb993c8f3af3bdedadf3d9687aa4770d10e3709a80c"}, + {file = "mmh3-5.1.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:3f0e8ae9f961037f812afe3cce7da57abf734285961fffbeff9a4c011b737732"}, + {file = "mmh3-5.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:99297f207db967814f1f02135bb7fe7628b9eacb046134a34e1015b26b06edce"}, + {file = "mmh3-5.1.0-cp310-cp310-win32.whl", hash = "sha256:2e6c8dc3631a5e22007fbdb55e993b2dbce7985c14b25b572dd78403c2e79182"}, + {file = "mmh3-5.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:e4e8c7ad5a4dddcfde35fd28ef96744c1ee0f9d9570108aa5f7e77cf9cfdf0bf"}, + {file = "mmh3-5.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:45da549269883208912868a07d0364e1418d8292c4259ca11699ba1b2475bd26"}, + {file = "mmh3-5.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0b529dcda3f951ff363a51d5866bc6d63cf57f1e73e8961f864ae5010647079d"}, + {file = "mmh3-5.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4db1079b3ace965e562cdfc95847312f9273eb2ad3ebea983435c8423e06acd7"}, + {file = "mmh3-5.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:22d31e3a0ff89b8eb3b826d6fc8e19532998b2aa6b9143698043a1268da413e1"}, + {file = "mmh3-5.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2139bfbd354cd6cb0afed51c4b504f29bcd687a3b1460b7e89498329cc28a894"}, + {file = "mmh3-5.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8c8105c6a435bc2cd6ea2ef59558ab1a2976fd4a4437026f562856d08996673a"}, + {file = "mmh3-5.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57730067174a7f36fcd6ce012fe359bd5510fdaa5fe067bc94ed03e65dafb769"}, + {file = "mmh3-5.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bde80eb196d7fdc765a318604ded74a4378f02c5b46c17aa48a27d742edaded2"}, + {file = "mmh3-5.1.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9c8eddcb441abddeb419c16c56fd74b3e2df9e57f7aa2903221996718435c7a"}, + {file = "mmh3-5.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:99e07e4acafbccc7a28c076a847fb060ffc1406036bc2005acb1b2af620e53c3"}, + {file = "mmh3-5.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9e25ba5b530e9a7d65f41a08d48f4b3fedc1e89c26486361166a5544aa4cad33"}, + {file = "mmh3-5.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:bb9bf7475b4d99156ce2f0cf277c061a17560c8c10199c910a680869a278ddc7"}, + {file = "mmh3-5.1.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2a1b0878dd281ea3003368ab53ff6f568e175f1b39f281df1da319e58a19c23a"}, + {file = "mmh3-5.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:25f565093ac8b8aefe0f61f8f95c9a9d11dd69e6a9e9832ff0d293511bc36258"}, + {file = "mmh3-5.1.0-cp311-cp311-win32.whl", hash = "sha256:1e3554d8792387eac73c99c6eaea0b3f884e7130eb67986e11c403e4f9b6d372"}, + {file = "mmh3-5.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:8ad777a48197882492af50bf3098085424993ce850bdda406a358b6ab74be759"}, + {file = "mmh3-5.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:f29dc4efd99bdd29fe85ed6c81915b17b2ef2cf853abf7213a48ac6fb3eaabe1"}, + {file = "mmh3-5.1.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:45712987367cb9235026e3cbf4334670522a97751abfd00b5bc8bfa022c3311d"}, + {file = "mmh3-5.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b1020735eb35086ab24affbea59bb9082f7f6a0ad517cb89f0fc14f16cea4dae"}, + {file = "mmh3-5.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:babf2a78ce5513d120c358722a2e3aa7762d6071cd10cede026f8b32452be322"}, + {file = "mmh3-5.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4f47f58cd5cbef968c84a7c1ddc192fef0a36b48b0b8a3cb67354531aa33b00"}, + {file = "mmh3-5.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2044a601c113c981f2c1e14fa33adc9b826c9017034fe193e9eb49a6882dbb06"}, + {file = "mmh3-5.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c94d999c9f2eb2da44d7c2826d3fbffdbbbbcde8488d353fee7c848ecc42b968"}, + {file = "mmh3-5.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a015dcb24fa0c7a78f88e9419ac74f5001c1ed6a92e70fd1803f74afb26a4c83"}, + {file = "mmh3-5.1.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:457da019c491a2d20e2022c7d4ce723675e4c081d9efc3b4d8b9f28a5ea789bd"}, + {file = "mmh3-5.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:71408579a570193a4ac9c77344d68ddefa440b00468a0b566dcc2ba282a9c559"}, + {file = "mmh3-5.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8b3a04bc214a6e16c81f02f855e285c6df274a2084787eeafaa45f2fbdef1b63"}, + {file = "mmh3-5.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:832dae26a35514f6d3c1e267fa48e8de3c7b978afdafa0529c808ad72e13ada3"}, + {file = "mmh3-5.1.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bf658a61fc92ef8a48945ebb1076ef4ad74269e353fffcb642dfa0890b13673b"}, + {file = "mmh3-5.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3313577453582b03383731b66447cdcdd28a68f78df28f10d275d7d19010c1df"}, + {file = "mmh3-5.1.0-cp312-cp312-win32.whl", hash = "sha256:1d6508504c531ab86c4424b5a5ff07c1132d063863339cf92f6657ff7a580f76"}, + {file = "mmh3-5.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:aa75981fcdf3f21759d94f2c81b6a6e04a49dfbcdad88b152ba49b8e20544776"}, + {file = "mmh3-5.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:a4c1a76808dfea47f7407a0b07aaff9087447ef6280716fd0783409b3088bb3c"}, + {file = "mmh3-5.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7a523899ca29cfb8a5239618474a435f3d892b22004b91779fcb83504c0d5b8c"}, + {file = "mmh3-5.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:17cef2c3a6ca2391ca7171a35ed574b5dab8398163129a3e3a4c05ab85a4ff40"}, + {file = "mmh3-5.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:52e12895b30110f3d89dae59a888683cc886ed0472dd2eca77497edef6161997"}, + {file = "mmh3-5.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e0d6719045cda75c3f40397fc24ab67b18e0cb8f69d3429ab4c39763c4c608dd"}, + {file = "mmh3-5.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d19fa07d303a91f8858982c37e6939834cb11893cb3ff20e6ee6fa2a7563826a"}, + {file = "mmh3-5.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:31b47a620d622fbde8ca1ca0435c5d25de0ac57ab507209245e918128e38e676"}, + {file = "mmh3-5.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00f810647c22c179b6821079f7aa306d51953ac893587ee09cf1afb35adf87cb"}, + {file = "mmh3-5.1.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6128b610b577eed1e89ac7177ab0c33d06ade2aba93f5c89306032306b5f1c6"}, + {file = "mmh3-5.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1e550a45d2ff87a1c11b42015107f1778c93f4c6f8e731bf1b8fa770321b8cc4"}, + {file = "mmh3-5.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:785ae09276342f79fd8092633e2d52c0f7c44d56e8cfda8274ccc9b76612dba2"}, + {file = "mmh3-5.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0f4be3703a867ef976434afd3661a33884abe73ceb4ee436cac49d3b4c2aaa7b"}, + {file = "mmh3-5.1.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e513983830c4ff1f205ab97152a0050cf7164f1b4783d702256d39c637b9d107"}, + {file = "mmh3-5.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b9135c300535c828c0bae311b659f33a31c941572eae278568d1a953c4a57b59"}, + {file = "mmh3-5.1.0-cp313-cp313-win32.whl", hash = "sha256:c65dbd12885a5598b70140d24de5839551af5a99b29f9804bb2484b29ef07692"}, + {file = "mmh3-5.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:10db7765201fc65003fa998faa067417ef6283eb5f9bba8f323c48fd9c33e91f"}, + {file = "mmh3-5.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:b22fe2e54be81f6c07dcb36b96fa250fb72effe08aa52fbb83eade6e1e2d5fd7"}, + {file = "mmh3-5.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:166b67749a1d8c93b06f5e90576f1ba838a65c8e79f28ffd9dfafba7c7d0a084"}, + {file = "mmh3-5.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:adba83c7ba5cc8ea201ee1e235f8413a68e7f7b8a657d582cc6c6c9d73f2830e"}, + {file = "mmh3-5.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a61f434736106804eb0b1612d503c4e6eb22ba31b16e6a2f987473de4226fa55"}, + {file = "mmh3-5.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba9ce59816b30866093f048b3312c2204ff59806d3a02adee71ff7bd22b87554"}, + {file = "mmh3-5.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd51597bef1e503363b05cb579db09269e6e6c39d419486626b255048daf545b"}, + {file = "mmh3-5.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d51a1ed642d3fb37b8f4cab966811c52eb246c3e1740985f701ef5ad4cdd2145"}, + {file = "mmh3-5.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:709bfe81c53bf8a3609efcbd65c72305ade60944f66138f697eefc1a86b6e356"}, + {file = "mmh3-5.1.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e01a9b0092b6f82e861137c8e9bb9899375125b24012eb5219e61708be320032"}, + {file = "mmh3-5.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:27e46a2c13c9a805e03c9ec7de0ca8e096794688ab2125bdce4229daf60c4a56"}, + {file = "mmh3-5.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:5766299c1d26f6bfd0a638e070bd17dbd98d4ccb067d64db3745bf178e700ef0"}, + {file = "mmh3-5.1.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:7785205e3e4443fdcbb73766798c7647f94c2f538b90f666688f3e757546069e"}, + {file = "mmh3-5.1.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:8e574fbd39afb433b3ab95683b1b4bf18313dc46456fc9daaddc2693c19ca565"}, + {file = "mmh3-5.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1b6727a5a20e32cbf605743749f3862abe5f5e097cbf2afc7be5aafd32a549ae"}, + {file = "mmh3-5.1.0-cp39-cp39-win32.whl", hash = "sha256:d6eaa711d4b9220fe5252032a44bf68e5dcfb7b21745a96efc9e769b0dd57ec2"}, + {file = "mmh3-5.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:49d444913f6c02980e5241a53fe9af2338f2043d6ce5b6f5ea7d302c52c604ac"}, + {file = "mmh3-5.1.0-cp39-cp39-win_arm64.whl", hash = "sha256:0daaeaedd78773b70378f2413c7d6b10239a75d955d30d54f460fb25d599942d"}, + {file = "mmh3-5.1.0.tar.gz", hash = "sha256:136e1e670500f177f49ec106a4ebf0adf20d18d96990cc36ea492c651d2b406c"}, +] + +[package.extras] +benchmark = ["pymmh3 (==0.0.5)", "pyperf (==2.8.1)", "xxhash (==3.5.0)"] +docs = ["myst-parser (==4.0.0)", "shibuya (==2024.12.21)", "sphinx (==8.1.3)", "sphinx-copybutton (==0.5.2)"] +lint = ["black (==24.10.0)", "clang-format (==19.1.7)", "isort (==5.13.2)", "pylint (==3.3.3)"] +plot = ["matplotlib (==3.10.0)", "pandas (==2.2.3)"] +test = ["pytest (==8.3.4)", "pytest-sugar (==1.0.0)"] +type = ["mypy (==1.14.1)"] + +[[package]] +name = "mpmath" +version = "1.3.0" +description = "Python library for arbitrary-precision floating-point arithmetic" +optional = false +python-versions = "*" +files = [ + {file = "mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c"}, + {file = "mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f"}, +] + +[package.extras] +develop = ["codecov", "pycodestyle", "pytest (>=4.6)", "pytest-cov", "wheel"] +docs = ["sphinx"] +gmpy = ["gmpy2 (>=2.1.0a4)"] +tests = ["pytest (>=4.6)"] + +[[package]] +name = "multidict" +version = "6.4.4" +description = "multidict implementation" +optional = false +python-versions = ">=3.9" +files = [ + {file = "multidict-6.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8adee3ac041145ffe4488ea73fa0a622b464cc25340d98be76924d0cda8545ff"}, + {file = "multidict-6.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b61e98c3e2a861035aaccd207da585bdcacef65fe01d7a0d07478efac005e028"}, + {file = "multidict-6.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:75493f28dbadecdbb59130e74fe935288813301a8554dc32f0c631b6bdcdf8b0"}, + {file = "multidict-6.4.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ffc3c6a37e048b5395ee235e4a2a0d639c2349dffa32d9367a42fc20d399772"}, + {file = "multidict-6.4.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:87cb72263946b301570b0f63855569a24ee8758aaae2cd182aae7d95fbc92ca7"}, + {file = "multidict-6.4.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9bbf7bd39822fd07e3609b6b4467af4c404dd2b88ee314837ad1830a7f4a8299"}, + {file = "multidict-6.4.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d1f7cbd4f1f44ddf5fd86a8675b7679176eae770f2fc88115d6dddb6cefb59bc"}, + {file = "multidict-6.4.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb5ac9e5bfce0e6282e7f59ff7b7b9a74aa8e5c60d38186a4637f5aa764046ad"}, + {file = "multidict-6.4.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4efc31dfef8c4eeb95b6b17d799eedad88c4902daba39ce637e23a17ea078915"}, + {file = "multidict-6.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9fcad2945b1b91c29ef2b4050f590bfcb68d8ac8e0995a74e659aa57e8d78e01"}, + {file = "multidict-6.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:d877447e7368c7320832acb7159557e49b21ea10ffeb135c1077dbbc0816b598"}, + {file = "multidict-6.4.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:33a12ebac9f380714c298cbfd3e5b9c0c4e89c75fe612ae496512ee51028915f"}, + {file = "multidict-6.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:0f14ea68d29b43a9bf37953881b1e3eb75b2739e896ba4a6aa4ad4c5b9ffa145"}, + {file = "multidict-6.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0327ad2c747a6600e4797d115d3c38a220fdb28e54983abe8964fd17e95ae83c"}, + {file = "multidict-6.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d1a20707492db9719a05fc62ee215fd2c29b22b47c1b1ba347f9abc831e26683"}, + {file = "multidict-6.4.4-cp310-cp310-win32.whl", hash = "sha256:d83f18315b9fca5db2452d1881ef20f79593c4aa824095b62cb280019ef7aa3d"}, + {file = "multidict-6.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:9c17341ee04545fd962ae07330cb5a39977294c883485c8d74634669b1f7fe04"}, + {file = "multidict-6.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4f5f29794ac0e73d2a06ac03fd18870adc0135a9d384f4a306a951188ed02f95"}, + {file = "multidict-6.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c04157266344158ebd57b7120d9b0b35812285d26d0e78193e17ef57bfe2979a"}, + {file = "multidict-6.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bb61ffd3ab8310d93427e460f565322c44ef12769f51f77277b4abad7b6f7223"}, + {file = "multidict-6.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e0ba18a9afd495f17c351d08ebbc4284e9c9f7971d715f196b79636a4d0de44"}, + {file = "multidict-6.4.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9faf1b1dcaadf9f900d23a0e6d6c8eadd6a95795a0e57fcca73acce0eb912065"}, + {file = "multidict-6.4.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a4d1cb1327c6082c4fce4e2a438483390964c02213bc6b8d782cf782c9b1471f"}, + {file = "multidict-6.4.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:941f1bec2f5dbd51feeb40aea654c2747f811ab01bdd3422a48a4e4576b7d76a"}, + {file = "multidict-6.4.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5f8a146184da7ea12910a4cec51ef85e44f6268467fb489c3caf0cd512f29c2"}, + {file = "multidict-6.4.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:232b7237e57ec3c09be97206bfb83a0aa1c5d7d377faa019c68a210fa35831f1"}, + {file = "multidict-6.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:55ae0721c1513e5e3210bca4fc98456b980b0c2c016679d3d723119b6b202c42"}, + {file = "multidict-6.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:51d662c072579f63137919d7bb8fc250655ce79f00c82ecf11cab678f335062e"}, + {file = "multidict-6.4.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0e05c39962baa0bb19a6b210e9b1422c35c093b651d64246b6c2e1a7e242d9fd"}, + {file = "multidict-6.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d5b1cc3ab8c31d9ebf0faa6e3540fb91257590da330ffe6d2393d4208e638925"}, + {file = "multidict-6.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:93ec84488a384cd7b8a29c2c7f467137d8a73f6fe38bb810ecf29d1ade011a7c"}, + {file = "multidict-6.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b308402608493638763abc95f9dc0030bbd6ac6aff784512e8ac3da73a88af08"}, + {file = "multidict-6.4.4-cp311-cp311-win32.whl", hash = "sha256:343892a27d1a04d6ae455ecece12904d242d299ada01633d94c4f431d68a8c49"}, + {file = "multidict-6.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:73484a94f55359780c0f458bbd3c39cb9cf9c182552177d2136e828269dee529"}, + {file = "multidict-6.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:dc388f75a1c00000824bf28b7633e40854f4127ede80512b44c3cfeeea1839a2"}, + {file = "multidict-6.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:98af87593a666f739d9dba5d0ae86e01b0e1a9cfcd2e30d2d361fbbbd1a9162d"}, + {file = "multidict-6.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aff4cafea2d120327d55eadd6b7f1136a8e5a0ecf6fb3b6863e8aca32cd8e50a"}, + {file = "multidict-6.4.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:169c4ba7858176b797fe551d6e99040c531c775d2d57b31bcf4de6d7a669847f"}, + {file = "multidict-6.4.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b9eb4c59c54421a32b3273d4239865cb14ead53a606db066d7130ac80cc8ec93"}, + {file = "multidict-6.4.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7cf3bd54c56aa16fdb40028d545eaa8d051402b61533c21e84046e05513d5780"}, + {file = "multidict-6.4.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f682c42003c7264134bfe886376299db4cc0c6cd06a3295b41b347044bcb5482"}, + {file = "multidict-6.4.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a920f9cf2abdf6e493c519492d892c362007f113c94da4c239ae88429835bad1"}, + {file = "multidict-6.4.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:530d86827a2df6504526106b4c104ba19044594f8722d3e87714e847c74a0275"}, + {file = "multidict-6.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ecde56ea2439b96ed8a8d826b50c57364612ddac0438c39e473fafad7ae1c23b"}, + {file = "multidict-6.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:dc8c9736d8574b560634775ac0def6bdc1661fc63fa27ffdfc7264c565bcb4f2"}, + {file = "multidict-6.4.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7f3d3b3c34867579ea47cbd6c1f2ce23fbfd20a273b6f9e3177e256584f1eacc"}, + {file = "multidict-6.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:87a728af265e08f96b6318ebe3c0f68b9335131f461efab2fc64cc84a44aa6ed"}, + {file = "multidict-6.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9f193eeda1857f8e8d3079a4abd258f42ef4a4bc87388452ed1e1c4d2b0c8740"}, + {file = "multidict-6.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be06e73c06415199200e9a2324a11252a3d62030319919cde5e6950ffeccf72e"}, + {file = "multidict-6.4.4-cp312-cp312-win32.whl", hash = "sha256:622f26ea6a7e19b7c48dd9228071f571b2fbbd57a8cd71c061e848f281550e6b"}, + {file = "multidict-6.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:5e2bcda30d5009996ff439e02a9f2b5c3d64a20151d34898c000a6281faa3781"}, + {file = "multidict-6.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:82ffabefc8d84c2742ad19c37f02cde5ec2a1ee172d19944d380f920a340e4b9"}, + {file = "multidict-6.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6a2f58a66fe2c22615ad26156354005391e26a2f3721c3621504cd87c1ea87bf"}, + {file = "multidict-6.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5883d6ee0fd9d8a48e9174df47540b7545909841ac82354c7ae4cbe9952603bd"}, + {file = "multidict-6.4.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9abcf56a9511653fa1d052bfc55fbe53dbee8f34e68bd6a5a038731b0ca42d15"}, + {file = "multidict-6.4.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6ed5ae5605d4ad5a049fad2a28bb7193400700ce2f4ae484ab702d1e3749c3f9"}, + {file = "multidict-6.4.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bbfcb60396f9bcfa63e017a180c3105b8c123a63e9d1428a36544e7d37ca9e20"}, + {file = "multidict-6.4.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b0f1987787f5f1e2076b59692352ab29a955b09ccc433c1f6b8e8e18666f608b"}, + {file = "multidict-6.4.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d0121ccce8c812047d8d43d691a1ad7641f72c4f730474878a5aeae1b8ead8c"}, + {file = "multidict-6.4.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83ec4967114295b8afd120a8eec579920c882831a3e4c3331d591a8e5bfbbc0f"}, + {file = "multidict-6.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:995f985e2e268deaf17867801b859a282e0448633f1310e3704b30616d269d69"}, + {file = "multidict-6.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:d832c608f94b9f92a0ec8b7e949be7792a642b6e535fcf32f3e28fab69eeb046"}, + {file = "multidict-6.4.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d21c1212171cf7da703c5b0b7a0e85be23b720818aef502ad187d627316d5645"}, + {file = "multidict-6.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:cbebaa076aaecad3d4bb4c008ecc73b09274c952cf6a1b78ccfd689e51f5a5b0"}, + {file = "multidict-6.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:c93a6fb06cc8e5d3628b2b5fda215a5db01e8f08fc15fadd65662d9b857acbe4"}, + {file = "multidict-6.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8cd8f81f1310182362fb0c7898145ea9c9b08a71081c5963b40ee3e3cac589b1"}, + {file = "multidict-6.4.4-cp313-cp313-win32.whl", hash = "sha256:3e9f1cd61a0ab857154205fb0b1f3d3ace88d27ebd1409ab7af5096e409614cd"}, + {file = "multidict-6.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:8ffb40b74400e4455785c2fa37eba434269149ec525fc8329858c862e4b35373"}, + {file = "multidict-6.4.4-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:6a602151dbf177be2450ef38966f4be3467d41a86c6a845070d12e17c858a156"}, + {file = "multidict-6.4.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0d2b9712211b860d123815a80b859075d86a4d54787e247d7fbee9db6832cf1c"}, + {file = "multidict-6.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d2fa86af59f8fc1972e121ade052145f6da22758f6996a197d69bb52f8204e7e"}, + {file = "multidict-6.4.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50855d03e9e4d66eab6947ba688ffb714616f985838077bc4b490e769e48da51"}, + {file = "multidict-6.4.4-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5bce06b83be23225be1905dcdb6b789064fae92499fbc458f59a8c0e68718601"}, + {file = "multidict-6.4.4-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66ed0731f8e5dfd8369a883b6e564aca085fb9289aacabd9decd70568b9a30de"}, + {file = "multidict-6.4.4-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:329ae97fc2f56f44d91bc47fe0972b1f52d21c4b7a2ac97040da02577e2daca2"}, + {file = "multidict-6.4.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c27e5dcf520923d6474d98b96749e6805f7677e93aaaf62656005b8643f907ab"}, + {file = "multidict-6.4.4-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:058cc59b9e9b143cc56715e59e22941a5d868c322242278d28123a5d09cdf6b0"}, + {file = "multidict-6.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:69133376bc9a03f8c47343d33f91f74a99c339e8b58cea90433d8e24bb298031"}, + {file = "multidict-6.4.4-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:d6b15c55721b1b115c5ba178c77104123745b1417527ad9641a4c5e2047450f0"}, + {file = "multidict-6.4.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:a887b77f51d3d41e6e1a63cf3bc7ddf24de5939d9ff69441387dfefa58ac2e26"}, + {file = "multidict-6.4.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:632a3bf8f1787f7ef7d3c2f68a7bde5be2f702906f8b5842ad6da9d974d0aab3"}, + {file = "multidict-6.4.4-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:a145c550900deb7540973c5cdb183b0d24bed6b80bf7bddf33ed8f569082535e"}, + {file = "multidict-6.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cc5d83c6619ca5c9672cb78b39ed8542f1975a803dee2cda114ff73cbb076edd"}, + {file = "multidict-6.4.4-cp313-cp313t-win32.whl", hash = "sha256:3312f63261b9df49be9d57aaa6abf53a6ad96d93b24f9cc16cf979956355ce6e"}, + {file = "multidict-6.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:ba852168d814b2c73333073e1c7116d9395bea69575a01b0b3c89d2d5a87c8fb"}, + {file = "multidict-6.4.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:603f39bd1cf85705c6c1ba59644b480dfe495e6ee2b877908de93322705ad7cf"}, + {file = "multidict-6.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fc60f91c02e11dfbe3ff4e1219c085695c339af72d1641800fe6075b91850c8f"}, + {file = "multidict-6.4.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:496bcf01c76a70a31c3d746fd39383aad8d685ce6331e4c709e9af4ced5fa221"}, + {file = "multidict-6.4.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4219390fb5bf8e548e77b428bb36a21d9382960db5321b74d9d9987148074d6b"}, + {file = "multidict-6.4.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef4e9096ff86dfdcbd4a78253090ba13b1d183daa11b973e842465d94ae1772"}, + {file = "multidict-6.4.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:49a29d7133b1fc214e818bbe025a77cc6025ed9a4f407d2850373ddde07fd04a"}, + {file = "multidict-6.4.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e32053d6d3a8b0dfe49fde05b496731a0e6099a4df92154641c00aa76786aef5"}, + {file = "multidict-6.4.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cc403092a49509e8ef2d2fd636a8ecefc4698cc57bbe894606b14579bc2a955"}, + {file = "multidict-6.4.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5363f9b2a7f3910e5c87d8b1855c478c05a2dc559ac57308117424dfaad6805c"}, + {file = "multidict-6.4.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2e543a40e4946cf70a88a3be87837a3ae0aebd9058ba49e91cacb0b2cd631e2b"}, + {file = "multidict-6.4.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:60d849912350da557fe7de20aa8cf394aada6980d0052cc829eeda4a0db1c1db"}, + {file = "multidict-6.4.4-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:19d08b4f22eae45bb018b9f06e2838c1e4b853c67628ef8ae126d99de0da6395"}, + {file = "multidict-6.4.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:d693307856d1ef08041e8b6ff01d5b4618715007d288490ce2c7e29013c12b9a"}, + {file = "multidict-6.4.4-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:fad6daaed41021934917f4fb03ca2db8d8a4d79bf89b17ebe77228eb6710c003"}, + {file = "multidict-6.4.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c10d17371bff801af0daf8b073c30b6cf14215784dc08cd5c43ab5b7b8029bbc"}, + {file = "multidict-6.4.4-cp39-cp39-win32.whl", hash = "sha256:7e23f2f841fcb3ebd4724a40032d32e0892fbba4143e43d2a9e7695c5e50e6bd"}, + {file = "multidict-6.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:4d7b50b673ffb4ff4366e7ab43cf1f0aef4bd3608735c5fbdf0bdb6f690da411"}, + {file = "multidict-6.4.4-py3-none-any.whl", hash = "sha256:bd4557071b561a8b3b6075c3ce93cf9bfb6182cb241805c3d66ced3b75eff4ac"}, + {file = "multidict-6.4.4.tar.gz", hash = "sha256:69ee9e6ba214b5245031b76233dd95408a0fd57fdb019ddcc1ead4790932a8e8"}, +] + +[[package]] +name = "networkx" +version = "3.5" +description = "Python package for creating and manipulating graphs and networks" +optional = false +python-versions = ">=3.11" +files = [ + {file = "networkx-3.5-py3-none-any.whl", hash = "sha256:0030d386a9a06dee3565298b4a734b68589749a544acbb6c412dc9e2489ec6ec"}, + {file = "networkx-3.5.tar.gz", hash = "sha256:d4c6f9cf81f52d69230866796b82afbccdec3db7ae4fbd1b65ea750feed50037"}, +] + +[package.extras] +default = ["matplotlib (>=3.8)", "numpy (>=1.25)", "pandas (>=2.0)", "scipy (>=1.11.2)"] +developer = ["mypy (>=1.15)", "pre-commit (>=4.1)"] +doc = ["intersphinx-registry", "myst-nb (>=1.1)", "numpydoc (>=1.8.0)", "pillow (>=10)", "pydata-sphinx-theme (>=0.16)", "sphinx (>=8.0)", "sphinx-gallery (>=0.18)", "texext (>=0.6.7)"] +example = ["cairocffi (>=1.7)", "contextily (>=1.6)", "igraph (>=0.11)", "momepy (>=0.7.2)", "osmnx (>=2.0.0)", "scikit-learn (>=1.5)", "seaborn (>=0.13)"] +extra = ["lxml (>=4.6)", "pydot (>=3.0.1)", "pygraphviz (>=1.14)", "sympy (>=1.10)"] +test = ["pytest (>=7.2)", "pytest-cov (>=4.0)", "pytest-xdist (>=3.0)"] +test-extras = ["pytest-mpl", "pytest-randomly"] + +[[package]] +name = "numpy" +version = "2.3.0" +description = "Fundamental package for array computing in Python" +optional = false +python-versions = ">=3.11" +files = [ + {file = "numpy-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c3c9fdde0fa18afa1099d6257eb82890ea4f3102847e692193b54e00312a9ae9"}, + {file = "numpy-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:46d16f72c2192da7b83984aa5455baee640e33a9f1e61e656f29adf55e406c2b"}, + {file = "numpy-2.3.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a0be278be9307c4ab06b788f2a077f05e180aea817b3e41cebbd5aaf7bd85ed3"}, + {file = "numpy-2.3.0-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:99224862d1412d2562248d4710126355d3a8db7672170a39d6909ac47687a8a4"}, + {file = "numpy-2.3.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:2393a914db64b0ead0ab80c962e42d09d5f385802006a6c87835acb1f58adb96"}, + {file = "numpy-2.3.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:7729c8008d55e80784bd113787ce876ca117185c579c0d626f59b87d433ea779"}, + {file = "numpy-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:06d4fb37a8d383b769281714897420c5cc3545c79dc427df57fc9b852ee0bf58"}, + {file = "numpy-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c39ec392b5db5088259c68250e342612db82dc80ce044cf16496cf14cf6bc6f8"}, + {file = "numpy-2.3.0-cp311-cp311-win32.whl", hash = "sha256:ee9d3ee70d62827bc91f3ea5eee33153212c41f639918550ac0475e3588da59f"}, + {file = "numpy-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:43c55b6a860b0eb44d42341438b03513cf3879cb3617afb749ad49307e164edd"}, + {file = "numpy-2.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:2e6a1409eee0cb0316cb64640a49a49ca44deb1a537e6b1121dc7c458a1299a8"}, + {file = "numpy-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:389b85335838155a9076e9ad7f8fdba0827496ec2d2dc32ce69ce7898bde03ba"}, + {file = "numpy-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9498f60cd6bb8238d8eaf468a3d5bb031d34cd12556af53510f05fcf581c1b7e"}, + {file = "numpy-2.3.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:622a65d40d8eb427d8e722fd410ac3ad4958002f109230bc714fa551044ebae2"}, + {file = "numpy-2.3.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:b9446d9d8505aadadb686d51d838f2b6688c9e85636a0c3abaeb55ed54756459"}, + {file = "numpy-2.3.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:50080245365d75137a2bf46151e975de63146ae6d79f7e6bd5c0e85c9931d06a"}, + {file = "numpy-2.3.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:c24bb4113c66936eeaa0dc1e47c74770453d34f46ee07ae4efd853a2ed1ad10a"}, + {file = "numpy-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4d8d294287fdf685281e671886c6dcdf0291a7c19db3e5cb4178d07ccf6ecc67"}, + {file = "numpy-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6295f81f093b7f5769d1728a6bd8bf7466de2adfa771ede944ce6711382b89dc"}, + {file = "numpy-2.3.0-cp312-cp312-win32.whl", hash = "sha256:e6648078bdd974ef5d15cecc31b0c410e2e24178a6e10bf511e0557eed0f2570"}, + {file = "numpy-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:0898c67a58cdaaf29994bc0e2c65230fd4de0ac40afaf1584ed0b02cd74c6fdd"}, + {file = "numpy-2.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:bd8df082b6c4695753ad6193018c05aac465d634834dca47a3ae06d4bb22d9ea"}, + {file = "numpy-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5754ab5595bfa2c2387d241296e0381c21f44a4b90a776c3c1d39eede13a746a"}, + {file = "numpy-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d11fa02f77752d8099573d64e5fe33de3229b6632036ec08f7080f46b6649959"}, + {file = "numpy-2.3.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:aba48d17e87688a765ab1cd557882052f238e2f36545dfa8e29e6a91aef77afe"}, + {file = "numpy-2.3.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4dc58865623023b63b10d52f18abaac3729346a7a46a778381e0e3af4b7f3beb"}, + {file = "numpy-2.3.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:df470d376f54e052c76517393fa443758fefcdd634645bc9c1f84eafc67087f0"}, + {file = "numpy-2.3.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:87717eb24d4a8a64683b7a4e91ace04e2f5c7c77872f823f02a94feee186168f"}, + {file = "numpy-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d8fa264d56882b59dcb5ea4d6ab6f31d0c58a57b41aec605848b6eb2ef4a43e8"}, + {file = "numpy-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e651756066a0eaf900916497e20e02fe1ae544187cb0fe88de981671ee7f6270"}, + {file = "numpy-2.3.0-cp313-cp313-win32.whl", hash = "sha256:e43c3cce3b6ae5f94696669ff2a6eafd9a6b9332008bafa4117af70f4b88be6f"}, + {file = "numpy-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:81ae0bf2564cf475f94be4a27ef7bcf8af0c3e28da46770fc904da9abd5279b5"}, + {file = "numpy-2.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:c8738baa52505fa6e82778580b23f945e3578412554d937093eac9205e845e6e"}, + {file = "numpy-2.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:39b27d8b38942a647f048b675f134dd5a567f95bfff481f9109ec308515c51d8"}, + {file = "numpy-2.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0eba4a1ea88f9a6f30f56fdafdeb8da3774349eacddab9581a21234b8535d3d3"}, + {file = "numpy-2.3.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:b0f1f11d0a1da54927436505a5a7670b154eac27f5672afc389661013dfe3d4f"}, + {file = "numpy-2.3.0-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:690d0a5b60a47e1f9dcec7b77750a4854c0d690e9058b7bef3106e3ae9117808"}, + {file = "numpy-2.3.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:8b51ead2b258284458e570942137155978583e407babc22e3d0ed7af33ce06f8"}, + {file = "numpy-2.3.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:aaf81c7b82c73bd9b45e79cfb9476cb9c29e937494bfe9092c26aece812818ad"}, + {file = "numpy-2.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f420033a20b4f6a2a11f585f93c843ac40686a7c3fa514060a97d9de93e5e72b"}, + {file = "numpy-2.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d344ca32ab482bcf8735d8f95091ad081f97120546f3d250240868430ce52555"}, + {file = "numpy-2.3.0-cp313-cp313t-win32.whl", hash = "sha256:48a2e8eaf76364c32a1feaa60d6925eaf32ed7a040183b807e02674305beef61"}, + {file = "numpy-2.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ba17f93a94e503551f154de210e4d50c5e3ee20f7e7a1b5f6ce3f22d419b93bb"}, + {file = "numpy-2.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:f14e016d9409680959691c109be98c436c6249eaf7f118b424679793607b5944"}, + {file = "numpy-2.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:80b46117c7359de8167cc00a2c7d823bdd505e8c7727ae0871025a86d668283b"}, + {file = "numpy-2.3.0-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:5814a0f43e70c061f47abd5857d120179609ddc32a613138cbb6c4e9e2dbdda5"}, + {file = "numpy-2.3.0-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:ef6c1e88fd6b81ac6d215ed71dc8cd027e54d4bf1d2682d362449097156267a2"}, + {file = "numpy-2.3.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:33a5a12a45bb82d9997e2c0b12adae97507ad7c347546190a18ff14c28bbca12"}, + {file = "numpy-2.3.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:54dfc8681c1906d239e95ab1508d0a533c4a9505e52ee2d71a5472b04437ef97"}, + {file = "numpy-2.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:e017a8a251ff4d18d71f139e28bdc7c31edba7a507f72b1414ed902cbe48c74d"}, + {file = "numpy-2.3.0.tar.gz", hash = "sha256:581f87f9e9e9db2cba2141400e160e9dd644ee248788d6f90636eeb8fd9260a6"}, +] + +[[package]] +name = "oauthlib" +version = "3.2.2" +description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic" +optional = false +python-versions = ">=3.6" +files = [ + {file = "oauthlib-3.2.2-py3-none-any.whl", hash = "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca"}, + {file = "oauthlib-3.2.2.tar.gz", hash = "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918"}, +] + +[package.extras] +rsa = ["cryptography (>=3.0.0)"] +signals = ["blinker (>=1.4.0)"] +signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"] + +[[package]] +name = "onnxruntime" +version = "1.22.0" +description = "ONNX Runtime is a runtime accelerator for Machine Learning models" +optional = false +python-versions = ">=3.10" +files = [ + {file = "onnxruntime-1.22.0-cp310-cp310-macosx_13_0_universal2.whl", hash = "sha256:85d8826cc8054e4d6bf07f779dc742a363c39094015bdad6a08b3c18cfe0ba8c"}, + {file = "onnxruntime-1.22.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:468c9502a12f6f49ec335c2febd22fdceecc1e4cc96dfc27e419ba237dff5aff"}, + {file = "onnxruntime-1.22.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:681fe356d853630a898ee05f01ddb95728c9a168c9460e8361d0a240c9b7cb97"}, + {file = "onnxruntime-1.22.0-cp310-cp310-win_amd64.whl", hash = "sha256:20bca6495d06925631e201f2b257cc37086752e8fe7b6c83a67c6509f4759bc9"}, + {file = "onnxruntime-1.22.0-cp311-cp311-macosx_13_0_universal2.whl", hash = "sha256:8d6725c5b9a681d8fe72f2960c191a96c256367887d076b08466f52b4e0991df"}, + {file = "onnxruntime-1.22.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fef17d665a917866d1f68f09edc98223b9a27e6cb167dec69da4c66484ad12fd"}, + {file = "onnxruntime-1.22.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b978aa63a9a22095479c38371a9b359d4c15173cbb164eaad5f2cd27d666aa65"}, + {file = "onnxruntime-1.22.0-cp311-cp311-win_amd64.whl", hash = "sha256:03d3ef7fb11adf154149d6e767e21057e0e577b947dd3f66190b212528e1db31"}, + {file = "onnxruntime-1.22.0-cp312-cp312-macosx_13_0_universal2.whl", hash = "sha256:f3c0380f53c1e72a41b3f4d6af2ccc01df2c17844072233442c3a7e74851ab97"}, + {file = "onnxruntime-1.22.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8601128eaef79b636152aea76ae6981b7c9fc81a618f584c15d78d42b310f1c"}, + {file = "onnxruntime-1.22.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6964a975731afc19dc3418fad8d4e08c48920144ff590149429a5ebe0d15fb3c"}, + {file = "onnxruntime-1.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:c0d534a43d1264d1273c2d4f00a5a588fa98d21117a3345b7104fa0bbcaadb9a"}, + {file = "onnxruntime-1.22.0-cp313-cp313-macosx_13_0_universal2.whl", hash = "sha256:fe7c051236aae16d8e2e9ffbfc1e115a0cc2450e873a9c4cb75c0cc96c1dae07"}, + {file = "onnxruntime-1.22.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6a6bbed10bc5e770c04d422893d3045b81acbbadc9fb759a2cd1ca00993da919"}, + {file = "onnxruntime-1.22.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9fe45ee3e756300fccfd8d61b91129a121d3d80e9d38e01f03ff1295badc32b8"}, + {file = "onnxruntime-1.22.0-cp313-cp313-win_amd64.whl", hash = "sha256:5a31d84ef82b4b05d794a4ce8ba37b0d9deb768fd580e36e17b39e0b4840253b"}, + {file = "onnxruntime-1.22.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a2ac5bd9205d831541db4e508e586e764a74f14efdd3f89af7fd20e1bf4a1ed"}, + {file = "onnxruntime-1.22.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:64845709f9e8a2809e8e009bc4c8f73b788cee9c6619b7d9930344eae4c9cd36"}, +] + +[package.dependencies] +coloredlogs = "*" +flatbuffers = "*" +numpy = ">=1.21.6" +packaging = "*" +protobuf = "*" +sympy = "*" + +[[package]] +name = "opentelemetry-api" +version = "1.34.1" +description = "OpenTelemetry Python API" +optional = false +python-versions = ">=3.9" +files = [ + {file = "opentelemetry_api-1.34.1-py3-none-any.whl", hash = "sha256:b7df4cb0830d5a6c29ad0c0691dbae874d8daefa934b8b1d642de48323d32a8c"}, + {file = "opentelemetry_api-1.34.1.tar.gz", hash = "sha256:64f0bd06d42824843731d05beea88d4d4b6ae59f9fe347ff7dfa2cc14233bbb3"}, +] + +[package.dependencies] +importlib-metadata = ">=6.0,<8.8.0" +typing-extensions = ">=4.5.0" + +[[package]] +name = "opentelemetry-exporter-otlp-proto-common" +version = "1.34.1" +description = "OpenTelemetry Protobuf encoding" +optional = false +python-versions = ">=3.9" +files = [ + {file = "opentelemetry_exporter_otlp_proto_common-1.34.1-py3-none-any.whl", hash = "sha256:8e2019284bf24d3deebbb6c59c71e6eef3307cd88eff8c633e061abba33f7e87"}, + {file = "opentelemetry_exporter_otlp_proto_common-1.34.1.tar.gz", hash = "sha256:b59a20a927facd5eac06edaf87a07e49f9e4a13db487b7d8a52b37cb87710f8b"}, +] + +[package.dependencies] +opentelemetry-proto = "1.34.1" + +[[package]] +name = "opentelemetry-exporter-otlp-proto-grpc" +version = "1.34.1" +description = "OpenTelemetry Collector Protobuf over gRPC Exporter" +optional = false +python-versions = ">=3.9" +files = [ + {file = "opentelemetry_exporter_otlp_proto_grpc-1.34.1-py3-none-any.whl", hash = "sha256:04bb8b732b02295be79f8a86a4ad28fae3d4ddb07307a98c7aa6f331de18cca6"}, + {file = "opentelemetry_exporter_otlp_proto_grpc-1.34.1.tar.gz", hash = "sha256:7c841b90caa3aafcfc4fee58487a6c71743c34c6dc1787089d8b0578bbd794dd"}, +] + +[package.dependencies] +googleapis-common-protos = ">=1.52,<2.0" +grpcio = [ + {version = ">=1.63.2,<2.0.0", markers = "python_version < \"3.13\""}, + {version = ">=1.66.2,<2.0.0", markers = "python_version >= \"3.13\""}, +] +opentelemetry-api = ">=1.15,<2.0" +opentelemetry-exporter-otlp-proto-common = "1.34.1" +opentelemetry-proto = "1.34.1" +opentelemetry-sdk = ">=1.34.1,<1.35.0" +typing-extensions = ">=4.5.0" + +[[package]] +name = "opentelemetry-instrumentation" +version = "0.55b1" +description = "Instrumentation Tools & Auto Instrumentation for OpenTelemetry Python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "opentelemetry_instrumentation-0.55b1-py3-none-any.whl", hash = "sha256:cbb1496b42bc394e01bc63701b10e69094e8564e281de063e4328d122cc7a97e"}, + {file = "opentelemetry_instrumentation-0.55b1.tar.gz", hash = "sha256:2dc50aa207b9bfa16f70a1a0571e011e737a9917408934675b89ef4d5718c87b"}, +] + +[package.dependencies] +opentelemetry-api = ">=1.4,<2.0" +opentelemetry-semantic-conventions = "0.55b1" +packaging = ">=18.0" +wrapt = ">=1.0.0,<2.0.0" + +[[package]] +name = "opentelemetry-instrumentation-asgi" +version = "0.55b1" +description = "ASGI instrumentation for OpenTelemetry" +optional = false +python-versions = ">=3.9" +files = [ + {file = "opentelemetry_instrumentation_asgi-0.55b1-py3-none-any.whl", hash = "sha256:186620f7d0a71c8c817c5cbe91c80faa8f9c50967d458b8131c5694e21eb8583"}, + {file = "opentelemetry_instrumentation_asgi-0.55b1.tar.gz", hash = "sha256:615cde388dd3af4d0e52629a6c75828253618aebcc6e65d93068463811528606"}, +] + +[package.dependencies] +asgiref = ">=3.0,<4.0" +opentelemetry-api = ">=1.12,<2.0" +opentelemetry-instrumentation = "0.55b1" +opentelemetry-semantic-conventions = "0.55b1" +opentelemetry-util-http = "0.55b1" + +[package.extras] +instruments = ["asgiref (>=3.0,<4.0)"] + +[[package]] +name = "opentelemetry-instrumentation-fastapi" +version = "0.55b1" +description = "OpenTelemetry FastAPI Instrumentation" +optional = false +python-versions = ">=3.9" +files = [ + {file = "opentelemetry_instrumentation_fastapi-0.55b1-py3-none-any.whl", hash = "sha256:af4c09aebb0bd6b4a0881483b175e76547d2bc96329c94abfb794bf44f29f6bb"}, + {file = "opentelemetry_instrumentation_fastapi-0.55b1.tar.gz", hash = "sha256:bb9f8c13a053e7ff7da221248067529cc320e9308d57f3908de0afa36f6c5744"}, +] + +[package.dependencies] +opentelemetry-api = ">=1.12,<2.0" +opentelemetry-instrumentation = "0.55b1" +opentelemetry-instrumentation-asgi = "0.55b1" +opentelemetry-semantic-conventions = "0.55b1" +opentelemetry-util-http = "0.55b1" + +[package.extras] +instruments = ["fastapi (>=0.92,<1.0)"] + +[[package]] +name = "opentelemetry-proto" +version = "1.34.1" +description = "OpenTelemetry Python Proto" +optional = false +python-versions = ">=3.9" +files = [ + {file = "opentelemetry_proto-1.34.1-py3-none-any.whl", hash = "sha256:eb4bb5ac27f2562df2d6857fc557b3a481b5e298bc04f94cc68041f00cebcbd2"}, + {file = "opentelemetry_proto-1.34.1.tar.gz", hash = "sha256:16286214e405c211fc774187f3e4bbb1351290b8dfb88e8948af209ce85b719e"}, +] + +[package.dependencies] +protobuf = ">=5.0,<6.0" + +[[package]] +name = "opentelemetry-sdk" +version = "1.34.1" +description = "OpenTelemetry Python SDK" +optional = false +python-versions = ">=3.9" +files = [ + {file = "opentelemetry_sdk-1.34.1-py3-none-any.whl", hash = "sha256:308effad4059562f1d92163c61c8141df649da24ce361827812c40abb2a1e96e"}, + {file = "opentelemetry_sdk-1.34.1.tar.gz", hash = "sha256:8091db0d763fcd6098d4781bbc80ff0971f94e260739aa6afe6fd379cdf3aa4d"}, +] + +[package.dependencies] +opentelemetry-api = "1.34.1" +opentelemetry-semantic-conventions = "0.55b1" +typing-extensions = ">=4.5.0" + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.55b1" +description = "OpenTelemetry Semantic Conventions" +optional = false +python-versions = ">=3.9" +files = [ + {file = "opentelemetry_semantic_conventions-0.55b1-py3-none-any.whl", hash = "sha256:5da81dfdf7d52e3d37f8fe88d5e771e191de924cfff5f550ab0b8f7b2409baed"}, + {file = "opentelemetry_semantic_conventions-0.55b1.tar.gz", hash = "sha256:ef95b1f009159c28d7a7849f5cbc71c4c34c845bb514d66adfdf1b3fff3598b3"}, +] + +[package.dependencies] +opentelemetry-api = "1.34.1" +typing-extensions = ">=4.5.0" + +[[package]] +name = "opentelemetry-util-http" +version = "0.55b1" +description = "Web util for OpenTelemetry" +optional = false +python-versions = ">=3.9" +files = [ + {file = "opentelemetry_util_http-0.55b1-py3-none-any.whl", hash = "sha256:e134218df8ff010e111466650e5f019496b29c3b4f1b7de0e8ff8ebeafeebdf4"}, + {file = "opentelemetry_util_http-0.55b1.tar.gz", hash = "sha256:29e119c1f6796cccf5fc2aedb55274435cde5976d0ac3fec3ca20a80118f821e"}, +] + +[[package]] +name = "orjson" +version = "3.10.18" +description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" +optional = false +python-versions = ">=3.9" +files = [ + {file = "orjson-3.10.18-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a45e5d68066b408e4bc383b6e4ef05e717c65219a9e1390abc6155a520cac402"}, + {file = "orjson-3.10.18-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be3b9b143e8b9db05368b13b04c84d37544ec85bb97237b3a923f076265ec89c"}, + {file = "orjson-3.10.18-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9b0aa09745e2c9b3bf779b096fa71d1cc2d801a604ef6dd79c8b1bfef52b2f92"}, + {file = "orjson-3.10.18-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53a245c104d2792e65c8d225158f2b8262749ffe64bc7755b00024757d957a13"}, + {file = "orjson-3.10.18-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f9495ab2611b7f8a0a8a505bcb0f0cbdb5469caafe17b0e404c3c746f9900469"}, + {file = "orjson-3.10.18-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:73be1cbcebadeabdbc468f82b087df435843c809cd079a565fb16f0f3b23238f"}, + {file = "orjson-3.10.18-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe8936ee2679e38903df158037a2f1c108129dee218975122e37847fb1d4ac68"}, + {file = "orjson-3.10.18-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7115fcbc8525c74e4c2b608129bef740198e9a120ae46184dac7683191042056"}, + {file = "orjson-3.10.18-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:771474ad34c66bc4d1c01f645f150048030694ea5b2709b87d3bda273ffe505d"}, + {file = "orjson-3.10.18-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:7c14047dbbea52886dd87169f21939af5d55143dad22d10db6a7514f058156a8"}, + {file = "orjson-3.10.18-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:641481b73baec8db14fdf58f8967e52dc8bda1f2aba3aa5f5c1b07ed6df50b7f"}, + {file = "orjson-3.10.18-cp310-cp310-win32.whl", hash = "sha256:607eb3ae0909d47280c1fc657c4284c34b785bae371d007595633f4b1a2bbe06"}, + {file = "orjson-3.10.18-cp310-cp310-win_amd64.whl", hash = "sha256:8770432524ce0eca50b7efc2a9a5f486ee0113a5fbb4231526d414e6254eba92"}, + {file = "orjson-3.10.18-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:e0a183ac3b8e40471e8d843105da6fbe7c070faab023be3b08188ee3f85719b8"}, + {file = "orjson-3.10.18-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:5ef7c164d9174362f85238d0cd4afdeeb89d9e523e4651add6a5d458d6f7d42d"}, + {file = "orjson-3.10.18-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afd14c5d99cdc7bf93f22b12ec3b294931518aa019e2a147e8aa2f31fd3240f7"}, + {file = "orjson-3.10.18-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7b672502323b6cd133c4af6b79e3bea36bad2d16bca6c1f645903fce83909a7a"}, + {file = "orjson-3.10.18-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:51f8c63be6e070ec894c629186b1c0fe798662b8687f3d9fdfa5e401c6bd7679"}, + {file = "orjson-3.10.18-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f9478ade5313d724e0495d167083c6f3be0dd2f1c9c8a38db9a9e912cdaf947"}, + {file = "orjson-3.10.18-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:187aefa562300a9d382b4b4eb9694806e5848b0cedf52037bb5c228c61bb66d4"}, + {file = "orjson-3.10.18-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9da552683bc9da222379c7a01779bddd0ad39dd699dd6300abaf43eadee38334"}, + {file = "orjson-3.10.18-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e450885f7b47a0231979d9c49b567ed1c4e9f69240804621be87c40bc9d3cf17"}, + {file = "orjson-3.10.18-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:5e3c9cc2ba324187cd06287ca24f65528f16dfc80add48dc99fa6c836bb3137e"}, + {file = "orjson-3.10.18-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:50ce016233ac4bfd843ac5471e232b865271d7d9d44cf9d33773bcd883ce442b"}, + {file = "orjson-3.10.18-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b3ceff74a8f7ffde0b2785ca749fc4e80e4315c0fd887561144059fb1c138aa7"}, + {file = "orjson-3.10.18-cp311-cp311-win32.whl", hash = "sha256:fdba703c722bd868c04702cac4cb8c6b8ff137af2623bc0ddb3b3e6a2c8996c1"}, + {file = "orjson-3.10.18-cp311-cp311-win_amd64.whl", hash = "sha256:c28082933c71ff4bc6ccc82a454a2bffcef6e1d7379756ca567c772e4fb3278a"}, + {file = "orjson-3.10.18-cp311-cp311-win_arm64.whl", hash = "sha256:a6c7c391beaedd3fa63206e5c2b7b554196f14debf1ec9deb54b5d279b1b46f5"}, + {file = "orjson-3.10.18-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:50c15557afb7f6d63bc6d6348e0337a880a04eaa9cd7c9d569bcb4e760a24753"}, + {file = "orjson-3.10.18-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:356b076f1662c9813d5fa56db7d63ccceef4c271b1fb3dd522aca291375fcf17"}, + {file = "orjson-3.10.18-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:559eb40a70a7494cd5beab2d73657262a74a2c59aff2068fdba8f0424ec5b39d"}, + {file = "orjson-3.10.18-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f3c29eb9a81e2fbc6fd7ddcfba3e101ba92eaff455b8d602bf7511088bbc0eae"}, + {file = "orjson-3.10.18-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6612787e5b0756a171c7d81ba245ef63a3533a637c335aa7fcb8e665f4a0966f"}, + {file = "orjson-3.10.18-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ac6bd7be0dcab5b702c9d43d25e70eb456dfd2e119d512447468f6405b4a69c"}, + {file = "orjson-3.10.18-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9f72f100cee8dde70100406d5c1abba515a7df926d4ed81e20a9730c062fe9ad"}, + {file = "orjson-3.10.18-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9dca85398d6d093dd41dc0983cbf54ab8e6afd1c547b6b8a311643917fbf4e0c"}, + {file = "orjson-3.10.18-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:22748de2a07fcc8781a70edb887abf801bb6142e6236123ff93d12d92db3d406"}, + {file = "orjson-3.10.18-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:3a83c9954a4107b9acd10291b7f12a6b29e35e8d43a414799906ea10e75438e6"}, + {file = "orjson-3.10.18-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:303565c67a6c7b1f194c94632a4a39918e067bd6176a48bec697393865ce4f06"}, + {file = "orjson-3.10.18-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:86314fdb5053a2f5a5d881f03fca0219bfdf832912aa88d18676a5175c6916b5"}, + {file = "orjson-3.10.18-cp312-cp312-win32.whl", hash = "sha256:187ec33bbec58c76dbd4066340067d9ece6e10067bb0cc074a21ae3300caa84e"}, + {file = "orjson-3.10.18-cp312-cp312-win_amd64.whl", hash = "sha256:f9f94cf6d3f9cd720d641f8399e390e7411487e493962213390d1ae45c7814fc"}, + {file = "orjson-3.10.18-cp312-cp312-win_arm64.whl", hash = "sha256:3d600be83fe4514944500fa8c2a0a77099025ec6482e8087d7659e891f23058a"}, + {file = "orjson-3.10.18-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:69c34b9441b863175cc6a01f2935de994025e773f814412030f269da4f7be147"}, + {file = "orjson-3.10.18-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:1ebeda919725f9dbdb269f59bc94f861afbe2a27dce5608cdba2d92772364d1c"}, + {file = "orjson-3.10.18-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5adf5f4eed520a4959d29ea80192fa626ab9a20b2ea13f8f6dc58644f6927103"}, + {file = "orjson-3.10.18-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7592bb48a214e18cd670974f289520f12b7aed1fa0b2e2616b8ed9e069e08595"}, + {file = "orjson-3.10.18-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f872bef9f042734110642b7a11937440797ace8c87527de25e0c53558b579ccc"}, + {file = "orjson-3.10.18-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0315317601149c244cb3ecef246ef5861a64824ccbcb8018d32c66a60a84ffbc"}, + {file = "orjson-3.10.18-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e0da26957e77e9e55a6c2ce2e7182a36a6f6b180ab7189315cb0995ec362e049"}, + {file = "orjson-3.10.18-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb70d489bc79b7519e5803e2cc4c72343c9dc1154258adf2f8925d0b60da7c58"}, + {file = "orjson-3.10.18-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e9e86a6af31b92299b00736c89caf63816f70a4001e750bda179e15564d7a034"}, + {file = "orjson-3.10.18-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:c382a5c0b5931a5fc5405053d36c1ce3fd561694738626c77ae0b1dfc0242ca1"}, + {file = "orjson-3.10.18-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8e4b2ae732431127171b875cb2668f883e1234711d3c147ffd69fe5be51a8012"}, + {file = "orjson-3.10.18-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2d808e34ddb24fc29a4d4041dcfafbae13e129c93509b847b14432717d94b44f"}, + {file = "orjson-3.10.18-cp313-cp313-win32.whl", hash = "sha256:ad8eacbb5d904d5591f27dee4031e2c1db43d559edb8f91778efd642d70e6bea"}, + {file = "orjson-3.10.18-cp313-cp313-win_amd64.whl", hash = "sha256:aed411bcb68bf62e85588f2a7e03a6082cc42e5a2796e06e72a962d7c6310b52"}, + {file = "orjson-3.10.18-cp313-cp313-win_arm64.whl", hash = "sha256:f54c1385a0e6aba2f15a40d703b858bedad36ded0491e55d35d905b2c34a4cc3"}, + {file = "orjson-3.10.18-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:c95fae14225edfd699454e84f61c3dd938df6629a00c6ce15e704f57b58433bb"}, + {file = "orjson-3.10.18-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5232d85f177f98e0cefabb48b5e7f60cff6f3f0365f9c60631fecd73849b2a82"}, + {file = "orjson-3.10.18-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2783e121cafedf0d85c148c248a20470018b4ffd34494a68e125e7d5857655d1"}, + {file = "orjson-3.10.18-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e54ee3722caf3db09c91f442441e78f916046aa58d16b93af8a91500b7bbf273"}, + {file = "orjson-3.10.18-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2daf7e5379b61380808c24f6fc182b7719301739e4271c3ec88f2984a2d61f89"}, + {file = "orjson-3.10.18-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7f39b371af3add20b25338f4b29a8d6e79a8c7ed0e9dd49e008228a065d07781"}, + {file = "orjson-3.10.18-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b819ed34c01d88c6bec290e6842966f8e9ff84b7694632e88341363440d4cc0"}, + {file = "orjson-3.10.18-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2f6c57debaef0b1aa13092822cbd3698a1fb0209a9ea013a969f4efa36bdea57"}, + {file = "orjson-3.10.18-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:755b6d61ffdb1ffa1e768330190132e21343757c9aa2308c67257cc81a1a6f5a"}, + {file = "orjson-3.10.18-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ce8d0a875a85b4c8579eab5ac535fb4b2a50937267482be402627ca7e7570ee3"}, + {file = "orjson-3.10.18-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:57b5d0673cbd26781bebc2bf86f99dd19bd5a9cb55f71cc4f66419f6b50f3d77"}, + {file = "orjson-3.10.18-cp39-cp39-win32.whl", hash = "sha256:951775d8b49d1d16ca8818b1f20c4965cae9157e7b562a2ae34d3967b8f21c8e"}, + {file = "orjson-3.10.18-cp39-cp39-win_amd64.whl", hash = "sha256:fdd9d68f83f0bc4406610b1ac68bdcded8c5ee58605cc69e643a06f4d075f429"}, + {file = "orjson-3.10.18.tar.gz", hash = "sha256:e8da3947d92123eda795b68228cafe2724815621fe35e8e320a9e9593a4bcd53"}, +] + +[[package]] +name = "overrides" +version = "7.7.0" +description = "A decorator to automatically detect mismatch when overriding a method." +optional = false +python-versions = ">=3.6" +files = [ + {file = "overrides-7.7.0-py3-none-any.whl", hash = "sha256:c7ed9d062f78b8e4c1a7b70bd8796b35ead4d9f510227ef9c5dc7626c60d7e49"}, + {file = "overrides-7.7.0.tar.gz", hash = "sha256:55158fa3d93b98cc75299b1e67078ad9003ca27945c76162c1c0766d6f91820a"}, +] + +[[package]] +name = "packaging" +version = "25.0" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, + {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, +] + +[[package]] +name = "pandas" +version = "2.3.0" +description = "Powerful data structures for data analysis, time series, and statistics" +optional = false +python-versions = ">=3.9" +files = [ + {file = "pandas-2.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:625466edd01d43b75b1883a64d859168e4556261a5035b32f9d743b67ef44634"}, + {file = "pandas-2.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a6872d695c896f00df46b71648eea332279ef4077a409e2fe94220208b6bb675"}, + {file = "pandas-2.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4dd97c19bd06bc557ad787a15b6489d2614ddaab5d104a0310eb314c724b2d2"}, + {file = "pandas-2.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:034abd6f3db8b9880aaee98f4f5d4dbec7c4829938463ec046517220b2f8574e"}, + {file = "pandas-2.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:23c2b2dc5213810208ca0b80b8666670eb4660bbfd9d45f58592cc4ddcfd62e1"}, + {file = "pandas-2.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:39ff73ec07be5e90330cc6ff5705c651ace83374189dcdcb46e6ff54b4a72cd6"}, + {file = "pandas-2.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:40cecc4ea5abd2921682b57532baea5588cc5f80f0231c624056b146887274d2"}, + {file = "pandas-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8adff9f138fc614347ff33812046787f7d43b3cef7c0f0171b3340cae333f6ca"}, + {file = "pandas-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e5f08eb9a445d07720776df6e641975665c9ea12c9d8a331e0f6890f2dcd76ef"}, + {file = "pandas-2.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa35c266c8cd1a67d75971a1912b185b492d257092bdd2709bbdebe574ed228d"}, + {file = "pandas-2.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14a0cc77b0f089d2d2ffe3007db58f170dae9b9f54e569b299db871a3ab5bf46"}, + {file = "pandas-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c06f6f144ad0a1bf84699aeea7eff6068ca5c63ceb404798198af7eb86082e33"}, + {file = "pandas-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ed16339bc354a73e0a609df36d256672c7d296f3f767ac07257801aa064ff73c"}, + {file = "pandas-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:fa07e138b3f6c04addfeaf56cc7fdb96c3b68a3fe5e5401251f231fce40a0d7a"}, + {file = "pandas-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2eb4728a18dcd2908c7fccf74a982e241b467d178724545a48d0caf534b38ebf"}, + {file = "pandas-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9d8c3187be7479ea5c3d30c32a5d73d62a621166675063b2edd21bc47614027"}, + {file = "pandas-2.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9ff730713d4c4f2f1c860e36c005c7cefc1c7c80c21c0688fd605aa43c9fcf09"}, + {file = "pandas-2.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba24af48643b12ffe49b27065d3babd52702d95ab70f50e1b34f71ca703e2c0d"}, + {file = "pandas-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:404d681c698e3c8a40a61d0cd9412cc7364ab9a9cc6e144ae2992e11a2e77a20"}, + {file = "pandas-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6021910b086b3ca756755e86ddc64e0ddafd5e58e076c72cb1585162e5ad259b"}, + {file = "pandas-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:094e271a15b579650ebf4c5155c05dcd2a14fd4fdd72cf4854b2f7ad31ea30be"}, + {file = "pandas-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c7e2fc25f89a49a11599ec1e76821322439d90820108309bf42130d2f36c983"}, + {file = "pandas-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c6da97aeb6a6d233fb6b17986234cc723b396b50a3c6804776351994f2a658fd"}, + {file = "pandas-2.3.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb32dc743b52467d488e7a7c8039b821da2826a9ba4f85b89ea95274f863280f"}, + {file = "pandas-2.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:213cd63c43263dbb522c1f8a7c9d072e25900f6975596f883f4bebd77295d4f3"}, + {file = "pandas-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1d2b33e68d0ce64e26a4acc2e72d747292084f4e8db4c847c6f5f6cbe56ed6d8"}, + {file = "pandas-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:430a63bae10b5086995db1b02694996336e5a8ac9a96b4200572b413dfdfccb9"}, + {file = "pandas-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:4930255e28ff5545e2ca404637bcc56f031893142773b3468dc021c6c32a1390"}, + {file = "pandas-2.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:f925f1ef673b4bd0271b1809b72b3270384f2b7d9d14a189b12b7fc02574d575"}, + {file = "pandas-2.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78ad363ddb873a631e92a3c063ade1ecfb34cae71e9a2be6ad100f875ac1042"}, + {file = "pandas-2.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:951805d146922aed8357e4cc5671b8b0b9be1027f0619cea132a9f3f65f2f09c"}, + {file = "pandas-2.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a881bc1309f3fce34696d07b00f13335c41f5f5a8770a33b09ebe23261cfc67"}, + {file = "pandas-2.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e1991bbb96f4050b09b5f811253c4f3cf05ee89a589379aa36cd623f21a31d6f"}, + {file = "pandas-2.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:bb3be958022198531eb7ec2008cfc78c5b1eed51af8600c6c5d9160d89d8d249"}, + {file = "pandas-2.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9efc0acbbffb5236fbdf0409c04edce96bec4bdaa649d49985427bd1ec73e085"}, + {file = "pandas-2.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:75651c14fde635e680496148a8526b328e09fe0572d9ae9b638648c46a544ba3"}, + {file = "pandas-2.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf5be867a0541a9fb47a4be0c5790a4bccd5b77b92f0a59eeec9375fafc2aa14"}, + {file = "pandas-2.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84141f722d45d0c2a89544dd29d35b3abfc13d2250ed7e68394eda7564bd6324"}, + {file = "pandas-2.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f95a2aef32614ed86216d3c450ab12a4e82084e8102e355707a1d96e33d51c34"}, + {file = "pandas-2.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e0f51973ba93a9f97185049326d75b942b9aeb472bec616a129806facb129ebb"}, + {file = "pandas-2.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:b198687ca9c8529662213538a9bb1e60fa0bf0f6af89292eb68fea28743fcd5a"}, + {file = "pandas-2.3.0.tar.gz", hash = "sha256:34600ab34ebf1131a7613a260a61dbe8b62c188ec0ea4c296da7c9a06b004133"}, +] + +[package.dependencies] +numpy = [ + {version = ">=1.23.2", markers = "python_version == \"3.11\""}, + {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, +] +python-dateutil = ">=2.8.2" +pytz = ">=2020.1" +tzdata = ">=2022.7" + +[package.extras] +all = ["PyQt5 (>=5.15.9)", "SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)", "beautifulsoup4 (>=4.11.2)", "bottleneck (>=1.3.6)", "dataframe-api-compat (>=0.1.7)", "fastparquet (>=2022.12.0)", "fsspec (>=2022.11.0)", "gcsfs (>=2022.11.0)", "html5lib (>=1.1)", "hypothesis (>=6.46.1)", "jinja2 (>=3.1.2)", "lxml (>=4.9.2)", "matplotlib (>=3.6.3)", "numba (>=0.56.4)", "numexpr (>=2.8.4)", "odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "pandas-gbq (>=0.19.0)", "psycopg2 (>=2.9.6)", "pyarrow (>=10.0.1)", "pymysql (>=1.0.2)", "pyreadstat (>=1.2.0)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "qtpy (>=2.3.0)", "s3fs (>=2022.11.0)", "scipy (>=1.10.0)", "tables (>=3.8.0)", "tabulate (>=0.9.0)", "xarray (>=2022.12.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)", "zstandard (>=0.19.0)"] +aws = ["s3fs (>=2022.11.0)"] +clipboard = ["PyQt5 (>=5.15.9)", "qtpy (>=2.3.0)"] +compression = ["zstandard (>=0.19.0)"] +computation = ["scipy (>=1.10.0)", "xarray (>=2022.12.0)"] +consortium-standard = ["dataframe-api-compat (>=0.1.7)"] +excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)"] +feather = ["pyarrow (>=10.0.1)"] +fss = ["fsspec (>=2022.11.0)"] +gcp = ["gcsfs (>=2022.11.0)", "pandas-gbq (>=0.19.0)"] +hdf5 = ["tables (>=3.8.0)"] +html = ["beautifulsoup4 (>=4.11.2)", "html5lib (>=1.1)", "lxml (>=4.9.2)"] +mysql = ["SQLAlchemy (>=2.0.0)", "pymysql (>=1.0.2)"] +output-formatting = ["jinja2 (>=3.1.2)", "tabulate (>=0.9.0)"] +parquet = ["pyarrow (>=10.0.1)"] +performance = ["bottleneck (>=1.3.6)", "numba (>=0.56.4)", "numexpr (>=2.8.4)"] +plot = ["matplotlib (>=3.6.3)"] +postgresql = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "psycopg2 (>=2.9.6)"] +pyarrow = ["pyarrow (>=10.0.1)"] +spss = ["pyreadstat (>=1.2.0)"] +sql-other = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)"] +test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)"] +xml = ["lxml (>=4.9.2)"] + +[[package]] +name = "pillow" +version = "11.2.1" +description = "Python Imaging Library (Fork)" +optional = false +python-versions = ">=3.9" +files = [ + {file = "pillow-11.2.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:d57a75d53922fc20c165016a20d9c44f73305e67c351bbc60d1adaf662e74047"}, + {file = "pillow-11.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:127bf6ac4a5b58b3d32fc8289656f77f80567d65660bc46f72c0d77e6600cc95"}, + {file = "pillow-11.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4ba4be812c7a40280629e55ae0b14a0aafa150dd6451297562e1764808bbe61"}, + {file = "pillow-11.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8bd62331e5032bc396a93609982a9ab6b411c05078a52f5fe3cc59234a3abd1"}, + {file = "pillow-11.2.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:562d11134c97a62fe3af29581f083033179f7ff435f78392565a1ad2d1c2c45c"}, + {file = "pillow-11.2.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:c97209e85b5be259994eb5b69ff50c5d20cca0f458ef9abd835e262d9d88b39d"}, + {file = "pillow-11.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0c3e6d0f59171dfa2e25d7116217543310908dfa2770aa64b8f87605f8cacc97"}, + {file = "pillow-11.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc1c3bc53befb6096b84165956e886b1729634a799e9d6329a0c512ab651e579"}, + {file = "pillow-11.2.1-cp310-cp310-win32.whl", hash = "sha256:312c77b7f07ab2139924d2639860e084ec2a13e72af54d4f08ac843a5fc9c79d"}, + {file = "pillow-11.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:9bc7ae48b8057a611e5fe9f853baa88093b9a76303937449397899385da06fad"}, + {file = "pillow-11.2.1-cp310-cp310-win_arm64.whl", hash = "sha256:2728567e249cdd939f6cc3d1f049595c66e4187f3c34078cbc0a7d21c47482d2"}, + {file = "pillow-11.2.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:35ca289f712ccfc699508c4658a1d14652e8033e9b69839edf83cbdd0ba39e70"}, + {file = "pillow-11.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0409af9f829f87a2dfb7e259f78f317a5351f2045158be321fd135973fff7bf"}, + {file = "pillow-11.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4e5c5edee874dce4f653dbe59db7c73a600119fbea8d31f53423586ee2aafd7"}, + {file = "pillow-11.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b93a07e76d13bff9444f1a029e0af2964e654bfc2e2c2d46bfd080df5ad5f3d8"}, + {file = "pillow-11.2.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:e6def7eed9e7fa90fde255afaf08060dc4b343bbe524a8f69bdd2a2f0018f600"}, + {file = "pillow-11.2.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:8f4f3724c068be008c08257207210c138d5f3731af6c155a81c2b09a9eb3a788"}, + {file = "pillow-11.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a0a6709b47019dff32e678bc12c63008311b82b9327613f534e496dacaefb71e"}, + {file = "pillow-11.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f6b0c664ccb879109ee3ca702a9272d877f4fcd21e5eb63c26422fd6e415365e"}, + {file = "pillow-11.2.1-cp311-cp311-win32.whl", hash = "sha256:cc5d875d56e49f112b6def6813c4e3d3036d269c008bf8aef72cd08d20ca6df6"}, + {file = "pillow-11.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:0f5c7eda47bf8e3c8a283762cab94e496ba977a420868cb819159980b6709193"}, + {file = "pillow-11.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:4d375eb838755f2528ac8cbc926c3e31cc49ca4ad0cf79cff48b20e30634a4a7"}, + {file = "pillow-11.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:78afba22027b4accef10dbd5eed84425930ba41b3ea0a86fa8d20baaf19d807f"}, + {file = "pillow-11.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78092232a4ab376a35d68c4e6d5e00dfd73454bd12b230420025fbe178ee3b0b"}, + {file = "pillow-11.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25a5f306095c6780c52e6bbb6109624b95c5b18e40aab1c3041da3e9e0cd3e2d"}, + {file = "pillow-11.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c7b29dbd4281923a2bfe562acb734cee96bbb129e96e6972d315ed9f232bef4"}, + {file = "pillow-11.2.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3e645b020f3209a0181a418bffe7b4a93171eef6c4ef6cc20980b30bebf17b7d"}, + {file = "pillow-11.2.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b2dbea1012ccb784a65349f57bbc93730b96e85b42e9bf7b01ef40443db720b4"}, + {file = "pillow-11.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:da3104c57bbd72948d75f6a9389e6727d2ab6333c3617f0a89d72d4940aa0443"}, + {file = "pillow-11.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:598174aef4589af795f66f9caab87ba4ff860ce08cd5bb447c6fc553ffee603c"}, + {file = "pillow-11.2.1-cp312-cp312-win32.whl", hash = "sha256:1d535df14716e7f8776b9e7fee118576d65572b4aad3ed639be9e4fa88a1cad3"}, + {file = "pillow-11.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:14e33b28bf17c7a38eede290f77db7c664e4eb01f7869e37fa98a5aa95978941"}, + {file = "pillow-11.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:21e1470ac9e5739ff880c211fc3af01e3ae505859392bf65458c224d0bf283eb"}, + {file = "pillow-11.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fdec757fea0b793056419bca3e9932eb2b0ceec90ef4813ea4c1e072c389eb28"}, + {file = "pillow-11.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0e130705d568e2f43a17bcbe74d90958e8a16263868a12c3e0d9c8162690830"}, + {file = "pillow-11.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bdb5e09068332578214cadd9c05e3d64d99e0e87591be22a324bdbc18925be0"}, + {file = "pillow-11.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d189ba1bebfbc0c0e529159631ec72bb9e9bc041f01ec6d3233d6d82eb823bc1"}, + {file = "pillow-11.2.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:191955c55d8a712fab8934a42bfefbf99dd0b5875078240943f913bb66d46d9f"}, + {file = "pillow-11.2.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:ad275964d52e2243430472fc5d2c2334b4fc3ff9c16cb0a19254e25efa03a155"}, + {file = "pillow-11.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:750f96efe0597382660d8b53e90dd1dd44568a8edb51cb7f9d5d918b80d4de14"}, + {file = "pillow-11.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fe15238d3798788d00716637b3d4e7bb6bde18b26e5d08335a96e88564a36b6b"}, + {file = "pillow-11.2.1-cp313-cp313-win32.whl", hash = "sha256:3fe735ced9a607fee4f481423a9c36701a39719252a9bb251679635f99d0f7d2"}, + {file = "pillow-11.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:74ee3d7ecb3f3c05459ba95eed5efa28d6092d751ce9bf20e3e253a4e497e691"}, + {file = "pillow-11.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:5119225c622403afb4b44bad4c1ca6c1f98eed79db8d3bc6e4e160fc6339d66c"}, + {file = "pillow-11.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8ce2e8411c7aaef53e6bb29fe98f28cd4fbd9a1d9be2eeea434331aac0536b22"}, + {file = "pillow-11.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9ee66787e095127116d91dea2143db65c7bb1e232f617aa5957c0d9d2a3f23a7"}, + {file = "pillow-11.2.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9622e3b6c1d8b551b6e6f21873bdcc55762b4b2126633014cea1803368a9aa16"}, + {file = "pillow-11.2.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63b5dff3a68f371ea06025a1a6966c9a1e1ee452fc8020c2cd0ea41b83e9037b"}, + {file = "pillow-11.2.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:31df6e2d3d8fc99f993fd253e97fae451a8db2e7207acf97859732273e108406"}, + {file = "pillow-11.2.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:062b7a42d672c45a70fa1f8b43d1d38ff76b63421cbbe7f88146b39e8a558d91"}, + {file = "pillow-11.2.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4eb92eca2711ef8be42fd3f67533765d9fd043b8c80db204f16c8ea62ee1a751"}, + {file = "pillow-11.2.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f91ebf30830a48c825590aede79376cb40f110b387c17ee9bd59932c961044f9"}, + {file = "pillow-11.2.1-cp313-cp313t-win32.whl", hash = "sha256:e0b55f27f584ed623221cfe995c912c61606be8513bfa0e07d2c674b4516d9dd"}, + {file = "pillow-11.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:36d6b82164c39ce5482f649b437382c0fb2395eabc1e2b1702a6deb8ad647d6e"}, + {file = "pillow-11.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:225c832a13326e34f212d2072982bb1adb210e0cc0b153e688743018c94a2681"}, + {file = "pillow-11.2.1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:7491cf8a79b8eb867d419648fff2f83cb0b3891c8b36da92cc7f1931d46108c8"}, + {file = "pillow-11.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8b02d8f9cb83c52578a0b4beadba92e37d83a4ef11570a8688bbf43f4ca50909"}, + {file = "pillow-11.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:014ca0050c85003620526b0ac1ac53f56fc93af128f7546623cc8e31875ab928"}, + {file = "pillow-11.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3692b68c87096ac6308296d96354eddd25f98740c9d2ab54e1549d6c8aea9d79"}, + {file = "pillow-11.2.1-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:f781dcb0bc9929adc77bad571b8621ecb1e4cdef86e940fe2e5b5ee24fd33b35"}, + {file = "pillow-11.2.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:2b490402c96f907a166615e9a5afacf2519e28295f157ec3a2bb9bd57de638cb"}, + {file = "pillow-11.2.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dd6b20b93b3ccc9c1b597999209e4bc5cf2853f9ee66e3fc9a400a78733ffc9a"}, + {file = "pillow-11.2.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:4b835d89c08a6c2ee7781b8dd0a30209a8012b5f09c0a665b65b0eb3560b6f36"}, + {file = "pillow-11.2.1-cp39-cp39-win32.whl", hash = "sha256:b10428b3416d4f9c61f94b494681280be7686bda15898a3a9e08eb66a6d92d67"}, + {file = "pillow-11.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:6ebce70c3f486acf7591a3d73431fa504a4e18a9b97ff27f5f47b7368e4b9dd1"}, + {file = "pillow-11.2.1-cp39-cp39-win_arm64.whl", hash = "sha256:c27476257b2fdcd7872d54cfd119b3a9ce4610fb85c8e32b70b42e3680a29a1e"}, + {file = "pillow-11.2.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:9b7b0d4fd2635f54ad82785d56bc0d94f147096493a79985d0ab57aedd563156"}, + {file = "pillow-11.2.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:aa442755e31c64037aa7c1cb186e0b369f8416c567381852c63444dd666fb772"}, + {file = "pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0d3348c95b766f54b76116d53d4cb171b52992a1027e7ca50c81b43b9d9e363"}, + {file = "pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85d27ea4c889342f7e35f6d56e7e1cb345632ad592e8c51b693d7b7556043ce0"}, + {file = "pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bf2c33d6791c598142f00c9c4c7d47f6476731c31081331664eb26d6ab583e01"}, + {file = "pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e616e7154c37669fc1dfc14584f11e284e05d1c650e1c0f972f281c4ccc53193"}, + {file = "pillow-11.2.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:39ad2e0f424394e3aebc40168845fee52df1394a4673a6ee512d840d14ab3013"}, + {file = "pillow-11.2.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:80f1df8dbe9572b4b7abdfa17eb5d78dd620b1d55d9e25f834efdbee872d3aed"}, + {file = "pillow-11.2.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ea926cfbc3957090becbcbbb65ad177161a2ff2ad578b5a6ec9bb1e1cd78753c"}, + {file = "pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:738db0e0941ca0376804d4de6a782c005245264edaa253ffce24e5a15cbdc7bd"}, + {file = "pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db98ab6565c69082ec9b0d4e40dd9f6181dab0dd236d26f7a50b8b9bfbd5076"}, + {file = "pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:036e53f4170e270ddb8797d4c590e6dd14d28e15c7da375c18978045f7e6c37b"}, + {file = "pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:14f73f7c291279bd65fda51ee87affd7c1e097709f7fdd0188957a16c264601f"}, + {file = "pillow-11.2.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:208653868d5c9ecc2b327f9b9ef34e0e42a4cdd172c2988fd81d62d2bc9bc044"}, + {file = "pillow-11.2.1.tar.gz", hash = "sha256:a64dd61998416367b7ef979b73d3a85853ba9bec4c2925f74e588879a58716b6"}, +] + +[package.extras] +docs = ["furo", "olefile", "sphinx (>=8.2)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"] +fpx = ["olefile"] +mic = ["olefile"] +test-arrow = ["pyarrow"] +tests = ["check-manifest", "coverage (>=7.4.2)", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout", "trove-classifiers (>=2024.10.12)"] +typing = ["typing-extensions"] +xmp = ["defusedxml"] + +[[package]] +name = "posthog" +version = "4.10.0" +description = "Integrate PostHog into any python application." +optional = false +python-versions = ">=3.9" +files = [ + {file = "posthog-4.10.0-py3-none-any.whl", hash = "sha256:b693d3d8209d000d8c5f4d6ea19096bfdfb83047fa8a14c937ae50a3394809a1"}, + {file = "posthog-4.10.0.tar.gz", hash = "sha256:513bfbb21344013294abc046b1142173189c5422a3906cf2280d1389b0c2e28b"}, +] + +[package.dependencies] +backoff = ">=1.10.0" +distro = ">=1.5.0" +python-dateutil = ">=2.2" +requests = ">=2.7,<3.0" +six = ">=1.5" + +[package.extras] +dev = ["django-stubs", "lxml", "mypy", "mypy-baseline", "packaging", "pre-commit", "pydantic", "ruff", "setuptools", "tomli", "tomli_w", "twine", "types-mock", "types-python-dateutil", "types-requests", "types-setuptools", "types-six", "wheel"] +langchain = ["langchain (>=0.2.0)"] +sentry = ["django", "sentry-sdk"] +test = ["anthropic", "coverage", "django", "freezegun (==1.5.1)", "google-genai", "langchain-anthropic (>=0.3.15)", "langchain-community (>=0.3.25)", "langchain-core (>=0.3.65)", "langchain-openai (>=0.3.22)", "langgraph (>=0.4.8)", "mock (>=2.0.0)", "openai", "parameterized (>=0.8.1)", "pydantic", "pytest", "pytest-asyncio", "pytest-timeout"] + +[[package]] +name = "propcache" +version = "0.3.2" +description = "Accelerated property cache" +optional = false +python-versions = ">=3.9" +files = [ + {file = "propcache-0.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:22d9962a358aedbb7a2e36187ff273adeaab9743373a272976d2e348d08c7770"}, + {file = "propcache-0.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0d0fda578d1dc3f77b6b5a5dce3b9ad69a8250a891760a548df850a5e8da87f3"}, + {file = "propcache-0.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3def3da3ac3ce41562d85db655d18ebac740cb3fa4367f11a52b3da9d03a5cc3"}, + {file = "propcache-0.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9bec58347a5a6cebf239daba9bda37dffec5b8d2ce004d9fe4edef3d2815137e"}, + {file = "propcache-0.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55ffda449a507e9fbd4aca1a7d9aa6753b07d6166140e5a18d2ac9bc49eac220"}, + {file = "propcache-0.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64a67fb39229a8a8491dd42f864e5e263155e729c2e7ff723d6e25f596b1e8cb"}, + {file = "propcache-0.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9da1cf97b92b51253d5b68cf5a2b9e0dafca095e36b7f2da335e27dc6172a614"}, + {file = "propcache-0.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5f559e127134b07425134b4065be45b166183fdcb433cb6c24c8e4149056ad50"}, + {file = "propcache-0.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:aff2e4e06435d61f11a428360a932138d0ec288b0a31dd9bd78d200bd4a2b339"}, + {file = "propcache-0.3.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:4927842833830942a5d0a56e6f4839bc484785b8e1ce8d287359794818633ba0"}, + {file = "propcache-0.3.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:6107ddd08b02654a30fb8ad7a132021759d750a82578b94cd55ee2772b6ebea2"}, + {file = "propcache-0.3.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:70bd8b9cd6b519e12859c99f3fc9a93f375ebd22a50296c3a295028bea73b9e7"}, + {file = "propcache-0.3.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2183111651d710d3097338dd1893fcf09c9f54e27ff1a8795495a16a469cc90b"}, + {file = "propcache-0.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:fb075ad271405dcad8e2a7ffc9a750a3bf70e533bd86e89f0603e607b93aa64c"}, + {file = "propcache-0.3.2-cp310-cp310-win32.whl", hash = "sha256:404d70768080d3d3bdb41d0771037da19d8340d50b08e104ca0e7f9ce55fce70"}, + {file = "propcache-0.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:7435d766f978b4ede777002e6b3b6641dd229cd1da8d3d3106a45770365f9ad9"}, + {file = "propcache-0.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0b8d2f607bd8f80ddc04088bc2a037fdd17884a6fcadc47a96e334d72f3717be"}, + {file = "propcache-0.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06766d8f34733416e2e34f46fea488ad5d60726bb9481d3cddf89a6fa2d9603f"}, + {file = "propcache-0.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2dc1f4a1df4fecf4e6f68013575ff4af84ef6f478fe5344317a65d38a8e6dc9"}, + {file = "propcache-0.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be29c4f4810c5789cf10ddf6af80b041c724e629fa51e308a7a0fb19ed1ef7bf"}, + {file = "propcache-0.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59d61f6970ecbd8ff2e9360304d5c8876a6abd4530cb752c06586849ac8a9dc9"}, + {file = "propcache-0.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:62180e0b8dbb6b004baec00a7983e4cc52f5ada9cd11f48c3528d8cfa7b96a66"}, + {file = "propcache-0.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c144ca294a204c470f18cf4c9d78887810d04a3e2fbb30eea903575a779159df"}, + {file = "propcache-0.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5c2a784234c28854878d68978265617aa6dc0780e53d44b4d67f3651a17a9a2"}, + {file = "propcache-0.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5745bc7acdafa978ca1642891b82c19238eadc78ba2aaa293c6863b304e552d7"}, + {file = "propcache-0.3.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:c0075bf773d66fa8c9d41f66cc132ecc75e5bb9dd7cce3cfd14adc5ca184cb95"}, + {file = "propcache-0.3.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5f57aa0847730daceff0497f417c9de353c575d8da3579162cc74ac294c5369e"}, + {file = "propcache-0.3.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:eef914c014bf72d18efb55619447e0aecd5fb7c2e3fa7441e2e5d6099bddff7e"}, + {file = "propcache-0.3.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2a4092e8549031e82facf3decdbc0883755d5bbcc62d3aea9d9e185549936dcf"}, + {file = "propcache-0.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:85871b050f174bc0bfb437efbdb68aaf860611953ed12418e4361bc9c392749e"}, + {file = "propcache-0.3.2-cp311-cp311-win32.whl", hash = "sha256:36c8d9b673ec57900c3554264e630d45980fd302458e4ac801802a7fd2ef7897"}, + {file = "propcache-0.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53af8cb6a781b02d2ea079b5b853ba9430fcbe18a8e3ce647d5982a3ff69f39"}, + {file = "propcache-0.3.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8de106b6c84506b31c27168582cd3cb3000a6412c16df14a8628e5871ff83c10"}, + {file = "propcache-0.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:28710b0d3975117239c76600ea351934ac7b5ff56e60953474342608dbbb6154"}, + {file = "propcache-0.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce26862344bdf836650ed2487c3d724b00fbfec4233a1013f597b78c1cb73615"}, + {file = "propcache-0.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bca54bd347a253af2cf4544bbec232ab982f4868de0dd684246b67a51bc6b1db"}, + {file = "propcache-0.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55780d5e9a2ddc59711d727226bb1ba83a22dd32f64ee15594b9392b1f544eb1"}, + {file = "propcache-0.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:035e631be25d6975ed87ab23153db6a73426a48db688070d925aa27e996fe93c"}, + {file = "propcache-0.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee6f22b6eaa39297c751d0e80c0d3a454f112f5c6481214fcf4c092074cecd67"}, + {file = "propcache-0.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7ca3aee1aa955438c4dba34fc20a9f390e4c79967257d830f137bd5a8a32ed3b"}, + {file = "propcache-0.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7a4f30862869fa2b68380d677cc1c5fcf1e0f2b9ea0cf665812895c75d0ca3b8"}, + {file = "propcache-0.3.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b77ec3c257d7816d9f3700013639db7491a434644c906a2578a11daf13176251"}, + {file = "propcache-0.3.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cab90ac9d3f14b2d5050928483d3d3b8fb6b4018893fc75710e6aa361ecb2474"}, + {file = "propcache-0.3.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0b504d29f3c47cf6b9e936c1852246c83d450e8e063d50562115a6be6d3a2535"}, + {file = "propcache-0.3.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:ce2ac2675a6aa41ddb2a0c9cbff53780a617ac3d43e620f8fd77ba1c84dcfc06"}, + {file = "propcache-0.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b4239611205294cc433845b914131b2a1f03500ff3c1ed093ed216b82621e1"}, + {file = "propcache-0.3.2-cp312-cp312-win32.whl", hash = "sha256:df4a81b9b53449ebc90cc4deefb052c1dd934ba85012aa912c7ea7b7e38b60c1"}, + {file = "propcache-0.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:7046e79b989d7fe457bb755844019e10f693752d169076138abf17f31380800c"}, + {file = "propcache-0.3.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ca592ed634a73ca002967458187109265e980422116c0a107cf93d81f95af945"}, + {file = "propcache-0.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9ecb0aad4020e275652ba3975740f241bd12a61f1a784df044cf7477a02bc252"}, + {file = "propcache-0.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7f08f1cc28bd2eade7a8a3d2954ccc673bb02062e3e7da09bc75d843386b342f"}, + {file = "propcache-0.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1a342c834734edb4be5ecb1e9fb48cb64b1e2320fccbd8c54bf8da8f2a84c33"}, + {file = "propcache-0.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a544caaae1ac73f1fecfae70ded3e93728831affebd017d53449e3ac052ac1e"}, + {file = "propcache-0.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:310d11aa44635298397db47a3ebce7db99a4cc4b9bbdfcf6c98a60c8d5261cf1"}, + {file = "propcache-0.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c1396592321ac83157ac03a2023aa6cc4a3cc3cfdecb71090054c09e5a7cce3"}, + {file = "propcache-0.3.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cabf5b5902272565e78197edb682017d21cf3b550ba0460ee473753f28d23c1"}, + {file = "propcache-0.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0a2f2235ac46a7aa25bdeb03a9e7060f6ecbd213b1f9101c43b3090ffb971ef6"}, + {file = "propcache-0.3.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:92b69e12e34869a6970fd2f3da91669899994b47c98f5d430b781c26f1d9f387"}, + {file = "propcache-0.3.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:54e02207c79968ebbdffc169591009f4474dde3b4679e16634d34c9363ff56b4"}, + {file = "propcache-0.3.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4adfb44cb588001f68c5466579d3f1157ca07f7504fc91ec87862e2b8e556b88"}, + {file = "propcache-0.3.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fd3e6019dc1261cd0291ee8919dd91fbab7b169bb76aeef6c716833a3f65d206"}, + {file = "propcache-0.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4c181cad81158d71c41a2bce88edce078458e2dd5ffee7eddd6b05da85079f43"}, + {file = "propcache-0.3.2-cp313-cp313-win32.whl", hash = "sha256:8a08154613f2249519e549de2330cf8e2071c2887309a7b07fb56098f5170a02"}, + {file = "propcache-0.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e41671f1594fc4ab0a6dec1351864713cb3a279910ae8b58f884a88a0a632c05"}, + {file = "propcache-0.3.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:9a3cf035bbaf035f109987d9d55dc90e4b0e36e04bbbb95af3055ef17194057b"}, + {file = "propcache-0.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:156c03d07dc1323d8dacaa221fbe028c5c70d16709cdd63502778e6c3ccca1b0"}, + {file = "propcache-0.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74413c0ba02ba86f55cf60d18daab219f7e531620c15f1e23d95563f505efe7e"}, + {file = "propcache-0.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f066b437bb3fa39c58ff97ab2ca351db465157d68ed0440abecb21715eb24b28"}, + {file = "propcache-0.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1304b085c83067914721e7e9d9917d41ad87696bf70f0bc7dee450e9c71ad0a"}, + {file = "propcache-0.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ab50cef01b372763a13333b4e54021bdcb291fc9a8e2ccb9c2df98be51bcde6c"}, + {file = "propcache-0.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fad3b2a085ec259ad2c2842666b2a0a49dea8463579c606426128925af1ed725"}, + {file = "propcache-0.3.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:261fa020c1c14deafd54c76b014956e2f86991af198c51139faf41c4d5e83892"}, + {file = "propcache-0.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:46d7f8aa79c927e5f987ee3a80205c987717d3659f035c85cf0c3680526bdb44"}, + {file = "propcache-0.3.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:6d8f3f0eebf73e3c0ff0e7853f68be638b4043c65a70517bb575eff54edd8dbe"}, + {file = "propcache-0.3.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:03c89c1b14a5452cf15403e291c0ccd7751d5b9736ecb2c5bab977ad6c5bcd81"}, + {file = "propcache-0.3.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:0cc17efde71e12bbaad086d679ce575268d70bc123a5a71ea7ad76f70ba30bba"}, + {file = "propcache-0.3.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:acdf05d00696bc0447e278bb53cb04ca72354e562cf88ea6f9107df8e7fd9770"}, + {file = "propcache-0.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4445542398bd0b5d32df908031cb1b30d43ac848e20470a878b770ec2dcc6330"}, + {file = "propcache-0.3.2-cp313-cp313t-win32.whl", hash = "sha256:f86e5d7cd03afb3a1db8e9f9f6eff15794e79e791350ac48a8c924e6f439f394"}, + {file = "propcache-0.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9704bedf6e7cbe3c65eca4379a9b53ee6a83749f047808cbb5044d40d7d72198"}, + {file = "propcache-0.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a7fad897f14d92086d6b03fdd2eb844777b0c4d7ec5e3bac0fbae2ab0602bbe5"}, + {file = "propcache-0.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1f43837d4ca000243fd7fd6301947d7cb93360d03cd08369969450cc6b2ce3b4"}, + {file = "propcache-0.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:261df2e9474a5949c46e962065d88eb9b96ce0f2bd30e9d3136bcde84befd8f2"}, + {file = "propcache-0.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e514326b79e51f0a177daab1052bc164d9d9e54133797a3a58d24c9c87a3fe6d"}, + {file = "propcache-0.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d4a996adb6904f85894570301939afeee65f072b4fd265ed7e569e8d9058e4ec"}, + {file = "propcache-0.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:76cace5d6b2a54e55b137669b30f31aa15977eeed390c7cbfb1dafa8dfe9a701"}, + {file = "propcache-0.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31248e44b81d59d6addbb182c4720f90b44e1efdc19f58112a3c3a1615fb47ef"}, + {file = "propcache-0.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abb7fa19dbf88d3857363e0493b999b8011eea856b846305d8c0512dfdf8fbb1"}, + {file = "propcache-0.3.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:d81ac3ae39d38588ad0549e321e6f773a4e7cc68e7751524a22885d5bbadf886"}, + {file = "propcache-0.3.2-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:cc2782eb0f7a16462285b6f8394bbbd0e1ee5f928034e941ffc444012224171b"}, + {file = "propcache-0.3.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:db429c19a6c7e8a1c320e6a13c99799450f411b02251fb1b75e6217cf4a14fcb"}, + {file = "propcache-0.3.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:21d8759141a9e00a681d35a1f160892a36fb6caa715ba0b832f7747da48fb6ea"}, + {file = "propcache-0.3.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2ca6d378f09adb13837614ad2754fa8afaee330254f404299611bce41a8438cb"}, + {file = "propcache-0.3.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:34a624af06c048946709f4278b4176470073deda88d91342665d95f7c6270fbe"}, + {file = "propcache-0.3.2-cp39-cp39-win32.whl", hash = "sha256:4ba3fef1c30f306b1c274ce0b8baaa2c3cdd91f645c48f06394068f37d3837a1"}, + {file = "propcache-0.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:7a2368eed65fc69a7a7a40b27f22e85e7627b74216f0846b04ba5c116e191ec9"}, + {file = "propcache-0.3.2-py3-none-any.whl", hash = "sha256:98f1ec44fb675f5052cccc8e609c46ed23a35a1cfd18545ad4e29002d858a43f"}, + {file = "propcache-0.3.2.tar.gz", hash = "sha256:20d7d62e4e7ef05f221e0db2856b979540686342e7dd9973b815599c7057e168"}, +] + +[[package]] +name = "protobuf" +version = "5.29.5" +description = "" +optional = false +python-versions = ">=3.8" +files = [ + {file = "protobuf-5.29.5-cp310-abi3-win32.whl", hash = "sha256:3f1c6468a2cfd102ff4703976138844f78ebd1fb45f49011afc5139e9e283079"}, + {file = "protobuf-5.29.5-cp310-abi3-win_amd64.whl", hash = "sha256:3f76e3a3675b4a4d867b52e4a5f5b78a2ef9565549d4037e06cf7b0942b1d3fc"}, + {file = "protobuf-5.29.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e38c5add5a311f2a6eb0340716ef9b039c1dfa428b28f25a7838ac329204a671"}, + {file = "protobuf-5.29.5-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:fa18533a299d7ab6c55a238bf8629311439995f2e7eca5caaff08663606e9015"}, + {file = "protobuf-5.29.5-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:63848923da3325e1bf7e9003d680ce6e14b07e55d0473253a690c3a8b8fd6e61"}, + {file = "protobuf-5.29.5-cp38-cp38-win32.whl", hash = "sha256:ef91363ad4faba7b25d844ef1ada59ff1604184c0bcd8b39b8a6bef15e1af238"}, + {file = "protobuf-5.29.5-cp38-cp38-win_amd64.whl", hash = "sha256:7318608d56b6402d2ea7704ff1e1e4597bee46d760e7e4dd42a3d45e24b87f2e"}, + {file = "protobuf-5.29.5-cp39-cp39-win32.whl", hash = "sha256:6f642dc9a61782fa72b90878af134c5afe1917c89a568cd3476d758d3c3a0736"}, + {file = "protobuf-5.29.5-cp39-cp39-win_amd64.whl", hash = "sha256:470f3af547ef17847a28e1f47200a1cbf0ba3ff57b7de50d22776607cd2ea353"}, + {file = "protobuf-5.29.5-py3-none-any.whl", hash = "sha256:6cf42630262c59b2d8de33954443d94b746c952b01434fc58a417fdbd2e84bd5"}, + {file = "protobuf-5.29.5.tar.gz", hash = "sha256:bc1463bafd4b0929216c35f437a8e28731a2b7fe3d98bb77a600efced5a15c84"}, +] + +[[package]] +name = "pyasn1" +version = "0.6.1" +description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629"}, + {file = "pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034"}, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.2" +description = "A collection of ASN.1-based protocols modules" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a"}, + {file = "pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6"}, +] + +[package.dependencies] +pyasn1 = ">=0.6.1,<0.7.0" + +[[package]] +name = "pydantic" +version = "2.11.7" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.9" +files = [ + {file = "pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b"}, + {file = "pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db"}, +] + +[package.dependencies] +annotated-types = ">=0.6.0" +pydantic-core = "2.33.2" +typing-extensions = ">=4.12.2" +typing-inspection = ">=0.4.0" + +[package.extras] +email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata"] + +[[package]] +name = "pydantic-core" +version = "2.33.2" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.9" +files = [ + {file = "pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8"}, + {file = "pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a"}, + {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac"}, + {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a"}, + {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b"}, + {file = "pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22"}, + {file = "pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640"}, + {file = "pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7"}, + {file = "pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e"}, + {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d"}, + {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30"}, + {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf"}, + {file = "pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51"}, + {file = "pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab"}, + {file = "pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65"}, + {file = "pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc"}, + {file = "pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b"}, + {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1"}, + {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6"}, + {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea"}, + {file = "pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290"}, + {file = "pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2"}, + {file = "pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab"}, + {file = "pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f"}, + {file = "pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9"}, + {file = "pydantic_core-2.33.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a2b911a5b90e0374d03813674bf0a5fbbb7741570dcd4b4e85a2e48d17def29d"}, + {file = "pydantic_core-2.33.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6fa6dfc3e4d1f734a34710f391ae822e0a8eb8559a85c6979e14e65ee6ba2954"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c54c939ee22dc8e2d545da79fc5381f1c020d6d3141d3bd747eab59164dc89fb"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53a57d2ed685940a504248187d5685e49eb5eef0f696853647bf37c418c538f7"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09fb9dd6571aacd023fe6aaca316bd01cf60ab27240d7eb39ebd66a3a15293b4"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0e6116757f7959a712db11f3e9c0a99ade00a5bbedae83cb801985aa154f071b"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d55ab81c57b8ff8548c3e4947f119551253f4e3787a7bbc0b6b3ca47498a9d3"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c20c462aa4434b33a2661701b861604913f912254e441ab8d78d30485736115a"}, + {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:44857c3227d3fb5e753d5fe4a3420d6376fa594b07b621e220cd93703fe21782"}, + {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:eb9b459ca4df0e5c87deb59d37377461a538852765293f9e6ee834f0435a93b9"}, + {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9fcd347d2cc5c23b06de6d3b7b8275be558a0c90549495c699e379a80bf8379e"}, + {file = "pydantic_core-2.33.2-cp39-cp39-win32.whl", hash = "sha256:83aa99b1285bc8f038941ddf598501a86f1536789740991d7d8756e34f1e74d9"}, + {file = "pydantic_core-2.33.2-cp39-cp39-win_amd64.whl", hash = "sha256:f481959862f57f29601ccced557cc2e817bce7533ab8e01a797a48b49c9692b3"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:87acbfcf8e90ca885206e98359d7dca4bcbb35abdc0ff66672a293e1d7a19101"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:7f92c15cd1e97d4b12acd1cc9004fa092578acfa57b67ad5e43a197175d01a64"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3f26877a748dc4251cfcfda9dfb5f13fcb034f5308388066bcfe9031b63ae7d"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac89aea9af8cd672fa7b510e7b8c33b0bba9a43186680550ccf23020f32d535"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:970919794d126ba8645f3837ab6046fb4e72bbc057b3709144066204c19a455d"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3eb3fe62804e8f859c49ed20a8451342de53ed764150cb14ca71357c765dc2a6"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:3abcd9392a36025e3bd55f9bd38d908bd17962cc49bc6da8e7e96285336e2bca"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:3a1c81334778f9e3af2f8aeb7a960736e5cab1dfebfb26aabca09afd2906c039"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2807668ba86cb38c6817ad9bc66215ab8584d1d304030ce4f0887336f28a5e27"}, + {file = "pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + +[[package]] +name = "pygments" +version = "2.19.1" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"}, + {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pypika" +version = "0.48.9" +description = "A SQL query builder API for Python" +optional = false +python-versions = "*" +files = [ + {file = "PyPika-0.48.9.tar.gz", hash = "sha256:838836a61747e7c8380cd1b7ff638694b7a7335345d0f559b04b2cd832ad5378"}, +] + +[[package]] +name = "pyproject-hooks" +version = "1.2.0" +description = "Wrappers to call pyproject.toml-based build backend hooks." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913"}, + {file = "pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8"}, +] + +[[package]] +name = "pyreadline3" +version = "3.5.4" +description = "A python implementation of GNU readline." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyreadline3-3.5.4-py3-none-any.whl", hash = "sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6"}, + {file = "pyreadline3-3.5.4.tar.gz", hash = "sha256:8d57d53039a1c75adba8e50dd3d992b28143480816187ea5efbd5c78e6c885b7"}, +] + +[package.extras] +dev = ["build", "flake8", "mypy", "pytest", "twine"] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "python-dotenv" +version = "1.1.0" +description = "Read key-value pairs from a .env file and set them as environment variables" +optional = false +python-versions = ">=3.9" +files = [ + {file = "python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d"}, + {file = "python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5"}, +] + +[package.extras] +cli = ["click (>=5.0)"] + +[[package]] +name = "python-multipart" +version = "0.0.20" +description = "A streaming multipart parser for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104"}, + {file = "python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13"}, +] + +[[package]] +name = "pytz" +version = "2025.2" +description = "World timezone definitions, modern and historical" +optional = false +python-versions = "*" +files = [ + {file = "pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00"}, + {file = "pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3"}, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, + {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, + {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, + {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, + {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, + {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, + {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, + {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, + {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, + {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, + {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, + {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, + {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, + {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, + {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, + {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, +] + +[[package]] +name = "referencing" +version = "0.36.2" +description = "JSON Referencing + Python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0"}, + {file = "referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa"}, +] + +[package.dependencies] +attrs = ">=22.2.0" +rpds-py = ">=0.7.0" +typing-extensions = {version = ">=4.4.0", markers = "python_version < \"3.13\""} + +[[package]] +name = "regex" +version = "2024.11.6" +description = "Alternative regular expression module, to replace re." +optional = false +python-versions = ">=3.8" +files = [ + {file = "regex-2024.11.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff590880083d60acc0433f9c3f713c51f7ac6ebb9adf889c79a261ecf541aa91"}, + {file = "regex-2024.11.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:658f90550f38270639e83ce492f27d2c8d2cd63805c65a13a14d36ca126753f0"}, + {file = "regex-2024.11.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:164d8b7b3b4bcb2068b97428060b2a53be050085ef94eca7f240e7947f1b080e"}, + {file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3660c82f209655a06b587d55e723f0b813d3a7db2e32e5e7dc64ac2a9e86fde"}, + {file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d22326fcdef5e08c154280b71163ced384b428343ae16a5ab2b3354aed12436e"}, + {file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1ac758ef6aebfc8943560194e9fd0fa18bcb34d89fd8bd2af18183afd8da3a2"}, + {file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:997d6a487ff00807ba810e0f8332c18b4eb8d29463cfb7c820dc4b6e7562d0cf"}, + {file = "regex-2024.11.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:02a02d2bb04fec86ad61f3ea7f49c015a0681bf76abb9857f945d26159d2968c"}, + {file = "regex-2024.11.6-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f02f93b92358ee3f78660e43b4b0091229260c5d5c408d17d60bf26b6c900e86"}, + {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:06eb1be98df10e81ebaded73fcd51989dcf534e3c753466e4b60c4697a003b67"}, + {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:040df6fe1a5504eb0f04f048e6d09cd7c7110fef851d7c567a6b6e09942feb7d"}, + {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabbfc59f2c6edba2a6622c647b716e34e8e3867e0ab975412c5c2f79b82da2"}, + {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8447d2d39b5abe381419319f942de20b7ecd60ce86f16a23b0698f22e1b70008"}, + {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:da8f5fc57d1933de22a9e23eec290a0d8a5927a5370d24bda9a6abe50683fe62"}, + {file = "regex-2024.11.6-cp310-cp310-win32.whl", hash = "sha256:b489578720afb782f6ccf2840920f3a32e31ba28a4b162e13900c3e6bd3f930e"}, + {file = "regex-2024.11.6-cp310-cp310-win_amd64.whl", hash = "sha256:5071b2093e793357c9d8b2929dfc13ac5f0a6c650559503bb81189d0a3814519"}, + {file = "regex-2024.11.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5478c6962ad548b54a591778e93cd7c456a7a29f8eca9c49e4f9a806dcc5d638"}, + {file = "regex-2024.11.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c89a8cc122b25ce6945f0423dc1352cb9593c68abd19223eebbd4e56612c5b7"}, + {file = "regex-2024.11.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:94d87b689cdd831934fa3ce16cc15cd65748e6d689f5d2b8f4f4df2065c9fa20"}, + {file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1062b39a0a2b75a9c694f7a08e7183a80c63c0d62b301418ffd9c35f55aaa114"}, + {file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:167ed4852351d8a750da48712c3930b031f6efdaa0f22fa1933716bfcd6bf4a3"}, + {file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d548dafee61f06ebdb584080621f3e0c23fff312f0de1afc776e2a2ba99a74f"}, + {file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a19f302cd1ce5dd01a9099aaa19cae6173306d1302a43b627f62e21cf18ac0"}, + {file = "regex-2024.11.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bec9931dfb61ddd8ef2ebc05646293812cb6b16b60cf7c9511a832b6f1854b55"}, + {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9714398225f299aa85267fd222f7142fcb5c769e73d7733344efc46f2ef5cf89"}, + {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:202eb32e89f60fc147a41e55cb086db2a3f8cb82f9a9a88440dcfc5d37faae8d"}, + {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4181b814e56078e9b00427ca358ec44333765f5ca1b45597ec7446d3a1ef6e34"}, + {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:068376da5a7e4da51968ce4c122a7cd31afaaec4fccc7856c92f63876e57b51d"}, + {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f2c4184420d881a3475fb2c6f4d95d53a8d50209a2500723d831036f7c45"}, + {file = "regex-2024.11.6-cp311-cp311-win32.whl", hash = "sha256:c36f9b6f5f8649bb251a5f3f66564438977b7ef8386a52460ae77e6070d309d9"}, + {file = "regex-2024.11.6-cp311-cp311-win_amd64.whl", hash = "sha256:02e28184be537f0e75c1f9b2f8847dc51e08e6e171c6bde130b2687e0c33cf60"}, + {file = "regex-2024.11.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:52fb28f528778f184f870b7cf8f225f5eef0a8f6e3778529bdd40c7b3920796a"}, + {file = "regex-2024.11.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdd6028445d2460f33136c55eeb1f601ab06d74cb3347132e1c24250187500d9"}, + {file = "regex-2024.11.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:805e6b60c54bf766b251e94526ebad60b7de0c70f70a4e6210ee2891acb70bf2"}, + {file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b85c2530be953a890eaffde05485238f07029600e8f098cdf1848d414a8b45e4"}, + {file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb26437975da7dc36b7efad18aa9dd4ea569d2357ae6b783bf1118dabd9ea577"}, + {file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abfa5080c374a76a251ba60683242bc17eeb2c9818d0d30117b4486be10c59d3"}, + {file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b7fa6606c2881c1db9479b0eaa11ed5dfa11c8d60a474ff0e095099f39d98e"}, + {file = "regex-2024.11.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c32f75920cf99fe6b6c539c399a4a128452eaf1af27f39bce8909c9a3fd8cbe"}, + {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:982e6d21414e78e1f51cf595d7f321dcd14de1f2881c5dc6a6e23bbbbd68435e"}, + {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a7c2155f790e2fb448faed6dd241386719802296ec588a8b9051c1f5c481bc29"}, + {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:149f5008d286636e48cd0b1dd65018548944e495b0265b45e1bffecce1ef7f39"}, + {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e5364a4502efca094731680e80009632ad6624084aff9a23ce8c8c6820de3e51"}, + {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0a86e7eeca091c09e021db8eb72d54751e527fa47b8d5787caf96d9831bd02ad"}, + {file = "regex-2024.11.6-cp312-cp312-win32.whl", hash = "sha256:32f9a4c643baad4efa81d549c2aadefaeba12249b2adc5af541759237eee1c54"}, + {file = "regex-2024.11.6-cp312-cp312-win_amd64.whl", hash = "sha256:a93c194e2df18f7d264092dc8539b8ffb86b45b899ab976aa15d48214138e81b"}, + {file = "regex-2024.11.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a6ba92c0bcdf96cbf43a12c717eae4bc98325ca3730f6b130ffa2e3c3c723d84"}, + {file = "regex-2024.11.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:525eab0b789891ac3be914d36893bdf972d483fe66551f79d3e27146191a37d4"}, + {file = "regex-2024.11.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:086a27a0b4ca227941700e0b31425e7a28ef1ae8e5e05a33826e17e47fbfdba0"}, + {file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bde01f35767c4a7899b7eb6e823b125a64de314a8ee9791367c9a34d56af18d0"}, + {file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b583904576650166b3d920d2bcce13971f6f9e9a396c673187f49811b2769dc7"}, + {file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c4de13f06a0d54fa0d5ab1b7138bfa0d883220965a29616e3ea61b35d5f5fc7"}, + {file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cde6e9f2580eb1665965ce9bf17ff4952f34f5b126beb509fee8f4e994f143c"}, + {file = "regex-2024.11.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d7f453dca13f40a02b79636a339c5b62b670141e63efd511d3f8f73fba162b3"}, + {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59dfe1ed21aea057a65c6b586afd2a945de04fc7db3de0a6e3ed5397ad491b07"}, + {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b97c1e0bd37c5cd7902e65f410779d39eeda155800b65fc4d04cc432efa9bc6e"}, + {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d1e379028e0fc2ae3654bac3cbbef81bf3fd571272a42d56c24007979bafb6"}, + {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:13291b39131e2d002a7940fb176e120bec5145f3aeb7621be6534e46251912c4"}, + {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f51f88c126370dcec4908576c5a627220da6c09d0bff31cfa89f2523843316d"}, + {file = "regex-2024.11.6-cp313-cp313-win32.whl", hash = "sha256:63b13cfd72e9601125027202cad74995ab26921d8cd935c25f09c630436348ff"}, + {file = "regex-2024.11.6-cp313-cp313-win_amd64.whl", hash = "sha256:2b3361af3198667e99927da8b84c1b010752fa4b1115ee30beaa332cabc3ef1a"}, + {file = "regex-2024.11.6-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:3a51ccc315653ba012774efca4f23d1d2a8a8f278a6072e29c7147eee7da446b"}, + {file = "regex-2024.11.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ad182d02e40de7459b73155deb8996bbd8e96852267879396fb274e8700190e3"}, + {file = "regex-2024.11.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ba9b72e5643641b7d41fa1f6d5abda2c9a263ae835b917348fc3c928182ad467"}, + {file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40291b1b89ca6ad8d3f2b82782cc33807f1406cf68c8d440861da6304d8ffbbd"}, + {file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cdf58d0e516ee426a48f7b2c03a332a4114420716d55769ff7108c37a09951bf"}, + {file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a36fdf2af13c2b14738f6e973aba563623cb77d753bbbd8d414d18bfaa3105dd"}, + {file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1cee317bfc014c2419a76bcc87f071405e3966da434e03e13beb45f8aced1a6"}, + {file = "regex-2024.11.6-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50153825ee016b91549962f970d6a4442fa106832e14c918acd1c8e479916c4f"}, + {file = "regex-2024.11.6-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ea1bfda2f7162605f6e8178223576856b3d791109f15ea99a9f95c16a7636fb5"}, + {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:df951c5f4a1b1910f1a99ff42c473ff60f8225baa1cdd3539fe2819d9543e9df"}, + {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:072623554418a9911446278f16ecb398fb3b540147a7828c06e2011fa531e773"}, + {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:f654882311409afb1d780b940234208a252322c24a93b442ca714d119e68086c"}, + {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:89d75e7293d2b3e674db7d4d9b1bee7f8f3d1609428e293771d1a962617150cc"}, + {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:f65557897fc977a44ab205ea871b690adaef6b9da6afda4790a2484b04293a5f"}, + {file = "regex-2024.11.6-cp38-cp38-win32.whl", hash = "sha256:6f44ec28b1f858c98d3036ad5d7d0bfc568bdd7a74f9c24e25f41ef1ebfd81a4"}, + {file = "regex-2024.11.6-cp38-cp38-win_amd64.whl", hash = "sha256:bb8f74f2f10dbf13a0be8de623ba4f9491faf58c24064f32b65679b021ed0001"}, + {file = "regex-2024.11.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5704e174f8ccab2026bd2f1ab6c510345ae8eac818b613d7d73e785f1310f839"}, + {file = "regex-2024.11.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:220902c3c5cc6af55d4fe19ead504de80eb91f786dc102fbd74894b1551f095e"}, + {file = "regex-2024.11.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5e7e351589da0850c125f1600a4c4ba3c722efefe16b297de54300f08d734fbf"}, + {file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5056b185ca113c88e18223183aa1a50e66507769c9640a6ff75859619d73957b"}, + {file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e34b51b650b23ed3354b5a07aab37034d9f923db2a40519139af34f485f77d0"}, + {file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5670bce7b200273eee1840ef307bfa07cda90b38ae56e9a6ebcc9f50da9c469b"}, + {file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08986dce1339bc932923e7d1232ce9881499a0e02925f7402fb7c982515419ef"}, + {file = "regex-2024.11.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:93c0b12d3d3bc25af4ebbf38f9ee780a487e8bf6954c115b9f015822d3bb8e48"}, + {file = "regex-2024.11.6-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:764e71f22ab3b305e7f4c21f1a97e1526a25ebdd22513e251cf376760213da13"}, + {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f056bf21105c2515c32372bbc057f43eb02aae2fda61052e2f7622c801f0b4e2"}, + {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:69ab78f848845569401469da20df3e081e6b5a11cb086de3eed1d48f5ed57c95"}, + {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:86fddba590aad9208e2fa8b43b4c098bb0ec74f15718bb6a704e3c63e2cef3e9"}, + {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:684d7a212682996d21ca12ef3c17353c021fe9de6049e19ac8481ec35574a70f"}, + {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a03e02f48cd1abbd9f3b7e3586d97c8f7a9721c436f51a5245b3b9483044480b"}, + {file = "regex-2024.11.6-cp39-cp39-win32.whl", hash = "sha256:41758407fc32d5c3c5de163888068cfee69cb4c2be844e7ac517a52770f9af57"}, + {file = "regex-2024.11.6-cp39-cp39-win_amd64.whl", hash = "sha256:b2837718570f95dd41675328e111345f9b7095d821bac435aac173ac80b19983"}, + {file = "regex-2024.11.6.tar.gz", hash = "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519"}, +] + +[[package]] +name = "requests" +version = "2.32.4" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.8" +files = [ + {file = "requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c"}, + {file = "requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset_normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "requests-oauthlib" +version = "2.0.0" +description = "OAuthlib authentication support for Requests." +optional = false +python-versions = ">=3.4" +files = [ + {file = "requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9"}, + {file = "requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36"}, +] + +[package.dependencies] +oauthlib = ">=3.0.0" +requests = ">=2.0.0" + +[package.extras] +rsa = ["oauthlib[signedtoken] (>=3.0.0)"] + +[[package]] +name = "rich" +version = "14.0.0" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0"}, + {file = "rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725"}, +] + +[package.dependencies] +markdown-it-py = ">=2.2.0" +pygments = ">=2.13.0,<3.0.0" + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] + +[[package]] +name = "rpds-py" +version = "0.25.1" +description = "Python bindings to Rust's persistent data structures (rpds)" +optional = false +python-versions = ">=3.9" +files = [ + {file = "rpds_py-0.25.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:f4ad628b5174d5315761b67f212774a32f5bad5e61396d38108bd801c0a8f5d9"}, + {file = "rpds_py-0.25.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8c742af695f7525e559c16f1562cf2323db0e3f0fbdcabdf6865b095256b2d40"}, + {file = "rpds_py-0.25.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:605ffe7769e24b1800b4d024d24034405d9404f0bc2f55b6db3362cd34145a6f"}, + {file = "rpds_py-0.25.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ccc6f3ddef93243538be76f8e47045b4aad7a66a212cd3a0f23e34469473d36b"}, + {file = "rpds_py-0.25.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f70316f760174ca04492b5ab01be631a8ae30cadab1d1081035136ba12738cfa"}, + {file = "rpds_py-0.25.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e1dafef8df605fdb46edcc0bf1573dea0d6d7b01ba87f85cd04dc855b2b4479e"}, + {file = "rpds_py-0.25.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0701942049095741a8aeb298a31b203e735d1c61f4423511d2b1a41dcd8a16da"}, + {file = "rpds_py-0.25.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e87798852ae0b37c88babb7f7bbbb3e3fecc562a1c340195b44c7e24d403e380"}, + {file = "rpds_py-0.25.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3bcce0edc1488906c2d4c75c94c70a0417e83920dd4c88fec1078c94843a6ce9"}, + {file = "rpds_py-0.25.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e2f6a2347d3440ae789505693a02836383426249d5293541cd712e07e7aecf54"}, + {file = "rpds_py-0.25.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4fd52d3455a0aa997734f3835cbc4c9f32571345143960e7d7ebfe7b5fbfa3b2"}, + {file = "rpds_py-0.25.1-cp310-cp310-win32.whl", hash = "sha256:3f0b1798cae2bbbc9b9db44ee068c556d4737911ad53a4e5093d09d04b3bbc24"}, + {file = "rpds_py-0.25.1-cp310-cp310-win_amd64.whl", hash = "sha256:3ebd879ab996537fc510a2be58c59915b5dd63bccb06d1ef514fee787e05984a"}, + {file = "rpds_py-0.25.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:5f048bbf18b1f9120685c6d6bb70cc1a52c8cc11bdd04e643d28d3be0baf666d"}, + {file = "rpds_py-0.25.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4fbb0dbba559959fcb5d0735a0f87cdbca9e95dac87982e9b95c0f8f7ad10255"}, + {file = "rpds_py-0.25.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4ca54b9cf9d80b4016a67a0193ebe0bcf29f6b0a96f09db942087e294d3d4c2"}, + {file = "rpds_py-0.25.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ee3e26eb83d39b886d2cb6e06ea701bba82ef30a0de044d34626ede51ec98b0"}, + {file = "rpds_py-0.25.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:89706d0683c73a26f76a5315d893c051324d771196ae8b13e6ffa1ffaf5e574f"}, + {file = "rpds_py-0.25.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c2013ee878c76269c7b557a9a9c042335d732e89d482606990b70a839635feb7"}, + {file = "rpds_py-0.25.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45e484db65e5380804afbec784522de84fa95e6bb92ef1bd3325d33d13efaebd"}, + {file = "rpds_py-0.25.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:48d64155d02127c249695abb87d39f0faf410733428d499867606be138161d65"}, + {file = "rpds_py-0.25.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:048893e902132fd6548a2e661fb38bf4896a89eea95ac5816cf443524a85556f"}, + {file = "rpds_py-0.25.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0317177b1e8691ab5879f4f33f4b6dc55ad3b344399e23df2e499de7b10a548d"}, + {file = "rpds_py-0.25.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bffcf57826d77a4151962bf1701374e0fc87f536e56ec46f1abdd6a903354042"}, + {file = "rpds_py-0.25.1-cp311-cp311-win32.whl", hash = "sha256:cda776f1967cb304816173b30994faaf2fd5bcb37e73118a47964a02c348e1bc"}, + {file = "rpds_py-0.25.1-cp311-cp311-win_amd64.whl", hash = "sha256:dc3c1ff0abc91444cd20ec643d0f805df9a3661fcacf9c95000329f3ddf268a4"}, + {file = "rpds_py-0.25.1-cp311-cp311-win_arm64.whl", hash = "sha256:5a3ddb74b0985c4387719fc536faced33cadf2172769540c62e2a94b7b9be1c4"}, + {file = "rpds_py-0.25.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b5ffe453cde61f73fea9430223c81d29e2fbf412a6073951102146c84e19e34c"}, + {file = "rpds_py-0.25.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:115874ae5e2fdcfc16b2aedc95b5eef4aebe91b28e7e21951eda8a5dc0d3461b"}, + {file = "rpds_py-0.25.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a714bf6e5e81b0e570d01f56e0c89c6375101b8463999ead3a93a5d2a4af91fa"}, + {file = "rpds_py-0.25.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:35634369325906bcd01577da4c19e3b9541a15e99f31e91a02d010816b49bfda"}, + {file = "rpds_py-0.25.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d4cb2b3ddc16710548801c6fcc0cfcdeeff9dafbc983f77265877793f2660309"}, + {file = "rpds_py-0.25.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9ceca1cf097ed77e1a51f1dbc8d174d10cb5931c188a4505ff9f3e119dfe519b"}, + {file = "rpds_py-0.25.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c2cd1a4b0c2b8c5e31ffff50d09f39906fe351389ba143c195566056c13a7ea"}, + {file = "rpds_py-0.25.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1de336a4b164c9188cb23f3703adb74a7623ab32d20090d0e9bf499a2203ad65"}, + {file = "rpds_py-0.25.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9fca84a15333e925dd59ce01da0ffe2ffe0d6e5d29a9eeba2148916d1824948c"}, + {file = "rpds_py-0.25.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:88ec04afe0c59fa64e2f6ea0dd9657e04fc83e38de90f6de201954b4d4eb59bd"}, + {file = "rpds_py-0.25.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a8bd2f19e312ce3e1d2c635618e8a8d8132892bb746a7cf74780a489f0f6cdcb"}, + {file = "rpds_py-0.25.1-cp312-cp312-win32.whl", hash = "sha256:e5e2f7280d8d0d3ef06f3ec1b4fd598d386cc6f0721e54f09109a8132182fbfe"}, + {file = "rpds_py-0.25.1-cp312-cp312-win_amd64.whl", hash = "sha256:db58483f71c5db67d643857404da360dce3573031586034b7d59f245144cc192"}, + {file = "rpds_py-0.25.1-cp312-cp312-win_arm64.whl", hash = "sha256:6d50841c425d16faf3206ddbba44c21aa3310a0cebc3c1cdfc3e3f4f9f6f5728"}, + {file = "rpds_py-0.25.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:659d87430a8c8c704d52d094f5ba6fa72ef13b4d385b7e542a08fc240cb4a559"}, + {file = "rpds_py-0.25.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:68f6f060f0bbdfb0245267da014d3a6da9be127fe3e8cc4a68c6f833f8a23bb1"}, + {file = "rpds_py-0.25.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:083a9513a33e0b92cf6e7a6366036c6bb43ea595332c1ab5c8ae329e4bcc0a9c"}, + {file = "rpds_py-0.25.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:816568614ecb22b18a010c7a12559c19f6fe993526af88e95a76d5a60b8b75fb"}, + {file = "rpds_py-0.25.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c6564c0947a7f52e4792983f8e6cf9bac140438ebf81f527a21d944f2fd0a40"}, + {file = "rpds_py-0.25.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c4a128527fe415d73cf1f70a9a688d06130d5810be69f3b553bf7b45e8acf79"}, + {file = "rpds_py-0.25.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a49e1d7a4978ed554f095430b89ecc23f42014a50ac385eb0c4d163ce213c325"}, + {file = "rpds_py-0.25.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d74ec9bc0e2feb81d3f16946b005748119c0f52a153f6db6a29e8cd68636f295"}, + {file = "rpds_py-0.25.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3af5b4cc10fa41e5bc64e5c198a1b2d2864337f8fcbb9a67e747e34002ce812b"}, + {file = "rpds_py-0.25.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:79dc317a5f1c51fd9c6a0c4f48209c6b8526d0524a6904fc1076476e79b00f98"}, + {file = "rpds_py-0.25.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1521031351865e0181bc585147624d66b3b00a84109b57fcb7a779c3ec3772cd"}, + {file = "rpds_py-0.25.1-cp313-cp313-win32.whl", hash = "sha256:5d473be2b13600b93a5675d78f59e63b51b1ba2d0476893415dfbb5477e65b31"}, + {file = "rpds_py-0.25.1-cp313-cp313-win_amd64.whl", hash = "sha256:a7b74e92a3b212390bdce1d93da9f6488c3878c1d434c5e751cbc202c5e09500"}, + {file = "rpds_py-0.25.1-cp313-cp313-win_arm64.whl", hash = "sha256:dd326a81afe332ede08eb39ab75b301d5676802cdffd3a8f287a5f0b694dc3f5"}, + {file = "rpds_py-0.25.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:a58d1ed49a94d4183483a3ce0af22f20318d4a1434acee255d683ad90bf78129"}, + {file = "rpds_py-0.25.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f251bf23deb8332823aef1da169d5d89fa84c89f67bdfb566c49dea1fccfd50d"}, + {file = "rpds_py-0.25.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8dbd586bfa270c1103ece2109314dd423df1fa3d9719928b5d09e4840cec0d72"}, + {file = "rpds_py-0.25.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6d273f136e912aa101a9274c3145dcbddbe4bac560e77e6d5b3c9f6e0ed06d34"}, + {file = "rpds_py-0.25.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:666fa7b1bd0a3810a7f18f6d3a25ccd8866291fbbc3c9b912b917a6715874bb9"}, + {file = "rpds_py-0.25.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:921954d7fbf3fccc7de8f717799304b14b6d9a45bbeec5a8d7408ccbf531faf5"}, + {file = "rpds_py-0.25.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3d86373ff19ca0441ebeb696ef64cb58b8b5cbacffcda5a0ec2f3911732a194"}, + {file = "rpds_py-0.25.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c8980cde3bb8575e7c956a530f2c217c1d6aac453474bf3ea0f9c89868b531b6"}, + {file = "rpds_py-0.25.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8eb8c84ecea987a2523e057c0d950bcb3f789696c0499290b8d7b3107a719d78"}, + {file = "rpds_py-0.25.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:e43a005671a9ed5a650f3bc39e4dbccd6d4326b24fb5ea8be5f3a43a6f576c72"}, + {file = "rpds_py-0.25.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:58f77c60956501a4a627749a6dcb78dac522f249dd96b5c9f1c6af29bfacfb66"}, + {file = "rpds_py-0.25.1-cp313-cp313t-win32.whl", hash = "sha256:2cb9e5b5e26fc02c8a4345048cd9998c2aca7c2712bd1b36da0c72ee969a3523"}, + {file = "rpds_py-0.25.1-cp313-cp313t-win_amd64.whl", hash = "sha256:401ca1c4a20cc0510d3435d89c069fe0a9ae2ee6495135ac46bdd49ec0495763"}, + {file = "rpds_py-0.25.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:ce4c8e485a3c59593f1a6f683cf0ea5ab1c1dc94d11eea5619e4fb5228b40fbd"}, + {file = "rpds_py-0.25.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d8222acdb51a22929c3b2ddb236b69c59c72af4019d2cba961e2f9add9b6e634"}, + {file = "rpds_py-0.25.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4593c4eae9b27d22df41cde518b4b9e4464d139e4322e2127daa9b5b981b76be"}, + {file = "rpds_py-0.25.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd035756830c712b64725a76327ce80e82ed12ebab361d3a1cdc0f51ea21acb0"}, + {file = "rpds_py-0.25.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:114a07e85f32b125404f28f2ed0ba431685151c037a26032b213c882f26eb908"}, + {file = "rpds_py-0.25.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dec21e02e6cc932538b5203d3a8bd6aa1480c98c4914cb88eea064ecdbc6396a"}, + {file = "rpds_py-0.25.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:09eab132f41bf792c7a0ea1578e55df3f3e7f61888e340779b06050a9a3f16e9"}, + {file = "rpds_py-0.25.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c98f126c4fc697b84c423e387337d5b07e4a61e9feac494362a59fd7a2d9ed80"}, + {file = "rpds_py-0.25.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0e6a327af8ebf6baba1c10fadd04964c1965d375d318f4435d5f3f9651550f4a"}, + {file = "rpds_py-0.25.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:bc120d1132cff853ff617754196d0ac0ae63befe7c8498bd67731ba368abe451"}, + {file = "rpds_py-0.25.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:140f61d9bed7839446bdd44852e30195c8e520f81329b4201ceead4d64eb3a9f"}, + {file = "rpds_py-0.25.1-cp39-cp39-win32.whl", hash = "sha256:9c006f3aadeda131b438c3092124bd196b66312f0caa5823ef09585a669cf449"}, + {file = "rpds_py-0.25.1-cp39-cp39-win_amd64.whl", hash = "sha256:a61d0b2c7c9a0ae45732a77844917b427ff16ad5464b4d4f5e4adb955f582890"}, + {file = "rpds_py-0.25.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b24bf3cd93d5b6ecfbedec73b15f143596c88ee249fa98cefa9a9dc9d92c6f28"}, + {file = "rpds_py-0.25.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:0eb90e94f43e5085623932b68840b6f379f26db7b5c2e6bcef3179bd83c9330f"}, + {file = "rpds_py-0.25.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d50e4864498a9ab639d6d8854b25e80642bd362ff104312d9770b05d66e5fb13"}, + {file = "rpds_py-0.25.1-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7c9409b47ba0650544b0bb3c188243b83654dfe55dcc173a86832314e1a6a35d"}, + {file = "rpds_py-0.25.1-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:796ad874c89127c91970652a4ee8b00d56368b7e00d3477f4415fe78164c8000"}, + {file = "rpds_py-0.25.1-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:85608eb70a659bf4c1142b2781083d4b7c0c4e2c90eff11856a9754e965b2540"}, + {file = "rpds_py-0.25.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4feb9211d15d9160bc85fa72fed46432cdc143eb9cf6d5ca377335a921ac37b"}, + {file = "rpds_py-0.25.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ccfa689b9246c48947d31dd9d8b16d89a0ecc8e0e26ea5253068efb6c542b76e"}, + {file = "rpds_py-0.25.1-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:3c5b317ecbd8226887994852e85de562f7177add602514d4ac40f87de3ae45a8"}, + {file = "rpds_py-0.25.1-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:454601988aab2c6e8fd49e7634c65476b2b919647626208e376afcd22019eeb8"}, + {file = "rpds_py-0.25.1-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:1c0c434a53714358532d13539272db75a5ed9df75a4a090a753ac7173ec14e11"}, + {file = "rpds_py-0.25.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f73ce1512e04fbe2bc97836e89830d6b4314c171587a99688082d090f934d20a"}, + {file = "rpds_py-0.25.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ee86d81551ec68a5c25373c5643d343150cc54672b5e9a0cafc93c1870a53954"}, + {file = "rpds_py-0.25.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:89c24300cd4a8e4a51e55c31a8ff3918e6651b241ee8876a42cc2b2a078533ba"}, + {file = "rpds_py-0.25.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:771c16060ff4e79584dc48902a91ba79fd93eade3aa3a12d6d2a4aadaf7d542b"}, + {file = "rpds_py-0.25.1-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:785ffacd0ee61c3e60bdfde93baa6d7c10d86f15655bd706c89da08068dc5038"}, + {file = "rpds_py-0.25.1-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2a40046a529cc15cef88ac5ab589f83f739e2d332cb4d7399072242400ed68c9"}, + {file = "rpds_py-0.25.1-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:85fc223d9c76cabe5d0bff82214459189720dc135db45f9f66aa7cffbf9ff6c1"}, + {file = "rpds_py-0.25.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0be9965f93c222fb9b4cc254235b3b2b215796c03ef5ee64f995b1b69af0762"}, + {file = "rpds_py-0.25.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8378fa4a940f3fb509c081e06cb7f7f2adae8cf46ef258b0e0ed7519facd573e"}, + {file = "rpds_py-0.25.1-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:33358883a4490287e67a2c391dfaea4d9359860281db3292b6886bf0be3d8692"}, + {file = "rpds_py-0.25.1-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1d1fadd539298e70cac2f2cb36f5b8a65f742b9b9f1014dd4ea1f7785e2470bf"}, + {file = "rpds_py-0.25.1-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:9a46c2fb2545e21181445515960006e85d22025bd2fe6db23e76daec6eb689fe"}, + {file = "rpds_py-0.25.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:50f2c501a89c9a5f4e454b126193c5495b9fb441a75b298c60591d8a2eb92e1b"}, + {file = "rpds_py-0.25.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:7d779b325cc8238227c47fbc53964c8cc9a941d5dbae87aa007a1f08f2f77b23"}, + {file = "rpds_py-0.25.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:036ded36bedb727beeabc16dc1dad7cb154b3fa444e936a03b67a86dc6a5066e"}, + {file = "rpds_py-0.25.1-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:245550f5a1ac98504147cba96ffec8fabc22b610742e9150138e5d60774686d7"}, + {file = "rpds_py-0.25.1-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ff7c23ba0a88cb7b104281a99476cccadf29de2a0ef5ce864959a52675b1ca83"}, + {file = "rpds_py-0.25.1-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e37caa8cdb3b7cf24786451a0bdb853f6347b8b92005eeb64225ae1db54d1c2b"}, + {file = "rpds_py-0.25.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f2f48ab00181600ee266a095fe815134eb456163f7d6699f525dee471f312cf"}, + {file = "rpds_py-0.25.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9e5fc7484fa7dce57e25063b0ec9638ff02a908304f861d81ea49273e43838c1"}, + {file = "rpds_py-0.25.1-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:d3c10228d6cf6fe2b63d2e7985e94f6916fa46940df46b70449e9ff9297bd3d1"}, + {file = "rpds_py-0.25.1-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:5d9e40f32745db28c1ef7aad23f6fc458dc1e29945bd6781060f0d15628b8ddf"}, + {file = "rpds_py-0.25.1-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:35a8d1a24b5936b35c5003313bc177403d8bdef0f8b24f28b1c4a255f94ea992"}, + {file = "rpds_py-0.25.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:6099263f526efff9cf3883dfef505518730f7a7a93049b1d90d42e50a22b4793"}, + {file = "rpds_py-0.25.1.tar.gz", hash = "sha256:8960b6dac09b62dac26e75d7e2c4a22efb835d827a7278c34f72b2b84fa160e3"}, +] + +[[package]] +name = "rsa" +version = "4.9.1" +description = "Pure-Python RSA implementation" +optional = false +python-versions = "<4,>=3.6" +files = [ + {file = "rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762"}, + {file = "rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75"}, +] + +[package.dependencies] +pyasn1 = ">=0.1.3" + +[[package]] +name = "safetensors" +version = "0.5.3" +description = "" +optional = false +python-versions = ">=3.7" +files = [ + {file = "safetensors-0.5.3-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:bd20eb133db8ed15b40110b7c00c6df51655a2998132193de2f75f72d99c7073"}, + {file = "safetensors-0.5.3-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:21d01c14ff6c415c485616b8b0bf961c46b3b343ca59110d38d744e577f9cce7"}, + {file = "safetensors-0.5.3-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:11bce6164887cd491ca75c2326a113ba934be596e22b28b1742ce27b1d076467"}, + {file = "safetensors-0.5.3-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4a243be3590bc3301c821da7a18d87224ef35cbd3e5f5727e4e0728b8172411e"}, + {file = "safetensors-0.5.3-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8bd84b12b1670a6f8e50f01e28156422a2bc07fb16fc4e98bded13039d688a0d"}, + {file = "safetensors-0.5.3-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:391ac8cab7c829452175f871fcaf414aa1e292b5448bd02620f675a7f3e7abb9"}, + {file = "safetensors-0.5.3-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cead1fa41fc54b1e61089fa57452e8834f798cb1dc7a09ba3524f1eb08e0317a"}, + {file = "safetensors-0.5.3-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1077f3e94182d72618357b04b5ced540ceb71c8a813d3319f1aba448e68a770d"}, + {file = "safetensors-0.5.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:799021e78287bac619c7b3f3606730a22da4cda27759ddf55d37c8db7511c74b"}, + {file = "safetensors-0.5.3-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:df26da01aaac504334644e1b7642fa000bfec820e7cef83aeac4e355e03195ff"}, + {file = "safetensors-0.5.3-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:32c3ef2d7af8b9f52ff685ed0bc43913cdcde135089ae322ee576de93eae5135"}, + {file = "safetensors-0.5.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:37f1521be045e56fc2b54c606d4455573e717b2d887c579ee1dbba5f868ece04"}, + {file = "safetensors-0.5.3-cp38-abi3-win32.whl", hash = "sha256:cfc0ec0846dcf6763b0ed3d1846ff36008c6e7290683b61616c4b040f6a54ace"}, + {file = "safetensors-0.5.3-cp38-abi3-win_amd64.whl", hash = "sha256:836cbbc320b47e80acd40e44c8682db0e8ad7123209f69b093def21ec7cafd11"}, + {file = "safetensors-0.5.3.tar.gz", hash = "sha256:b6b0d6ecacec39a4fdd99cc19f4576f5219ce858e6fd8dbe7609df0b8dc56965"}, +] + +[package.extras] +all = ["safetensors[jax]", "safetensors[numpy]", "safetensors[paddlepaddle]", "safetensors[pinned-tf]", "safetensors[quality]", "safetensors[testing]", "safetensors[torch]"] +dev = ["safetensors[all]"] +jax = ["flax (>=0.6.3)", "jax (>=0.3.25)", "jaxlib (>=0.3.25)", "safetensors[numpy]"] +mlx = ["mlx (>=0.0.9)"] +numpy = ["numpy (>=1.21.6)"] +paddlepaddle = ["paddlepaddle (>=2.4.1)", "safetensors[numpy]"] +pinned-tf = ["safetensors[numpy]", "tensorflow (==2.18.0)"] +quality = ["black (==22.3)", "click (==8.0.4)", "flake8 (>=3.8.3)", "isort (>=5.5.4)"] +tensorflow = ["safetensors[numpy]", "tensorflow (>=2.11.0)"] +testing = ["h5py (>=3.7.0)", "huggingface-hub (>=0.12.1)", "hypothesis (>=6.70.2)", "pytest (>=7.2.0)", "pytest-benchmark (>=4.0.0)", "safetensors[numpy]", "setuptools-rust (>=1.5.2)"] +torch = ["safetensors[numpy]", "torch (>=1.10)"] + +[[package]] +name = "scikit-learn" +version = "1.7.0" +description = "A set of python modules for machine learning and data mining" +optional = false +python-versions = ">=3.10" +files = [ + {file = "scikit_learn-1.7.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9fe7f51435f49d97bd41d724bb3e11eeb939882af9c29c931a8002c357e8cdd5"}, + {file = "scikit_learn-1.7.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:d0c93294e1e1acbee2d029b1f2a064f26bd928b284938d51d412c22e0c977eb3"}, + {file = "scikit_learn-1.7.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf3755f25f145186ad8c403312f74fb90df82a4dfa1af19dc96ef35f57237a94"}, + {file = "scikit_learn-1.7.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2726c8787933add436fb66fb63ad18e8ef342dfb39bbbd19dc1e83e8f828a85a"}, + {file = "scikit_learn-1.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:e2539bb58886a531b6e86a510c0348afaadd25005604ad35966a85c2ec378800"}, + {file = "scikit_learn-1.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8ef09b1615e1ad04dc0d0054ad50634514818a8eb3ee3dee99af3bffc0ef5007"}, + {file = "scikit_learn-1.7.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:7d7240c7b19edf6ed93403f43b0fcb0fe95b53bc0b17821f8fb88edab97085ef"}, + {file = "scikit_learn-1.7.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80bd3bd4e95381efc47073a720d4cbab485fc483966f1709f1fd559afac57ab8"}, + {file = "scikit_learn-1.7.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9dbe48d69aa38ecfc5a6cda6c5df5abef0c0ebdb2468e92437e2053f84abb8bc"}, + {file = "scikit_learn-1.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:8fa979313b2ffdfa049ed07252dc94038def3ecd49ea2a814db5401c07f1ecfa"}, + {file = "scikit_learn-1.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c2c7243d34aaede0efca7a5a96d67fddaebb4ad7e14a70991b9abee9dc5c0379"}, + {file = "scikit_learn-1.7.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:9f39f6a811bf3f15177b66c82cbe0d7b1ebad9f190737dcdef77cfca1ea3c19c"}, + {file = "scikit_learn-1.7.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63017a5f9a74963d24aac7590287149a8d0f1a0799bbe7173c0d8ba1523293c0"}, + {file = "scikit_learn-1.7.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b2f8a0b1e73e9a08b7cc498bb2aeab36cdc1f571f8ab2b35c6e5d1c7115d97d"}, + {file = "scikit_learn-1.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:34cc8d9d010d29fb2b7cbcd5ccc24ffdd80515f65fe9f1e4894ace36b267ce19"}, + {file = "scikit_learn-1.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5b7974f1f32bc586c90145df51130e02267e4b7e77cab76165c76cf43faca0d9"}, + {file = "scikit_learn-1.7.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:014e07a23fe02e65f9392898143c542a50b6001dbe89cb867e19688e468d049b"}, + {file = "scikit_learn-1.7.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7e7ced20582d3a5516fb6f405fd1d254e1f5ce712bfef2589f51326af6346e8"}, + {file = "scikit_learn-1.7.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1babf2511e6ffd695da7a983b4e4d6de45dce39577b26b721610711081850906"}, + {file = "scikit_learn-1.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:5abd2acff939d5bd4701283f009b01496832d50ddafa83c90125a4e41c33e314"}, + {file = "scikit_learn-1.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:e39d95a929b112047c25b775035c8c234c5ca67e681ce60d12413afb501129f7"}, + {file = "scikit_learn-1.7.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:0521cb460426c56fee7e07f9365b0f45ec8ca7b2d696534ac98bfb85e7ae4775"}, + {file = "scikit_learn-1.7.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:317ca9f83acbde2883bd6bb27116a741bfcb371369706b4f9973cf30e9a03b0d"}, + {file = "scikit_learn-1.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:126c09740a6f016e815ab985b21e3a0656835414521c81fc1a8da78b679bdb75"}, + {file = "scikit_learn-1.7.0.tar.gz", hash = "sha256:c01e869b15aec88e2cdb73d27f15bdbe03bce8e2fb43afbe77c45d399e73a5a3"}, +] + +[package.dependencies] +joblib = ">=1.2.0" +numpy = ">=1.22.0" +scipy = ">=1.8.0" +threadpoolctl = ">=3.1.0" + +[package.extras] +benchmark = ["matplotlib (>=3.5.0)", "memory_profiler (>=0.57.0)", "pandas (>=1.4.0)"] +build = ["cython (>=3.0.10)", "meson-python (>=0.16.0)", "numpy (>=1.22.0)", "scipy (>=1.8.0)"] +docs = ["Pillow (>=8.4.0)", "matplotlib (>=3.5.0)", "memory_profiler (>=0.57.0)", "numpydoc (>=1.2.0)", "pandas (>=1.4.0)", "plotly (>=5.14.0)", "polars (>=0.20.30)", "pooch (>=1.6.0)", "pydata-sphinx-theme (>=0.15.3)", "scikit-image (>=0.19.0)", "seaborn (>=0.9.0)", "sphinx (>=7.3.7)", "sphinx-copybutton (>=0.5.2)", "sphinx-design (>=0.5.0)", "sphinx-design (>=0.6.0)", "sphinx-gallery (>=0.17.1)", "sphinx-prompt (>=1.4.0)", "sphinx-remove-toctrees (>=1.0.0.post1)", "sphinxcontrib-sass (>=0.3.4)", "sphinxext-opengraph (>=0.9.1)", "towncrier (>=24.8.0)"] +examples = ["matplotlib (>=3.5.0)", "pandas (>=1.4.0)", "plotly (>=5.14.0)", "pooch (>=1.6.0)", "scikit-image (>=0.19.0)", "seaborn (>=0.9.0)"] +install = ["joblib (>=1.2.0)", "numpy (>=1.22.0)", "scipy (>=1.8.0)", "threadpoolctl (>=3.1.0)"] +maintenance = ["conda-lock (==3.0.1)"] +tests = ["matplotlib (>=3.5.0)", "mypy (>=1.15)", "numpydoc (>=1.2.0)", "pandas (>=1.4.0)", "polars (>=0.20.30)", "pooch (>=1.6.0)", "pyamg (>=4.2.1)", "pyarrow (>=12.0.0)", "pytest (>=7.1.2)", "pytest-cov (>=2.9.0)", "ruff (>=0.11.7)", "scikit-image (>=0.19.0)"] + +[[package]] +name = "scipy" +version = "1.15.3" +description = "Fundamental algorithms for scientific computing in Python" +optional = false +python-versions = ">=3.10" +files = [ + {file = "scipy-1.15.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:a345928c86d535060c9c2b25e71e87c39ab2f22fc96e9636bd74d1dbf9de448c"}, + {file = "scipy-1.15.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:ad3432cb0f9ed87477a8d97f03b763fd1d57709f1bbde3c9369b1dff5503b253"}, + {file = "scipy-1.15.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:aef683a9ae6eb00728a542b796f52a5477b78252edede72b8327a886ab63293f"}, + {file = "scipy-1.15.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:1c832e1bd78dea67d5c16f786681b28dd695a8cb1fb90af2e27580d3d0967e92"}, + {file = "scipy-1.15.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:263961f658ce2165bbd7b99fa5135195c3a12d9bef045345016b8b50c315cb82"}, + {file = "scipy-1.15.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2abc762b0811e09a0d3258abee2d98e0c703eee49464ce0069590846f31d40"}, + {file = "scipy-1.15.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ed7284b21a7a0c8f1b6e5977ac05396c0d008b89e05498c8b7e8f4a1423bba0e"}, + {file = "scipy-1.15.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5380741e53df2c566f4d234b100a484b420af85deb39ea35a1cc1be84ff53a5c"}, + {file = "scipy-1.15.3-cp310-cp310-win_amd64.whl", hash = "sha256:9d61e97b186a57350f6d6fd72640f9e99d5a4a2b8fbf4b9ee9a841eab327dc13"}, + {file = "scipy-1.15.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:993439ce220d25e3696d1b23b233dd010169b62f6456488567e830654ee37a6b"}, + {file = "scipy-1.15.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:34716e281f181a02341ddeaad584205bd2fd3c242063bd3423d61ac259ca7eba"}, + {file = "scipy-1.15.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3b0334816afb8b91dab859281b1b9786934392aa3d527cd847e41bb6f45bee65"}, + {file = "scipy-1.15.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:6db907c7368e3092e24919b5e31c76998b0ce1684d51a90943cb0ed1b4ffd6c1"}, + {file = "scipy-1.15.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:721d6b4ef5dc82ca8968c25b111e307083d7ca9091bc38163fb89243e85e3889"}, + {file = "scipy-1.15.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39cb9c62e471b1bb3750066ecc3a3f3052b37751c7c3dfd0fd7e48900ed52982"}, + {file = "scipy-1.15.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:795c46999bae845966368a3c013e0e00947932d68e235702b5c3f6ea799aa8c9"}, + {file = "scipy-1.15.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:18aaacb735ab38b38db42cb01f6b92a2d0d4b6aabefeb07f02849e47f8fb3594"}, + {file = "scipy-1.15.3-cp311-cp311-win_amd64.whl", hash = "sha256:ae48a786a28412d744c62fd7816a4118ef97e5be0bee968ce8f0a2fba7acf3bb"}, + {file = "scipy-1.15.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6ac6310fdbfb7aa6612408bd2f07295bcbd3fda00d2d702178434751fe48e019"}, + {file = "scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:185cd3d6d05ca4b44a8f1595af87f9c372bb6acf9c808e99aa3e9aa03bd98cf6"}, + {file = "scipy-1.15.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:05dc6abcd105e1a29f95eada46d4a3f251743cfd7d3ae8ddb4088047f24ea477"}, + {file = "scipy-1.15.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:06efcba926324df1696931a57a176c80848ccd67ce6ad020c810736bfd58eb1c"}, + {file = "scipy-1.15.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05045d8b9bfd807ee1b9f38761993297b10b245f012b11b13b91ba8945f7e45"}, + {file = "scipy-1.15.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:271e3713e645149ea5ea3e97b57fdab61ce61333f97cfae392c28ba786f9bb49"}, + {file = "scipy-1.15.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6cfd56fc1a8e53f6e89ba3a7a7251f7396412d655bca2aa5611c8ec9a6784a1e"}, + {file = "scipy-1.15.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ff17c0bb1cb32952c09217d8d1eed9b53d1463e5f1dd6052c7857f83127d539"}, + {file = "scipy-1.15.3-cp312-cp312-win_amd64.whl", hash = "sha256:52092bc0472cfd17df49ff17e70624345efece4e1a12b23783a1ac59a1b728ed"}, + {file = "scipy-1.15.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c620736bcc334782e24d173c0fdbb7590a0a436d2fdf39310a8902505008759"}, + {file = "scipy-1.15.3-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:7e11270a000969409d37ed399585ee530b9ef6aa99d50c019de4cb01e8e54e62"}, + {file = "scipy-1.15.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8c9ed3ba2c8a2ce098163a9bdb26f891746d02136995df25227a20e71c396ebb"}, + {file = "scipy-1.15.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:0bdd905264c0c9cfa74a4772cdb2070171790381a5c4d312c973382fc6eaf730"}, + {file = "scipy-1.15.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79167bba085c31f38603e11a267d862957cbb3ce018d8b38f79ac043bc92d825"}, + {file = "scipy-1.15.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9deabd6d547aee2c9a81dee6cc96c6d7e9a9b1953f74850c179f91fdc729cb7"}, + {file = "scipy-1.15.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dde4fc32993071ac0c7dd2d82569e544f0bdaff66269cb475e0f369adad13f11"}, + {file = "scipy-1.15.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f77f853d584e72e874d87357ad70f44b437331507d1c311457bed8ed2b956126"}, + {file = "scipy-1.15.3-cp313-cp313-win_amd64.whl", hash = "sha256:b90ab29d0c37ec9bf55424c064312930ca5f4bde15ee8619ee44e69319aab163"}, + {file = "scipy-1.15.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3ac07623267feb3ae308487c260ac684b32ea35fd81e12845039952f558047b8"}, + {file = "scipy-1.15.3-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:6487aa99c2a3d509a5227d9a5e889ff05830a06b2ce08ec30df6d79db5fcd5c5"}, + {file = "scipy-1.15.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:50f9e62461c95d933d5c5ef4a1f2ebf9a2b4e83b0db374cb3f1de104d935922e"}, + {file = "scipy-1.15.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:14ed70039d182f411ffc74789a16df3835e05dc469b898233a245cdfd7f162cb"}, + {file = "scipy-1.15.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a769105537aa07a69468a0eefcd121be52006db61cdd8cac8a0e68980bbb723"}, + {file = "scipy-1.15.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db984639887e3dffb3928d118145ffe40eff2fa40cb241a306ec57c219ebbbb"}, + {file = "scipy-1.15.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:40e54d5c7e7ebf1aa596c374c49fa3135f04648a0caabcb66c52884b943f02b4"}, + {file = "scipy-1.15.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5e721fed53187e71d0ccf382b6bf977644c533e506c4d33c3fb24de89f5c3ed5"}, + {file = "scipy-1.15.3-cp313-cp313t-win_amd64.whl", hash = "sha256:76ad1fb5f8752eabf0fa02e4cc0336b4e8f021e2d5f061ed37d6d264db35e3ca"}, + {file = "scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf"}, +] + +[package.dependencies] +numpy = ">=1.23.5,<2.5" + +[package.extras] +dev = ["cython-lint (>=0.12.2)", "doit (>=0.36.0)", "mypy (==1.10.0)", "pycodestyle", "pydevtool", "rich-click", "ruff (>=0.0.292)", "types-psutil", "typing_extensions"] +doc = ["intersphinx_registry", "jupyterlite-pyodide-kernel", "jupyterlite-sphinx (>=0.19.1)", "jupytext", "matplotlib (>=3.5)", "myst-nb", "numpydoc", "pooch", "pydata-sphinx-theme (>=0.15.2)", "sphinx (>=5.0.0,<8.0.0)", "sphinx-copybutton", "sphinx-design (>=0.4.0)"] +test = ["Cython", "array-api-strict (>=2.0,<2.1.1)", "asv", "gmpy2", "hypothesis (>=6.30)", "meson", "mpmath", "ninja", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] + +[[package]] +name = "sentence-transformers" +version = "4.1.0" +description = "Embeddings, Retrieval, and Reranking" +optional = false +python-versions = ">=3.9" +files = [ + {file = "sentence_transformers-4.1.0-py3-none-any.whl", hash = "sha256:382a7f6be1244a100ce40495fb7523dbe8d71b3c10b299f81e6b735092b3b8ca"}, + {file = "sentence_transformers-4.1.0.tar.gz", hash = "sha256:f125ffd1c727533e0eca5d4567de72f84728de8f7482834de442fd90c2c3d50b"}, +] + +[package.dependencies] +huggingface-hub = ">=0.20.0" +Pillow = "*" +scikit-learn = "*" +scipy = "*" +torch = ">=1.11.0" +tqdm = "*" +transformers = ">=4.41.0,<5.0.0" +typing_extensions = ">=4.5.0" + +[package.extras] +dev = ["accelerate (>=0.20.3)", "datasets", "peft", "pre-commit", "pytest", "pytest-cov"] +onnx = ["optimum[onnxruntime] (>=1.23.1)"] +onnx-gpu = ["optimum[onnxruntime-gpu] (>=1.23.1)"] +openvino = ["optimum-intel[openvino] (>=1.20.0)"] +train = ["accelerate (>=0.20.3)", "datasets"] + +[[package]] +name = "setuptools" +version = "80.9.0" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.9" +files = [ + {file = "setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922"}, + {file = "setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c"}, +] + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.8.0)"] +core = ["importlib_metadata (>=6)", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] +type = ["importlib_metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (==1.14.*)", "pytest-mypy"] + +[[package]] +name = "shellingham" +version = "1.5.4" +description = "Tool to Detect Surrounding Shell" +optional = false +python-versions = ">=3.7" +files = [ + {file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"}, + {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, +] + +[[package]] +name = "six" +version = "1.17.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +description = "Sniff out which async library your code is running under" +optional = false +python-versions = ">=3.7" +files = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.41" +description = "Database Abstraction Library" +optional = false +python-versions = ">=3.7" +files = [ + {file = "SQLAlchemy-2.0.41-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6854175807af57bdb6425e47adbce7d20a4d79bbfd6f6d6519cd10bb7109a7f8"}, + {file = "SQLAlchemy-2.0.41-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05132c906066142103b83d9c250b60508af556982a385d96c4eaa9fb9720ac2b"}, + {file = "SQLAlchemy-2.0.41-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b4af17bda11e907c51d10686eda89049f9ce5669b08fbe71a29747f1e876036"}, + {file = "SQLAlchemy-2.0.41-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:c0b0e5e1b5d9f3586601048dd68f392dc0cc99a59bb5faf18aab057ce00d00b2"}, + {file = "SQLAlchemy-2.0.41-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:0b3dbf1e7e9bc95f4bac5e2fb6d3fb2f083254c3fdd20a1789af965caf2d2348"}, + {file = "SQLAlchemy-2.0.41-cp37-cp37m-win32.whl", hash = "sha256:1e3f196a0c59b0cae9a0cd332eb1a4bda4696e863f4f1cf84ab0347992c548c2"}, + {file = "SQLAlchemy-2.0.41-cp37-cp37m-win_amd64.whl", hash = "sha256:6ab60a5089a8f02009f127806f777fca82581c49e127f08413a66056bd9166dd"}, + {file = "sqlalchemy-2.0.41-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b1f09b6821406ea1f94053f346f28f8215e293344209129a9c0fcc3578598d7b"}, + {file = "sqlalchemy-2.0.41-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1936af879e3db023601196a1684d28e12f19ccf93af01bf3280a3262c4b6b4e5"}, + {file = "sqlalchemy-2.0.41-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2ac41acfc8d965fb0c464eb8f44995770239668956dc4cdf502d1b1ffe0d747"}, + {file = "sqlalchemy-2.0.41-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81c24e0c0fde47a9723c81d5806569cddef103aebbf79dbc9fcbb617153dea30"}, + {file = "sqlalchemy-2.0.41-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:23a8825495d8b195c4aa9ff1c430c28f2c821e8c5e2d98089228af887e5d7e29"}, + {file = "sqlalchemy-2.0.41-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:60c578c45c949f909a4026b7807044e7e564adf793537fc762b2489d522f3d11"}, + {file = "sqlalchemy-2.0.41-cp310-cp310-win32.whl", hash = "sha256:118c16cd3f1b00c76d69343e38602006c9cfb9998fa4f798606d28d63f23beda"}, + {file = "sqlalchemy-2.0.41-cp310-cp310-win_amd64.whl", hash = "sha256:7492967c3386df69f80cf67efd665c0f667cee67032090fe01d7d74b0e19bb08"}, + {file = "sqlalchemy-2.0.41-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6375cd674fe82d7aa9816d1cb96ec592bac1726c11e0cafbf40eeee9a4516b5f"}, + {file = "sqlalchemy-2.0.41-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9f8c9fdd15a55d9465e590a402f42082705d66b05afc3ffd2d2eb3c6ba919560"}, + {file = "sqlalchemy-2.0.41-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32f9dc8c44acdee06c8fc6440db9eae8b4af8b01e4b1aee7bdd7241c22edff4f"}, + {file = "sqlalchemy-2.0.41-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90c11ceb9a1f482c752a71f203a81858625d8df5746d787a4786bca4ffdf71c6"}, + {file = "sqlalchemy-2.0.41-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:911cc493ebd60de5f285bcae0491a60b4f2a9f0f5c270edd1c4dbaef7a38fc04"}, + {file = "sqlalchemy-2.0.41-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03968a349db483936c249f4d9cd14ff2c296adfa1290b660ba6516f973139582"}, + {file = "sqlalchemy-2.0.41-cp311-cp311-win32.whl", hash = "sha256:293cd444d82b18da48c9f71cd7005844dbbd06ca19be1ccf6779154439eec0b8"}, + {file = "sqlalchemy-2.0.41-cp311-cp311-win_amd64.whl", hash = "sha256:3d3549fc3e40667ec7199033a4e40a2f669898a00a7b18a931d3efb4c7900504"}, + {file = "sqlalchemy-2.0.41-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:81f413674d85cfd0dfcd6512e10e0f33c19c21860342a4890c3a2b59479929f9"}, + {file = "sqlalchemy-2.0.41-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:598d9ebc1e796431bbd068e41e4de4dc34312b7aa3292571bb3674a0cb415dd1"}, + {file = "sqlalchemy-2.0.41-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a104c5694dfd2d864a6f91b0956eb5d5883234119cb40010115fd45a16da5e70"}, + {file = "sqlalchemy-2.0.41-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6145afea51ff0af7f2564a05fa95eb46f542919e6523729663a5d285ecb3cf5e"}, + {file = "sqlalchemy-2.0.41-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b46fa6eae1cd1c20e6e6f44e19984d438b6b2d8616d21d783d150df714f44078"}, + {file = "sqlalchemy-2.0.41-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41836fe661cc98abfae476e14ba1906220f92c4e528771a8a3ae6a151242d2ae"}, + {file = "sqlalchemy-2.0.41-cp312-cp312-win32.whl", hash = "sha256:a8808d5cf866c781150d36a3c8eb3adccfa41a8105d031bf27e92c251e3969d6"}, + {file = "sqlalchemy-2.0.41-cp312-cp312-win_amd64.whl", hash = "sha256:5b14e97886199c1f52c14629c11d90c11fbb09e9334fa7bb5f6d068d9ced0ce0"}, + {file = "sqlalchemy-2.0.41-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4eeb195cdedaf17aab6b247894ff2734dcead6c08f748e617bfe05bd5a218443"}, + {file = "sqlalchemy-2.0.41-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d4ae769b9c1c7757e4ccce94b0641bc203bbdf43ba7a2413ab2523d8d047d8dc"}, + {file = "sqlalchemy-2.0.41-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a62448526dd9ed3e3beedc93df9bb6b55a436ed1474db31a2af13b313a70a7e1"}, + {file = "sqlalchemy-2.0.41-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc56c9788617b8964ad02e8fcfeed4001c1f8ba91a9e1f31483c0dffb207002a"}, + {file = "sqlalchemy-2.0.41-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c153265408d18de4cc5ded1941dcd8315894572cddd3c58df5d5b5705b3fa28d"}, + {file = "sqlalchemy-2.0.41-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f67766965996e63bb46cfbf2ce5355fc32d9dd3b8ad7e536a920ff9ee422e23"}, + {file = "sqlalchemy-2.0.41-cp313-cp313-win32.whl", hash = "sha256:bfc9064f6658a3d1cadeaa0ba07570b83ce6801a1314985bf98ec9b95d74e15f"}, + {file = "sqlalchemy-2.0.41-cp313-cp313-win_amd64.whl", hash = "sha256:82ca366a844eb551daff9d2e6e7a9e5e76d2612c8564f58db6c19a726869c1df"}, + {file = "sqlalchemy-2.0.41-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:90144d3b0c8b139408da50196c5cad2a6909b51b23df1f0538411cd23ffa45d3"}, + {file = "sqlalchemy-2.0.41-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:023b3ee6169969beea3bb72312e44d8b7c27c75b347942d943cf49397b7edeb5"}, + {file = "sqlalchemy-2.0.41-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:725875a63abf7c399d4548e686debb65cdc2549e1825437096a0af1f7e374814"}, + {file = "sqlalchemy-2.0.41-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81965cc20848ab06583506ef54e37cf15c83c7e619df2ad16807c03100745dea"}, + {file = "sqlalchemy-2.0.41-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:dd5ec3aa6ae6e4d5b5de9357d2133c07be1aff6405b136dad753a16afb6717dd"}, + {file = "sqlalchemy-2.0.41-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:ff8e80c4c4932c10493ff97028decfdb622de69cae87e0f127a7ebe32b4069c6"}, + {file = "sqlalchemy-2.0.41-cp38-cp38-win32.whl", hash = "sha256:4d44522480e0bf34c3d63167b8cfa7289c1c54264c2950cc5fc26e7850967e45"}, + {file = "sqlalchemy-2.0.41-cp38-cp38-win_amd64.whl", hash = "sha256:81eedafa609917040d39aa9332e25881a8e7a0862495fcdf2023a9667209deda"}, + {file = "sqlalchemy-2.0.41-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9a420a91913092d1e20c86a2f5f1fc85c1a8924dbcaf5e0586df8aceb09c9cc2"}, + {file = "sqlalchemy-2.0.41-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:906e6b0d7d452e9a98e5ab8507c0da791856b2380fdee61b765632bb8698026f"}, + {file = "sqlalchemy-2.0.41-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a373a400f3e9bac95ba2a06372c4fd1412a7cee53c37fc6c05f829bf672b8769"}, + {file = "sqlalchemy-2.0.41-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:087b6b52de812741c27231b5a3586384d60c353fbd0e2f81405a814b5591dc8b"}, + {file = "sqlalchemy-2.0.41-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:34ea30ab3ec98355235972dadc497bb659cc75f8292b760394824fab9cf39826"}, + {file = "sqlalchemy-2.0.41-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8280856dd7c6a68ab3a164b4a4b1c51f7691f6d04af4d4ca23d6ecf2261b7923"}, + {file = "sqlalchemy-2.0.41-cp39-cp39-win32.whl", hash = "sha256:b50eab9994d64f4a823ff99a0ed28a6903224ddbe7fef56a6dd865eec9243440"}, + {file = "sqlalchemy-2.0.41-cp39-cp39-win_amd64.whl", hash = "sha256:5e22575d169529ac3e0a120cf050ec9daa94b6a9597993d1702884f6954a7d71"}, + {file = "sqlalchemy-2.0.41-py3-none-any.whl", hash = "sha256:57df5dc6fdb5ed1a88a1ed2195fd31927e705cad62dedd86b46972752a80f576"}, + {file = "sqlalchemy-2.0.41.tar.gz", hash = "sha256:edba70118c4be3c2b1f90754d308d0b79c6fe2c0fdc52d8ddf603916f83f4db9"}, +] + +[package.dependencies] +greenlet = {version = ">=1", markers = "python_version < \"3.14\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")"} +typing-extensions = ">=4.6.0" + +[package.extras] +aiomysql = ["aiomysql (>=0.2.0)", "greenlet (>=1)"] +aioodbc = ["aioodbc", "greenlet (>=1)"] +aiosqlite = ["aiosqlite", "greenlet (>=1)", "typing_extensions (!=3.10.0.1)"] +asyncio = ["greenlet (>=1)"] +asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (>=1)"] +mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5,!=1.1.10)"] +mssql = ["pyodbc"] +mssql-pymssql = ["pymssql"] +mssql-pyodbc = ["pyodbc"] +mypy = ["mypy (>=0.910)"] +mysql = ["mysqlclient (>=1.4.0)"] +mysql-connector = ["mysql-connector-python"] +oracle = ["cx_oracle (>=8)"] +oracle-oracledb = ["oracledb (>=1.0.1)"] +postgresql = ["psycopg2 (>=2.7)"] +postgresql-asyncpg = ["asyncpg", "greenlet (>=1)"] +postgresql-pg8000 = ["pg8000 (>=1.29.1)"] +postgresql-psycopg = ["psycopg (>=3.0.7)"] +postgresql-psycopg2binary = ["psycopg2-binary"] +postgresql-psycopg2cffi = ["psycopg2cffi"] +postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"] +pymysql = ["pymysql"] +sqlcipher = ["sqlcipher3_binary"] + +[[package]] +name = "starlette" +version = "0.45.3" +description = "The little ASGI library that shines." +optional = false +python-versions = ">=3.9" +files = [ + {file = "starlette-0.45.3-py3-none-any.whl", hash = "sha256:dfb6d332576f136ec740296c7e8bb8c8a7125044e7c6da30744718880cdd059d"}, + {file = "starlette-0.45.3.tar.gz", hash = "sha256:2cbcba2a75806f8a41c722141486f37c28e30a0921c5f6fe4346cb0dcee1302f"}, +] + +[package.dependencies] +anyio = ">=3.6.2,<5" + +[package.extras] +full = ["httpx (>=0.27.0,<0.29.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"] + +[[package]] +name = "sympy" +version = "1.14.0" +description = "Computer algebra system (CAS) in Python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5"}, + {file = "sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517"}, +] + +[package.dependencies] +mpmath = ">=1.1.0,<1.4" + +[package.extras] +dev = ["hypothesis (>=6.70.0)", "pytest (>=7.1.0)"] + +[[package]] +name = "tenacity" +version = "9.1.2" +description = "Retry code until it succeeds" +optional = false +python-versions = ">=3.9" +files = [ + {file = "tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138"}, + {file = "tenacity-9.1.2.tar.gz", hash = "sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb"}, +] + +[package.extras] +doc = ["reno", "sphinx"] +test = ["pytest", "tornado (>=4.5)", "typeguard"] + +[[package]] +name = "threadpoolctl" +version = "3.6.0" +description = "threadpoolctl" +optional = false +python-versions = ">=3.9" +files = [ + {file = "threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb"}, + {file = "threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e"}, +] + +[[package]] +name = "tokenizers" +version = "0.21.1" +description = "" +optional = false +python-versions = ">=3.9" +files = [ + {file = "tokenizers-0.21.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:e78e413e9e668ad790a29456e677d9d3aa50a9ad311a40905d6861ba7692cf41"}, + {file = "tokenizers-0.21.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:cd51cd0a91ecc801633829fcd1fda9cf8682ed3477c6243b9a095539de4aecf3"}, + {file = "tokenizers-0.21.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28da6b72d4fb14ee200a1bd386ff74ade8992d7f725f2bde2c495a9a98cf4d9f"}, + {file = "tokenizers-0.21.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:34d8cfde551c9916cb92014e040806122295a6800914bab5865deb85623931cf"}, + {file = "tokenizers-0.21.1-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aaa852d23e125b73d283c98f007e06d4595732104b65402f46e8ef24b588d9f8"}, + {file = "tokenizers-0.21.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a21a15d5c8e603331b8a59548bbe113564136dc0f5ad8306dd5033459a226da0"}, + {file = "tokenizers-0.21.1-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2fdbd4c067c60a0ac7eca14b6bd18a5bebace54eb757c706b47ea93204f7a37c"}, + {file = "tokenizers-0.21.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dd9a0061e403546f7377df940e866c3e678d7d4e9643d0461ea442b4f89e61a"}, + {file = "tokenizers-0.21.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:db9484aeb2e200c43b915a1a0150ea885e35f357a5a8fabf7373af333dcc8dbf"}, + {file = "tokenizers-0.21.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:ed248ab5279e601a30a4d67bdb897ecbe955a50f1e7bb62bd99f07dd11c2f5b6"}, + {file = "tokenizers-0.21.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:9ac78b12e541d4ce67b4dfd970e44c060a2147b9b2a21f509566d556a509c67d"}, + {file = "tokenizers-0.21.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e5a69c1a4496b81a5ee5d2c1f3f7fbdf95e90a0196101b0ee89ed9956b8a168f"}, + {file = "tokenizers-0.21.1-cp39-abi3-win32.whl", hash = "sha256:1039a3a5734944e09de1d48761ade94e00d0fa760c0e0551151d4dd851ba63e3"}, + {file = "tokenizers-0.21.1-cp39-abi3-win_amd64.whl", hash = "sha256:0f0dcbcc9f6e13e675a66d7a5f2f225a736745ce484c1a4e07476a89ccdad382"}, + {file = "tokenizers-0.21.1.tar.gz", hash = "sha256:a1bb04dc5b448985f86ecd4b05407f5a8d97cb2c0532199b2a302a604a0165ab"}, +] + +[package.dependencies] +huggingface-hub = ">=0.16.4,<1.0" + +[package.extras] +dev = ["tokenizers[testing]"] +docs = ["setuptools-rust", "sphinx", "sphinx-rtd-theme"] +testing = ["black (==22.3)", "datasets", "numpy", "pytest", "requests", "ruff"] + +[[package]] +name = "torch" +version = "2.7.1+cpu" +description = "Tensors and Dynamic neural networks in Python with strong GPU acceleration" +optional = false +python-versions = ">=3.9.0" +files = [ + {file = "torch-2.7.1+cpu-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:c0df17cee97653d09a4e84488a33d21217f9b24208583c55cf28f0045aab0766"}, + {file = "torch-2.7.1+cpu-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:1f04a373a3f643821f721da9898ef77dce73b5b6bfc64486f0976f7fb5f90e83"}, + {file = "torch-2.7.1+cpu-cp310-cp310-win_amd64.whl", hash = "sha256:b4cc706973655151f198d027ed34c92ab31a3db55676b44251194e1280631426"}, + {file = "torch-2.7.1+cpu-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:5fe6045b8f426bf2d0426e4fe009f1667a954ec2aeb82f1bd0bf60c6d7a85445"}, + {file = "torch-2.7.1+cpu-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a1684793e352f03fa14f78857e55d65de4ada8405ded1da2bf4f452179c4b779"}, + {file = "torch-2.7.1+cpu-cp311-cp311-win_amd64.whl", hash = "sha256:7b977eccbc85ae2bd19d6998de7b1f1f4bd3c04eaffd3015deb7934389783399"}, + {file = "torch-2.7.1+cpu-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3bf2db5adf77b433844f080887ade049c4705ddf9fe1a32023ff84ff735aa5ad"}, + {file = "torch-2.7.1+cpu-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:8f8b3cfc53010a4b4a3c7ecb88c212e9decc4f5eeb6af75c3c803937d2d60947"}, + {file = "torch-2.7.1+cpu-cp312-cp312-win_amd64.whl", hash = "sha256:0bc887068772233f532b51a3e8c8cfc682ae62bef74bf4e0c53526c8b9e4138f"}, + {file = "torch-2.7.1+cpu-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:eb17646792ac4374ffc87e42369f45d21eff17c790868963b90483ef0b6db4ef"}, + {file = "torch-2.7.1+cpu-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:84ea1f6a1d15663037d01b121d6e33bb9da3c90af8e069e5072c30f413455a57"}, + {file = "torch-2.7.1+cpu-cp313-cp313-win_amd64.whl", hash = "sha256:b66f77f6f67317344ee083aa7ac4751a14395fcb38060d564bf513978d267153"}, + {file = "torch-2.7.1+cpu-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:56136a2aca6707df3c8811e46ea2d379eaafd18e656e2fd51e8e4d0ca995651b"}, + {file = "torch-2.7.1+cpu-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:355614185a2aea7155f9c88a20bfd49de5f3063866f3cf9b2f21b6e9e59e31e0"}, + {file = "torch-2.7.1+cpu-cp313-cp313t-win_amd64.whl", hash = "sha256:464bca1bc9452f2ccd676514688896e66b9488f2a0268ecd3ac497cf09c5aac1"}, + {file = "torch-2.7.1+cpu-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:a4551cb97b83df5f93fc0d7538332535828581e1db2f179afc287027afbdd6e8"}, + {file = "torch-2.7.1+cpu-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:d205cac087d60bc176bdc0b63a1d00dc7a4ee5ac76fd20a2ca318ac65674167e"}, + {file = "torch-2.7.1+cpu-cp39-cp39-win_amd64.whl", hash = "sha256:d25435bdc4780d3cb512aad55142aca9584ae1fe8f8691cda6d32f19faf5d58e"}, +] + +[package.dependencies] +filelock = "*" +fsspec = "*" +jinja2 = "*" +networkx = "*" +setuptools = {version = "*", markers = "python_version >= \"3.12\""} +sympy = ">=1.13.3" +typing-extensions = ">=4.10.0" + +[package.extras] +opt-einsum = ["opt-einsum (>=3.3)"] +optree = ["optree (>=0.13.0)"] + +[package.source] +type = "legacy" +url = "https://download.pytorch.org/whl/cpu" +reference = "pytorch-cpu" + +[[package]] +name = "torchaudio" +version = "2.7.1+cpu" +description = "An audio package for PyTorch" +optional = false +python-versions = "*" +files = [ + {file = "torchaudio-2.7.1+cpu-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a36569e17ff4519a21f2113e9a19a8def0d70e2fd9fabc9105ca57dee3809443"}, + {file = "torchaudio-2.7.1+cpu-cp310-cp310-win_amd64.whl", hash = "sha256:d7bd84b934f365e537e519838e9a5e7e6aef0d94e3d1419e8734f58b1142f326"}, + {file = "torchaudio-2.7.1+cpu-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:79b75d9f8dadad5da128fd6677fe717669ce580c0d54c8407792854ac8b97349"}, + {file = "torchaudio-2.7.1+cpu-cp311-cp311-win_amd64.whl", hash = "sha256:c6b82f209797d0b6e46c33a76facb39987141c453f85d9d0fa849363d47c2f42"}, + {file = "torchaudio-2.7.1+cpu-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:65bf843345ae05629b7f71609bab0808004dabfce6cf48ea508a5d4f5419ca74"}, + {file = "torchaudio-2.7.1+cpu-cp312-cp312-win_amd64.whl", hash = "sha256:a71ef774991658188721f53ebf05c8858b2baf0abb17b65bf447b294f3e63e2e"}, + {file = "torchaudio-2.7.1+cpu-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e169a2b62e55342f2f30e17640054707c8e339045a1ccc2db33517e9debb2767"}, + {file = "torchaudio-2.7.1+cpu-cp313-cp313-win_amd64.whl", hash = "sha256:6d4855a0d40d700b6a20b5d2691cfc9ea2296419e3ab0442ee2a1e8d0b73242a"}, + {file = "torchaudio-2.7.1+cpu-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:2ec85e79386c3e68ae67ac74033118253f0c7a64a8343a58b2df802e42ca9f74"}, + {file = "torchaudio-2.7.1+cpu-cp313-cp313t-win_amd64.whl", hash = "sha256:5856ce75fb0cfb2a0d8be4e3f9def122414f009aad4347161ad80f5b8f708fa4"}, + {file = "torchaudio-2.7.1+cpu-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:deb19d2a1cbbe49f9d14a9fe3dce65fef8dd98570aa8b6a65d7f5d1e0d16d0f3"}, + {file = "torchaudio-2.7.1+cpu-cp39-cp39-win_amd64.whl", hash = "sha256:2746064c15032e674d1bf6c6e1686f7a43184a8f065ee1f1bdb81c782e82537c"}, +] + +[package.dependencies] +torch = "2.7.1" + +[package.source] +type = "legacy" +url = "https://download.pytorch.org/whl/cpu" +reference = "pytorch-cpu" + +[[package]] +name = "torchvision" +version = "0.22.1+cpu" +description = "image and video datasets and models for torch deep learning" +optional = false +python-versions = ">=3.9" +files = [ + {file = "torchvision-0.22.1+cpu-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:e31f1273a8dd9760906288036ac3c8f5fef25eed393da0491db150d7be78910d"}, + {file = "torchvision-0.22.1+cpu-cp310-cp310-win_amd64.whl", hash = "sha256:445e442b94c365f7fd96596347c8a5a7fcfcbfca17a23baa8c9dcc8cb00fceee"}, + {file = "torchvision-0.22.1+cpu-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:4e0cbc165a472605d0c13da68ae22e84b17a6b815d5e600834777823e1bcb658"}, + {file = "torchvision-0.22.1+cpu-cp311-cp311-win_amd64.whl", hash = "sha256:9482adee074f60a45fd69892f7488281aadfda7836948c94b0a9b0caf55d1d67"}, + {file = "torchvision-0.22.1+cpu-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b5fa7044bd82c6358e8229351c98070cf3a7bf4a6e89ea46352ae6c65745ef94"}, + {file = "torchvision-0.22.1+cpu-cp312-cp312-win_amd64.whl", hash = "sha256:433cb4dbced7291f17064cea08ac1e5aebd02ec190e1c207d117ad62a8961f2b"}, + {file = "torchvision-0.22.1+cpu-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:a93c21f18c33a819616b3dda7655aa4de40b219682c654175b6bbeb65ecc2e5f"}, + {file = "torchvision-0.22.1+cpu-cp313-cp313-win_amd64.whl", hash = "sha256:34c914ad4728b81848ac802c5fc5eeb8de8ff4058cc59c1463a74ce4f4fbf0d8"}, + {file = "torchvision-0.22.1+cpu-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:ab7ae82529887c704c1b5d1d5198f65dc777d04fc3858b374503a6deedb82b19"}, + {file = "torchvision-0.22.1+cpu-cp313-cp313t-win_amd64.whl", hash = "sha256:b2d1c4bdbfd8e6c779dc810a6171b56224f1332fc46986810d4081bed1633804"}, + {file = "torchvision-0.22.1+cpu-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:c852e61bc903351169017e2e96389f28f6cfb52ca7c3945acceb31e7fe1b21e6"}, + {file = "torchvision-0.22.1+cpu-cp39-cp39-win_amd64.whl", hash = "sha256:99788dd0d97ac8cdf25c74481e869e298626ffd8d6532defff6711f60516c88a"}, +] + +[package.dependencies] +numpy = "*" +pillow = ">=5.3.0,<8.3.dev0 || >=8.4.dev0" +torch = "2.7.1" + +[package.extras] +gdown = ["gdown (>=4.7.3)"] +scipy = ["scipy"] + +[package.source] +type = "legacy" +url = "https://download.pytorch.org/whl/cpu" +reference = "pytorch-cpu" + +[[package]] +name = "tqdm" +version = "4.67.1" +description = "Fast, Extensible Progress Meter" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2"}, + {file = "tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[package.extras] +dev = ["nbval", "pytest (>=6)", "pytest-asyncio (>=0.24)", "pytest-cov", "pytest-timeout"] +discord = ["requests"] +notebook = ["ipywidgets (>=6)"] +slack = ["slack-sdk"] +telegram = ["requests"] + +[[package]] +name = "transformers" +version = "4.52.4" +description = "State-of-the-art Machine Learning for JAX, PyTorch and TensorFlow" +optional = false +python-versions = ">=3.9.0" +files = [ + {file = "transformers-4.52.4-py3-none-any.whl", hash = "sha256:203f5c19416d5877e36e88633943761719538a25d9775977a24fe77a1e5adfc7"}, + {file = "transformers-4.52.4.tar.gz", hash = "sha256:aff3764441c1adc192a08dba49740d3cbbcb72d850586075aed6bd89b98203e6"}, +] + +[package.dependencies] +filelock = "*" +huggingface-hub = ">=0.30.0,<1.0" +numpy = ">=1.17" +packaging = ">=20.0" +pyyaml = ">=5.1" +regex = "!=2019.12.17" +requests = "*" +safetensors = ">=0.4.3" +tokenizers = ">=0.21,<0.22" +tqdm = ">=4.27" + +[package.extras] +accelerate = ["accelerate (>=0.26.0)"] +all = ["Pillow (>=10.0.1,<=15.0)", "accelerate (>=0.26.0)", "av", "codecarbon (>=2.8.1)", "flax (>=0.4.1,<=0.7.0)", "jax (>=0.4.1,<=0.4.13)", "jaxlib (>=0.4.1,<=0.4.13)", "kenlm", "keras-nlp (>=0.3.1,<0.14.0)", "kernels (>=0.4.4,<0.5)", "librosa", "num2words", "onnxconverter-common", "optax (>=0.0.8,<=0.1.4)", "optuna", "phonemizer", "protobuf", "pyctcdecode (>=0.4.0)", "ray[tune] (>=2.7.0)", "scipy (<1.13.0)", "sentencepiece (>=0.1.91,!=0.1.92)", "sigopt", "tensorflow (>2.9,<2.16)", "tensorflow-text (<2.16)", "tf2onnx", "timm (<=1.0.11)", "tokenizers (>=0.21,<0.22)", "torch (>=2.1,<2.7)", "torchaudio", "torchvision"] +audio = ["kenlm", "librosa", "phonemizer", "pyctcdecode (>=0.4.0)"] +benchmark = ["optimum-benchmark (>=0.3.0)"] +codecarbon = ["codecarbon (>=2.8.1)"] +deepspeed = ["accelerate (>=0.26.0)", "deepspeed (>=0.9.3)"] +deepspeed-testing = ["GitPython (<3.1.19)", "accelerate (>=0.26.0)", "beautifulsoup4", "cookiecutter (==1.7.3)", "datasets (!=2.5.0)", "deepspeed (>=0.9.3)", "dill (<0.3.5)", "evaluate (>=0.2.0)", "faiss-cpu", "nltk (<=3.8.1)", "optuna", "parameterized", "protobuf", "psutil", "pydantic", "pytest (>=7.2.0)", "pytest-asyncio", "pytest-order", "pytest-rerunfailures", "pytest-rich", "pytest-timeout", "pytest-xdist", "rjieba", "rouge-score (!=0.0.7,!=0.0.8,!=0.1,!=0.1.1)", "ruff (==0.11.2)", "sacrebleu (>=1.4.12,<2.0.0)", "sacremoses", "sentencepiece (>=0.1.91,!=0.1.92)", "tensorboard", "timeout-decorator"] +dev = ["GitPython (<3.1.19)", "Pillow (>=10.0.1,<=15.0)", "accelerate (>=0.26.0)", "av", "beautifulsoup4", "codecarbon (>=2.8.1)", "cookiecutter (==1.7.3)", "datasets (!=2.5.0)", "dill (<0.3.5)", "evaluate (>=0.2.0)", "faiss-cpu", "flax (>=0.4.1,<=0.7.0)", "fugashi (>=1.0)", "ipadic (>=1.0.0,<2.0)", "isort (>=5.5.4)", "jax (>=0.4.1,<=0.4.13)", "jaxlib (>=0.4.1,<=0.4.13)", "kenlm", "keras-nlp (>=0.3.1,<0.14.0)", "kernels (>=0.4.4,<0.5)", "libcst", "librosa", "nltk (<=3.8.1)", "num2words", "onnxconverter-common", "optax (>=0.0.8,<=0.1.4)", "optuna", "parameterized", "phonemizer", "protobuf", "psutil", "pyctcdecode (>=0.4.0)", "pydantic", "pytest (>=7.2.0)", "pytest-asyncio", "pytest-order", "pytest-rerunfailures", "pytest-rich", "pytest-timeout", "pytest-xdist", "ray[tune] (>=2.7.0)", "rhoknp (>=1.1.0,<1.3.1)", "rich", "rjieba", "rouge-score (!=0.0.7,!=0.0.8,!=0.1,!=0.1.1)", "ruff (==0.11.2)", "sacrebleu (>=1.4.12,<2.0.0)", "sacremoses", "scikit-learn", "scipy (<1.13.0)", "sentencepiece (>=0.1.91,!=0.1.92)", "sigopt", "sudachidict-core (>=20220729)", "sudachipy (>=0.6.6)", "tensorboard", "tensorflow (>2.9,<2.16)", "tensorflow-text (<2.16)", "tf2onnx", "timeout-decorator", "timm (<=1.0.11)", "tokenizers (>=0.21,<0.22)", "torch (>=2.1,<2.7)", "torchaudio", "torchvision", "unidic (>=1.0.2)", "unidic-lite (>=1.0.7)", "urllib3 (<2.0.0)"] +dev-tensorflow = ["GitPython (<3.1.19)", "Pillow (>=10.0.1,<=15.0)", "beautifulsoup4", "cookiecutter (==1.7.3)", "datasets (!=2.5.0)", "dill (<0.3.5)", "evaluate (>=0.2.0)", "faiss-cpu", "isort (>=5.5.4)", "kenlm", "keras-nlp (>=0.3.1,<0.14.0)", "libcst", "librosa", "nltk (<=3.8.1)", "onnxconverter-common", "onnxruntime (>=1.4.0)", "onnxruntime-tools (>=1.4.2)", "parameterized", "phonemizer", "protobuf", "psutil", "pyctcdecode (>=0.4.0)", "pydantic", "pytest (>=7.2.0)", "pytest-asyncio", "pytest-order", "pytest-rerunfailures", "pytest-rich", "pytest-timeout", "pytest-xdist", "rich", "rjieba", "rouge-score (!=0.0.7,!=0.0.8,!=0.1,!=0.1.1)", "ruff (==0.11.2)", "sacrebleu (>=1.4.12,<2.0.0)", "sacremoses", "scikit-learn", "sentencepiece (>=0.1.91,!=0.1.92)", "tensorboard", "tensorflow (>2.9,<2.16)", "tensorflow-text (<2.16)", "tf2onnx", "timeout-decorator", "tokenizers (>=0.21,<0.22)", "urllib3 (<2.0.0)"] +dev-torch = ["GitPython (<3.1.19)", "Pillow (>=10.0.1,<=15.0)", "accelerate (>=0.26.0)", "beautifulsoup4", "codecarbon (>=2.8.1)", "cookiecutter (==1.7.3)", "datasets (!=2.5.0)", "dill (<0.3.5)", "evaluate (>=0.2.0)", "faiss-cpu", "fugashi (>=1.0)", "ipadic (>=1.0.0,<2.0)", "isort (>=5.5.4)", "kenlm", "kernels (>=0.4.4,<0.5)", "libcst", "librosa", "nltk (<=3.8.1)", "num2words", "onnxruntime (>=1.4.0)", "onnxruntime-tools (>=1.4.2)", "optuna", "parameterized", "phonemizer", "protobuf", "psutil", "pyctcdecode (>=0.4.0)", "pydantic", "pytest (>=7.2.0)", "pytest-asyncio", "pytest-order", "pytest-rerunfailures", "pytest-rich", "pytest-timeout", "pytest-xdist", "ray[tune] (>=2.7.0)", "rhoknp (>=1.1.0,<1.3.1)", "rich", "rjieba", "rouge-score (!=0.0.7,!=0.0.8,!=0.1,!=0.1.1)", "ruff (==0.11.2)", "sacrebleu (>=1.4.12,<2.0.0)", "sacremoses", "scikit-learn", "sentencepiece (>=0.1.91,!=0.1.92)", "sigopt", "sudachidict-core (>=20220729)", "sudachipy (>=0.6.6)", "tensorboard", "timeout-decorator", "timm (<=1.0.11)", "tokenizers (>=0.21,<0.22)", "torch (>=2.1,<2.7)", "torchaudio", "torchvision", "unidic (>=1.0.2)", "unidic-lite (>=1.0.7)", "urllib3 (<2.0.0)"] +flax = ["flax (>=0.4.1,<=0.7.0)", "jax (>=0.4.1,<=0.4.13)", "jaxlib (>=0.4.1,<=0.4.13)", "optax (>=0.0.8,<=0.1.4)", "scipy (<1.13.0)"] +flax-speech = ["kenlm", "librosa", "phonemizer", "pyctcdecode (>=0.4.0)"] +ftfy = ["ftfy"] +hf-xet = ["hf-xet"] +hub-kernels = ["kernels (>=0.4.4,<0.5)"] +integrations = ["kernels (>=0.4.4,<0.5)", "optuna", "ray[tune] (>=2.7.0)", "sigopt"] +ja = ["fugashi (>=1.0)", "ipadic (>=1.0.0,<2.0)", "rhoknp (>=1.1.0,<1.3.1)", "sudachidict-core (>=20220729)", "sudachipy (>=0.6.6)", "unidic (>=1.0.2)", "unidic-lite (>=1.0.7)"] +modelcreation = ["cookiecutter (==1.7.3)"] +natten = ["natten (>=0.14.6,<0.15.0)"] +num2words = ["num2words"] +onnx = ["onnxconverter-common", "onnxruntime (>=1.4.0)", "onnxruntime-tools (>=1.4.2)", "tf2onnx"] +onnxruntime = ["onnxruntime (>=1.4.0)", "onnxruntime-tools (>=1.4.2)"] +optuna = ["optuna"] +quality = ["GitPython (<3.1.19)", "datasets (!=2.5.0)", "isort (>=5.5.4)", "libcst", "rich", "ruff (==0.11.2)", "urllib3 (<2.0.0)"] +ray = ["ray[tune] (>=2.7.0)"] +retrieval = ["datasets (!=2.5.0)", "faiss-cpu"] +ruff = ["ruff (==0.11.2)"] +sagemaker = ["sagemaker (>=2.31.0)"] +sentencepiece = ["protobuf", "sentencepiece (>=0.1.91,!=0.1.92)"] +serving = ["fastapi", "pydantic", "starlette", "uvicorn"] +sigopt = ["sigopt"] +sklearn = ["scikit-learn"] +speech = ["kenlm", "librosa", "phonemizer", "pyctcdecode (>=0.4.0)", "torchaudio"] +testing = ["GitPython (<3.1.19)", "beautifulsoup4", "cookiecutter (==1.7.3)", "datasets (!=2.5.0)", "dill (<0.3.5)", "evaluate (>=0.2.0)", "faiss-cpu", "nltk (<=3.8.1)", "parameterized", "psutil", "pydantic", "pytest (>=7.2.0)", "pytest-asyncio", "pytest-order", "pytest-rerunfailures", "pytest-rich", "pytest-timeout", "pytest-xdist", "rjieba", "rouge-score (!=0.0.7,!=0.0.8,!=0.1,!=0.1.1)", "ruff (==0.11.2)", "sacrebleu (>=1.4.12,<2.0.0)", "sacremoses", "sentencepiece (>=0.1.91,!=0.1.92)", "tensorboard", "timeout-decorator"] +tf = ["keras-nlp (>=0.3.1,<0.14.0)", "onnxconverter-common", "tensorflow (>2.9,<2.16)", "tensorflow-text (<2.16)", "tf2onnx"] +tf-cpu = ["keras (>2.9,<2.16)", "keras-nlp (>=0.3.1,<0.14.0)", "onnxconverter-common", "tensorflow-cpu (>2.9,<2.16)", "tensorflow-probability (<0.24)", "tensorflow-text (<2.16)", "tf2onnx"] +tf-speech = ["kenlm", "librosa", "phonemizer", "pyctcdecode (>=0.4.0)"] +tiktoken = ["blobfile", "tiktoken"] +timm = ["timm (<=1.0.11)"] +tokenizers = ["tokenizers (>=0.21,<0.22)"] +torch = ["accelerate (>=0.26.0)", "torch (>=2.1,<2.7)"] +torch-speech = ["kenlm", "librosa", "phonemizer", "pyctcdecode (>=0.4.0)", "torchaudio"] +torch-vision = ["Pillow (>=10.0.1,<=15.0)", "torchvision"] +torchhub = ["filelock", "huggingface-hub (>=0.30.0,<1.0)", "importlib-metadata", "numpy (>=1.17)", "packaging (>=20.0)", "protobuf", "regex (!=2019.12.17)", "requests", "sentencepiece (>=0.1.91,!=0.1.92)", "tokenizers (>=0.21,<0.22)", "torch (>=2.1,<2.7)", "tqdm (>=4.27)"] +video = ["av"] +vision = ["Pillow (>=10.0.1,<=15.0)"] + +[[package]] +name = "typer" +version = "0.16.0" +description = "Typer, build great CLIs. Easy to code. Based on Python type hints." +optional = false +python-versions = ">=3.7" +files = [ + {file = "typer-0.16.0-py3-none-any.whl", hash = "sha256:1f79bed11d4d02d4310e3c1b7ba594183bcedb0ac73b27a9e5f28f6fb5b98855"}, + {file = "typer-0.16.0.tar.gz", hash = "sha256:af377ffaee1dbe37ae9440cb4e8f11686ea5ce4e9bae01b84ae7c63b87f1dd3b"}, +] + +[package.dependencies] +click = ">=8.0.0" +rich = ">=10.11.0" +shellingham = ">=1.3.0" +typing-extensions = ">=3.7.4.3" + +[[package]] +name = "typing-extensions" +version = "4.14.0" +description = "Backported and Experimental Type Hints for Python 3.9+" +optional = false +python-versions = ">=3.9" +files = [ + {file = "typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af"}, + {file = "typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4"}, +] + +[[package]] +name = "typing-inspection" +version = "0.4.1" +description = "Runtime typing introspection tools" +optional = false +python-versions = ">=3.9" +files = [ + {file = "typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51"}, + {file = "typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28"}, +] + +[package.dependencies] +typing-extensions = ">=4.12.0" + +[[package]] +name = "tzdata" +version = "2025.2" +description = "Provider of IANA time zone data" +optional = false +python-versions = ">=2" +files = [ + {file = "tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8"}, + {file = "tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9"}, +] + +[[package]] +name = "urllib3" +version = "2.4.0" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.9" +files = [ + {file = "urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813"}, + {file = "urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "uvicorn" +version = "0.34.3" +description = "The lightning-fast ASGI server." +optional = false +python-versions = ">=3.9" +files = [ + {file = "uvicorn-0.34.3-py3-none-any.whl", hash = "sha256:16246631db62bdfbf069b0645177d6e8a77ba950cfedbfd093acef9444e4d885"}, + {file = "uvicorn-0.34.3.tar.gz", hash = "sha256:35919a9a979d7a59334b6b10e05d77c1d0d574c50e0fc98b8b1a0f165708b55a"}, +] + +[package.dependencies] +click = ">=7.0" +colorama = {version = ">=0.4", optional = true, markers = "sys_platform == \"win32\" and extra == \"standard\""} +h11 = ">=0.8" +httptools = {version = ">=0.6.3", optional = true, markers = "extra == \"standard\""} +python-dotenv = {version = ">=0.13", optional = true, markers = "extra == \"standard\""} +pyyaml = {version = ">=5.1", optional = true, markers = "extra == \"standard\""} +uvloop = {version = ">=0.15.1", optional = true, markers = "(sys_platform != \"win32\" and sys_platform != \"cygwin\") and platform_python_implementation != \"PyPy\" and extra == \"standard\""} +watchfiles = {version = ">=0.13", optional = true, markers = "extra == \"standard\""} +websockets = {version = ">=10.4", optional = true, markers = "extra == \"standard\""} + +[package.extras] +standard = ["colorama (>=0.4)", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] + +[[package]] +name = "uvloop" +version = "0.21.0" +description = "Fast implementation of asyncio event loop on top of libuv" +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "uvloop-0.21.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ec7e6b09a6fdded42403182ab6b832b71f4edaf7f37a9a0e371a01db5f0cb45f"}, + {file = "uvloop-0.21.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:196274f2adb9689a289ad7d65700d37df0c0930fd8e4e743fa4834e850d7719d"}, + {file = "uvloop-0.21.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f38b2e090258d051d68a5b14d1da7203a3c3677321cf32a95a6f4db4dd8b6f26"}, + {file = "uvloop-0.21.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87c43e0f13022b998eb9b973b5e97200c8b90823454d4bc06ab33829e09fb9bb"}, + {file = "uvloop-0.21.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:10d66943def5fcb6e7b37310eb6b5639fd2ccbc38df1177262b0640c3ca68c1f"}, + {file = "uvloop-0.21.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:67dd654b8ca23aed0a8e99010b4c34aca62f4b7fce88f39d452ed7622c94845c"}, + {file = "uvloop-0.21.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c0f3fa6200b3108919f8bdabb9a7f87f20e7097ea3c543754cabc7d717d95cf8"}, + {file = "uvloop-0.21.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0878c2640cf341b269b7e128b1a5fed890adc4455513ca710d77d5e93aa6d6a0"}, + {file = "uvloop-0.21.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9fb766bb57b7388745d8bcc53a359b116b8a04c83a2288069809d2b3466c37e"}, + {file = "uvloop-0.21.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a375441696e2eda1c43c44ccb66e04d61ceeffcd76e4929e527b7fa401b90fb"}, + {file = "uvloop-0.21.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:baa0e6291d91649c6ba4ed4b2f982f9fa165b5bbd50a9e203c416a2797bab3c6"}, + {file = "uvloop-0.21.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4509360fcc4c3bd2c70d87573ad472de40c13387f5fda8cb58350a1d7475e58d"}, + {file = "uvloop-0.21.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:359ec2c888397b9e592a889c4d72ba3d6befba8b2bb01743f72fffbde663b59c"}, + {file = "uvloop-0.21.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2"}, + {file = "uvloop-0.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:baa4dcdbd9ae0a372f2167a207cd98c9f9a1ea1188a8a526431eef2f8116cc8d"}, + {file = "uvloop-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc"}, + {file = "uvloop-0.21.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb"}, + {file = "uvloop-0.21.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f"}, + {file = "uvloop-0.21.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281"}, + {file = "uvloop-0.21.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af"}, + {file = "uvloop-0.21.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6"}, + {file = "uvloop-0.21.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816"}, + {file = "uvloop-0.21.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc"}, + {file = "uvloop-0.21.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553"}, + {file = "uvloop-0.21.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:17df489689befc72c39a08359efac29bbee8eee5209650d4b9f34df73d22e414"}, + {file = "uvloop-0.21.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bc09f0ff191e61c2d592a752423c767b4ebb2986daa9ed62908e2b1b9a9ae206"}, + {file = "uvloop-0.21.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0ce1b49560b1d2d8a2977e3ba4afb2414fb46b86a1b64056bc4ab929efdafbe"}, + {file = "uvloop-0.21.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e678ad6fe52af2c58d2ae3c73dc85524ba8abe637f134bf3564ed07f555c5e79"}, + {file = "uvloop-0.21.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:460def4412e473896ef179a1671b40c039c7012184b627898eea5072ef6f017a"}, + {file = "uvloop-0.21.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:10da8046cc4a8f12c91a1c39d1dd1585c41162a15caaef165c2174db9ef18bdc"}, + {file = "uvloop-0.21.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c097078b8031190c934ed0ebfee8cc5f9ba9642e6eb88322b9958b649750f72b"}, + {file = "uvloop-0.21.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:46923b0b5ee7fc0020bef24afe7836cb068f5050ca04caf6b487c513dc1a20b2"}, + {file = "uvloop-0.21.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:53e420a3afe22cdcf2a0f4846e377d16e718bc70103d7088a4f7623567ba5fb0"}, + {file = "uvloop-0.21.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88cb67cdbc0e483da00af0b2c3cdad4b7c61ceb1ee0f33fe00e09c81e3a6cb75"}, + {file = "uvloop-0.21.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:221f4f2a1f46032b403bf3be628011caf75428ee3cc204a22addf96f586b19fd"}, + {file = "uvloop-0.21.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2d1f581393673ce119355d56da84fe1dd9d2bb8b3d13ce792524e1607139feff"}, + {file = "uvloop-0.21.0.tar.gz", hash = "sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3"}, +] + +[package.extras] +dev = ["Cython (>=3.0,<4.0)", "setuptools (>=60)"] +docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] +test = ["aiohttp (>=3.10.5)", "flake8 (>=5.0,<6.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=23.0.0,<23.1.0)", "pycodestyle (>=2.9.0,<2.10.0)"] + +[[package]] +name = "watchfiles" +version = "1.0.5" +description = "Simple, modern and high performance file watching and code reload in python." +optional = false +python-versions = ">=3.9" +files = [ + {file = "watchfiles-1.0.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:5c40fe7dd9e5f81e0847b1ea64e1f5dd79dd61afbedb57759df06767ac719b40"}, + {file = "watchfiles-1.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8c0db396e6003d99bb2d7232c957b5f0b5634bbd1b24e381a5afcc880f7373fb"}, + {file = "watchfiles-1.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b551d4fb482fc57d852b4541f911ba28957d051c8776e79c3b4a51eb5e2a1b11"}, + {file = "watchfiles-1.0.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:830aa432ba5c491d52a15b51526c29e4a4b92bf4f92253787f9726fe01519487"}, + {file = "watchfiles-1.0.5-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a16512051a822a416b0d477d5f8c0e67b67c1a20d9acecb0aafa3aa4d6e7d256"}, + {file = "watchfiles-1.0.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfe0cbc787770e52a96c6fda6726ace75be7f840cb327e1b08d7d54eadc3bc85"}, + {file = "watchfiles-1.0.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d363152c5e16b29d66cbde8fa614f9e313e6f94a8204eaab268db52231fe5358"}, + {file = "watchfiles-1.0.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ee32c9a9bee4d0b7bd7cbeb53cb185cf0b622ac761efaa2eba84006c3b3a614"}, + {file = "watchfiles-1.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29c7fd632ccaf5517c16a5188e36f6612d6472ccf55382db6c7fe3fcccb7f59f"}, + {file = "watchfiles-1.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8e637810586e6fe380c8bc1b3910accd7f1d3a9a7262c8a78d4c8fb3ba6a2b3d"}, + {file = "watchfiles-1.0.5-cp310-cp310-win32.whl", hash = "sha256:cd47d063fbeabd4c6cae1d4bcaa38f0902f8dc5ed168072874ea11d0c7afc1ff"}, + {file = "watchfiles-1.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:86c0df05b47a79d80351cd179893f2f9c1b1cae49d96e8b3290c7f4bd0ca0a92"}, + {file = "watchfiles-1.0.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:237f9be419e977a0f8f6b2e7b0475ababe78ff1ab06822df95d914a945eac827"}, + {file = "watchfiles-1.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0da39ff917af8b27a4bdc5a97ac577552a38aac0d260a859c1517ea3dc1a7c4"}, + {file = "watchfiles-1.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cfcb3952350e95603f232a7a15f6c5f86c5375e46f0bd4ae70d43e3e063c13d"}, + {file = "watchfiles-1.0.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:68b2dddba7a4e6151384e252a5632efcaa9bc5d1c4b567f3cb621306b2ca9f63"}, + {file = "watchfiles-1.0.5-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:95cf944fcfc394c5f9de794ce581914900f82ff1f855326f25ebcf24d5397418"}, + {file = "watchfiles-1.0.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ecf6cd9f83d7c023b1aba15d13f705ca7b7d38675c121f3cc4a6e25bd0857ee9"}, + {file = "watchfiles-1.0.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:852de68acd6212cd6d33edf21e6f9e56e5d98c6add46f48244bd479d97c967c6"}, + {file = "watchfiles-1.0.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5730f3aa35e646103b53389d5bc77edfbf578ab6dab2e005142b5b80a35ef25"}, + {file = "watchfiles-1.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:18b3bd29954bc4abeeb4e9d9cf0b30227f0f206c86657674f544cb032296acd5"}, + {file = "watchfiles-1.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ba5552a1b07c8edbf197055bc9d518b8f0d98a1c6a73a293bc0726dce068ed01"}, + {file = "watchfiles-1.0.5-cp311-cp311-win32.whl", hash = "sha256:2f1fefb2e90e89959447bc0420fddd1e76f625784340d64a2f7d5983ef9ad246"}, + {file = "watchfiles-1.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:b6e76ceb1dd18c8e29c73f47d41866972e891fc4cc7ba014f487def72c1cf096"}, + {file = "watchfiles-1.0.5-cp311-cp311-win_arm64.whl", hash = "sha256:266710eb6fddc1f5e51843c70e3bebfb0f5e77cf4f27129278c70554104d19ed"}, + {file = "watchfiles-1.0.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b5eb568c2aa6018e26da9e6c86f3ec3fd958cee7f0311b35c2630fa4217d17f2"}, + {file = "watchfiles-1.0.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0a04059f4923ce4e856b4b4e5e783a70f49d9663d22a4c3b3298165996d1377f"}, + {file = "watchfiles-1.0.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e380c89983ce6e6fe2dd1e1921b9952fb4e6da882931abd1824c092ed495dec"}, + {file = "watchfiles-1.0.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fe43139b2c0fdc4a14d4f8d5b5d967f7a2777fd3d38ecf5b1ec669b0d7e43c21"}, + {file = "watchfiles-1.0.5-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee0822ce1b8a14fe5a066f93edd20aada932acfe348bede8aa2149f1a4489512"}, + {file = "watchfiles-1.0.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a0dbcb1c2d8f2ab6e0a81c6699b236932bd264d4cef1ac475858d16c403de74d"}, + {file = "watchfiles-1.0.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a2014a2b18ad3ca53b1f6c23f8cd94a18ce930c1837bd891262c182640eb40a6"}, + {file = "watchfiles-1.0.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10f6ae86d5cb647bf58f9f655fcf577f713915a5d69057a0371bc257e2553234"}, + {file = "watchfiles-1.0.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1a7bac2bde1d661fb31f4d4e8e539e178774b76db3c2c17c4bb3e960a5de07a2"}, + {file = "watchfiles-1.0.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ab626da2fc1ac277bbf752446470b367f84b50295264d2d313e28dc4405d663"}, + {file = "watchfiles-1.0.5-cp312-cp312-win32.whl", hash = "sha256:9f4571a783914feda92018ef3901dab8caf5b029325b5fe4558c074582815249"}, + {file = "watchfiles-1.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:360a398c3a19672cf93527f7e8d8b60d8275119c5d900f2e184d32483117a705"}, + {file = "watchfiles-1.0.5-cp312-cp312-win_arm64.whl", hash = "sha256:1a2902ede862969077b97523987c38db28abbe09fb19866e711485d9fbf0d417"}, + {file = "watchfiles-1.0.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:0b289572c33a0deae62daa57e44a25b99b783e5f7aed81b314232b3d3c81a11d"}, + {file = "watchfiles-1.0.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a056c2f692d65bf1e99c41045e3bdcaea3cb9e6b5a53dcaf60a5f3bd95fc9763"}, + {file = "watchfiles-1.0.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9dca99744991fc9850d18015c4f0438865414e50069670f5f7eee08340d8b40"}, + {file = "watchfiles-1.0.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:894342d61d355446d02cd3988a7326af344143eb33a2fd5d38482a92072d9563"}, + {file = "watchfiles-1.0.5-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ab44e1580924d1ffd7b3938e02716d5ad190441965138b4aa1d1f31ea0877f04"}, + {file = "watchfiles-1.0.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d6f9367b132078b2ceb8d066ff6c93a970a18c3029cea37bfd7b2d3dd2e5db8f"}, + {file = "watchfiles-1.0.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2e55a9b162e06e3f862fb61e399fe9f05d908d019d87bf5b496a04ef18a970a"}, + {file = "watchfiles-1.0.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0125f91f70e0732a9f8ee01e49515c35d38ba48db507a50c5bdcad9503af5827"}, + {file = "watchfiles-1.0.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:13bb21f8ba3248386337c9fa51c528868e6c34a707f729ab041c846d52a0c69a"}, + {file = "watchfiles-1.0.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:839ebd0df4a18c5b3c1b890145b5a3f5f64063c2a0d02b13c76d78fe5de34936"}, + {file = "watchfiles-1.0.5-cp313-cp313-win32.whl", hash = "sha256:4a8ec1e4e16e2d5bafc9ba82f7aaecfeec990ca7cd27e84fb6f191804ed2fcfc"}, + {file = "watchfiles-1.0.5-cp313-cp313-win_amd64.whl", hash = "sha256:f436601594f15bf406518af922a89dcaab416568edb6f65c4e5bbbad1ea45c11"}, + {file = "watchfiles-1.0.5-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:2cfb371be97d4db374cba381b9f911dd35bb5f4c58faa7b8b7106c8853e5d225"}, + {file = "watchfiles-1.0.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a3904d88955fda461ea2531fcf6ef73584ca921415d5cfa44457a225f4a42bc1"}, + {file = "watchfiles-1.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2b7a21715fb12274a71d335cff6c71fe7f676b293d322722fe708a9ec81d91f5"}, + {file = "watchfiles-1.0.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dfd6ae1c385ab481766b3c61c44aca2b3cd775f6f7c0fa93d979ddec853d29d5"}, + {file = "watchfiles-1.0.5-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b659576b950865fdad31fa491d31d37cf78b27113a7671d39f919828587b429b"}, + {file = "watchfiles-1.0.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1909e0a9cd95251b15bff4261de5dd7550885bd172e3536824bf1cf6b121e200"}, + {file = "watchfiles-1.0.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:832ccc221927c860e7286c55c9b6ebcc0265d5e072f49c7f6456c7798d2b39aa"}, + {file = "watchfiles-1.0.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85fbb6102b3296926d0c62cfc9347f6237fb9400aecd0ba6bbda94cae15f2b3b"}, + {file = "watchfiles-1.0.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:15ac96dd567ad6c71c71f7b2c658cb22b7734901546cd50a475128ab557593ca"}, + {file = "watchfiles-1.0.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4b6227351e11c57ae997d222e13f5b6f1f0700d84b8c52304e8675d33a808382"}, + {file = "watchfiles-1.0.5-cp39-cp39-win32.whl", hash = "sha256:974866e0db748ebf1eccab17862bc0f0303807ed9cda465d1324625b81293a18"}, + {file = "watchfiles-1.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:9848b21ae152fe79c10dd0197304ada8f7b586d3ebc3f27f43c506e5a52a863c"}, + {file = "watchfiles-1.0.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f59b870db1f1ae5a9ac28245707d955c8721dd6565e7f411024fa374b5362d1d"}, + {file = "watchfiles-1.0.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9475b0093767e1475095f2aeb1d219fb9664081d403d1dff81342df8cd707034"}, + {file = "watchfiles-1.0.5-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc533aa50664ebd6c628b2f30591956519462f5d27f951ed03d6c82b2dfd9965"}, + {file = "watchfiles-1.0.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fed1cd825158dcaae36acce7b2db33dcbfd12b30c34317a88b8ed80f0541cc57"}, + {file = "watchfiles-1.0.5-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:554389562c29c2c182e3908b149095051f81d28c2fec79ad6c8997d7d63e0009"}, + {file = "watchfiles-1.0.5-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a74add8d7727e6404d5dc4dcd7fac65d4d82f95928bbee0cf5414c900e86773e"}, + {file = "watchfiles-1.0.5-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb1489f25b051a89fae574505cc26360c8e95e227a9500182a7fe0afcc500ce0"}, + {file = "watchfiles-1.0.5-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0901429650652d3f0da90bad42bdafc1f9143ff3605633c455c999a2d786cac"}, + {file = "watchfiles-1.0.5.tar.gz", hash = "sha256:b7529b5dcc114679d43827d8c35a07c493ad6f083633d573d81c660abc5979e9"}, +] + +[package.dependencies] +anyio = ">=3.0.0" + +[[package]] +name = "websocket-client" +version = "1.8.0" +description = "WebSocket client for Python with low level API options" +optional = false +python-versions = ">=3.8" +files = [ + {file = "websocket_client-1.8.0-py3-none-any.whl", hash = "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526"}, + {file = "websocket_client-1.8.0.tar.gz", hash = "sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da"}, +] + +[package.extras] +docs = ["Sphinx (>=6.0)", "myst-parser (>=2.0.0)", "sphinx-rtd-theme (>=1.1.0)"] +optional = ["python-socks", "wsaccel"] +test = ["websockets"] + +[[package]] +name = "websockets" +version = "15.0.1" +description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" +optional = false +python-versions = ">=3.9" +files = [ + {file = "websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b"}, + {file = "websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205"}, + {file = "websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a"}, + {file = "websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e"}, + {file = "websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf"}, + {file = "websockets-15.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb"}, + {file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d"}, + {file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9"}, + {file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c"}, + {file = "websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256"}, + {file = "websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41"}, + {file = "websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431"}, + {file = "websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57"}, + {file = "websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905"}, + {file = "websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562"}, + {file = "websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792"}, + {file = "websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413"}, + {file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8"}, + {file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3"}, + {file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf"}, + {file = "websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85"}, + {file = "websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065"}, + {file = "websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3"}, + {file = "websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665"}, + {file = "websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2"}, + {file = "websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215"}, + {file = "websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5"}, + {file = "websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65"}, + {file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe"}, + {file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4"}, + {file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597"}, + {file = "websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9"}, + {file = "websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7"}, + {file = "websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931"}, + {file = "websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675"}, + {file = "websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151"}, + {file = "websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22"}, + {file = "websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f"}, + {file = "websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8"}, + {file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375"}, + {file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d"}, + {file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4"}, + {file = "websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa"}, + {file = "websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561"}, + {file = "websockets-15.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5f4c04ead5aed67c8a1a20491d54cdfba5884507a48dd798ecaf13c74c4489f5"}, + {file = "websockets-15.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abdc0c6c8c648b4805c5eacd131910d2a7f6455dfd3becab248ef108e89ab16a"}, + {file = "websockets-15.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a625e06551975f4b7ea7102bc43895b90742746797e2e14b70ed61c43a90f09b"}, + {file = "websockets-15.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d591f8de75824cbb7acad4e05d2d710484f15f29d4a915092675ad3456f11770"}, + {file = "websockets-15.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47819cea040f31d670cc8d324bb6435c6f133b8c7a19ec3d61634e62f8d8f9eb"}, + {file = "websockets-15.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac017dd64572e5c3bd01939121e4d16cf30e5d7e110a119399cf3133b63ad054"}, + {file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4a9fac8e469d04ce6c25bb2610dc535235bd4aa14996b4e6dbebf5e007eba5ee"}, + {file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363c6f671b761efcb30608d24925a382497c12c506b51661883c3e22337265ed"}, + {file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2034693ad3097d5355bfdacfffcbd3ef5694f9718ab7f29c29689a9eae841880"}, + {file = "websockets-15.0.1-cp39-cp39-win32.whl", hash = "sha256:3b1ac0d3e594bf121308112697cf4b32be538fb1444468fb0a6ae4feebc83411"}, + {file = "websockets-15.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:b7643a03db5c95c799b89b31c036d5f27eeb4d259c798e878d6937d71832b1e4"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7f493881579c90fc262d9cdbaa05a6b54b3811c2f300766748db79f098db9940"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:47b099e1f4fbc95b701b6e85768e1fcdaf1630f3cbe4765fa216596f12310e2e"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67f2b6de947f8c757db2db9c71527933ad0019737ec374a8a6be9a956786aaf9"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d08eb4c2b7d6c41da6ca0600c077e93f5adcfd979cd777d747e9ee624556da4b"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b826973a4a2ae47ba357e4e82fa44a463b8f168e1ca775ac64521442b19e87f"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:21c1fa28a6a7e3cbdc171c694398b6df4744613ce9b36b1a498e816787e28123"}, + {file = "websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f"}, + {file = "websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee"}, +] + +[[package]] +name = "wrapt" +version = "1.17.2" +description = "Module for decorators, wrappers and monkey patching." +optional = false +python-versions = ">=3.8" +files = [ + {file = "wrapt-1.17.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3d57c572081fed831ad2d26fd430d565b76aa277ed1d30ff4d40670b1c0dd984"}, + {file = "wrapt-1.17.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b5e251054542ae57ac7f3fba5d10bfff615b6c2fb09abeb37d2f1463f841ae22"}, + {file = "wrapt-1.17.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:80dd7db6a7cb57ffbc279c4394246414ec99537ae81ffd702443335a61dbf3a7"}, + {file = "wrapt-1.17.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a6e821770cf99cc586d33833b2ff32faebdbe886bd6322395606cf55153246c"}, + {file = "wrapt-1.17.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b60fb58b90c6d63779cb0c0c54eeb38941bae3ecf7a73c764c52c88c2dcb9d72"}, + {file = "wrapt-1.17.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b870b5df5b71d8c3359d21be8f0d6c485fa0ebdb6477dda51a1ea54a9b558061"}, + {file = "wrapt-1.17.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4011d137b9955791f9084749cba9a367c68d50ab8d11d64c50ba1688c9b457f2"}, + {file = "wrapt-1.17.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:1473400e5b2733e58b396a04eb7f35f541e1fb976d0c0724d0223dd607e0f74c"}, + {file = "wrapt-1.17.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3cedbfa9c940fdad3e6e941db7138e26ce8aad38ab5fe9dcfadfed9db7a54e62"}, + {file = "wrapt-1.17.2-cp310-cp310-win32.whl", hash = "sha256:582530701bff1dec6779efa00c516496968edd851fba224fbd86e46cc6b73563"}, + {file = "wrapt-1.17.2-cp310-cp310-win_amd64.whl", hash = "sha256:58705da316756681ad3c9c73fd15499aa4d8c69f9fd38dc8a35e06c12468582f"}, + {file = "wrapt-1.17.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ff04ef6eec3eee8a5efef2401495967a916feaa353643defcc03fc74fe213b58"}, + {file = "wrapt-1.17.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4db983e7bca53819efdbd64590ee96c9213894272c776966ca6306b73e4affda"}, + {file = "wrapt-1.17.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9abc77a4ce4c6f2a3168ff34b1da9b0f311a8f1cfd694ec96b0603dff1c79438"}, + {file = "wrapt-1.17.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b929ac182f5ace000d459c59c2c9c33047e20e935f8e39371fa6e3b85d56f4a"}, + {file = "wrapt-1.17.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f09b286faeff3c750a879d336fb6d8713206fc97af3adc14def0cdd349df6000"}, + {file = "wrapt-1.17.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a7ed2d9d039bd41e889f6fb9364554052ca21ce823580f6a07c4ec245c1f5d6"}, + {file = "wrapt-1.17.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:129a150f5c445165ff941fc02ee27df65940fcb8a22a61828b1853c98763a64b"}, + {file = "wrapt-1.17.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1fb5699e4464afe5c7e65fa51d4f99e0b2eadcc176e4aa33600a3df7801d6662"}, + {file = "wrapt-1.17.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9a2bce789a5ea90e51a02dfcc39e31b7f1e662bc3317979aa7e5538e3a034f72"}, + {file = "wrapt-1.17.2-cp311-cp311-win32.whl", hash = "sha256:4afd5814270fdf6380616b321fd31435a462019d834f83c8611a0ce7484c7317"}, + {file = "wrapt-1.17.2-cp311-cp311-win_amd64.whl", hash = "sha256:acc130bc0375999da18e3d19e5a86403667ac0c4042a094fefb7eec8ebac7cf3"}, + {file = "wrapt-1.17.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d5e2439eecc762cd85e7bd37161d4714aa03a33c5ba884e26c81559817ca0925"}, + {file = "wrapt-1.17.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fc7cb4c1c744f8c05cd5f9438a3caa6ab94ce8344e952d7c45a8ed59dd88392"}, + {file = "wrapt-1.17.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8fdbdb757d5390f7c675e558fd3186d590973244fab0c5fe63d373ade3e99d40"}, + {file = "wrapt-1.17.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bb1d0dbf99411f3d871deb6faa9aabb9d4e744d67dcaaa05399af89d847a91d"}, + {file = "wrapt-1.17.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d18a4865f46b8579d44e4fe1e2bcbc6472ad83d98e22a26c963d46e4c125ef0b"}, + {file = "wrapt-1.17.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc570b5f14a79734437cb7b0500376b6b791153314986074486e0b0fa8d71d98"}, + {file = "wrapt-1.17.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6d9187b01bebc3875bac9b087948a2bccefe464a7d8f627cf6e48b1bbae30f82"}, + {file = "wrapt-1.17.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9e8659775f1adf02eb1e6f109751268e493c73716ca5761f8acb695e52a756ae"}, + {file = "wrapt-1.17.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e8b2816ebef96d83657b56306152a93909a83f23994f4b30ad4573b00bd11bb9"}, + {file = "wrapt-1.17.2-cp312-cp312-win32.whl", hash = "sha256:468090021f391fe0056ad3e807e3d9034e0fd01adcd3bdfba977b6fdf4213ea9"}, + {file = "wrapt-1.17.2-cp312-cp312-win_amd64.whl", hash = "sha256:ec89ed91f2fa8e3f52ae53cd3cf640d6feff92ba90d62236a81e4e563ac0e991"}, + {file = "wrapt-1.17.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6ed6ffac43aecfe6d86ec5b74b06a5be33d5bb9243d055141e8cabb12aa08125"}, + {file = "wrapt-1.17.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35621ae4c00e056adb0009f8e86e28eb4a41a4bfa8f9bfa9fca7d343fe94f998"}, + {file = "wrapt-1.17.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a604bf7a053f8362d27eb9fefd2097f82600b856d5abe996d623babd067b1ab5"}, + {file = "wrapt-1.17.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cbabee4f083b6b4cd282f5b817a867cf0b1028c54d445b7ec7cfe6505057cf8"}, + {file = "wrapt-1.17.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49703ce2ddc220df165bd2962f8e03b84c89fee2d65e1c24a7defff6f988f4d6"}, + {file = "wrapt-1.17.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8112e52c5822fc4253f3901b676c55ddf288614dc7011634e2719718eaa187dc"}, + {file = "wrapt-1.17.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9fee687dce376205d9a494e9c121e27183b2a3df18037f89d69bd7b35bcf59e2"}, + {file = "wrapt-1.17.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:18983c537e04d11cf027fbb60a1e8dfd5190e2b60cc27bc0808e653e7b218d1b"}, + {file = "wrapt-1.17.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:703919b1633412ab54bcf920ab388735832fdcb9f9a00ae49387f0fe67dad504"}, + {file = "wrapt-1.17.2-cp313-cp313-win32.whl", hash = "sha256:abbb9e76177c35d4e8568e58650aa6926040d6a9f6f03435b7a522bf1c487f9a"}, + {file = "wrapt-1.17.2-cp313-cp313-win_amd64.whl", hash = "sha256:69606d7bb691b50a4240ce6b22ebb319c1cfb164e5f6569835058196e0f3a845"}, + {file = "wrapt-1.17.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:4a721d3c943dae44f8e243b380cb645a709ba5bd35d3ad27bc2ed947e9c68192"}, + {file = "wrapt-1.17.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:766d8bbefcb9e00c3ac3b000d9acc51f1b399513f44d77dfe0eb026ad7c9a19b"}, + {file = "wrapt-1.17.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e496a8ce2c256da1eb98bd15803a79bee00fc351f5dfb9ea82594a3f058309e0"}, + {file = "wrapt-1.17.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d615e4fe22f4ad3528448c193b218e077656ca9ccb22ce2cb20db730f8d306"}, + {file = "wrapt-1.17.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a5aaeff38654462bc4b09023918b7f21790efb807f54c000a39d41d69cf552cb"}, + {file = "wrapt-1.17.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a7d15bbd2bc99e92e39f49a04653062ee6085c0e18b3b7512a4f2fe91f2d681"}, + {file = "wrapt-1.17.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e3890b508a23299083e065f435a492b5435eba6e304a7114d2f919d400888cc6"}, + {file = "wrapt-1.17.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8c8b293cd65ad716d13d8dd3624e42e5a19cc2a2f1acc74b30c2c13f15cb61a6"}, + {file = "wrapt-1.17.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c82b8785d98cdd9fed4cac84d765d234ed3251bd6afe34cb7ac523cb93e8b4f"}, + {file = "wrapt-1.17.2-cp313-cp313t-win32.whl", hash = "sha256:13e6afb7fe71fe7485a4550a8844cc9ffbe263c0f1a1eea569bc7091d4898555"}, + {file = "wrapt-1.17.2-cp313-cp313t-win_amd64.whl", hash = "sha256:eaf675418ed6b3b31c7a989fd007fa7c3be66ce14e5c3b27336383604c9da85c"}, + {file = "wrapt-1.17.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5c803c401ea1c1c18de70a06a6f79fcc9c5acfc79133e9869e730ad7f8ad8ef9"}, + {file = "wrapt-1.17.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f917c1180fdb8623c2b75a99192f4025e412597c50b2ac870f156de8fb101119"}, + {file = "wrapt-1.17.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ecc840861360ba9d176d413a5489b9a0aff6d6303d7e733e2c4623cfa26904a6"}, + {file = "wrapt-1.17.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb87745b2e6dc56361bfde481d5a378dc314b252a98d7dd19a651a3fa58f24a9"}, + {file = "wrapt-1.17.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:58455b79ec2661c3600e65c0a716955adc2410f7383755d537584b0de41b1d8a"}, + {file = "wrapt-1.17.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4e42a40a5e164cbfdb7b386c966a588b1047558a990981ace551ed7e12ca9c2"}, + {file = "wrapt-1.17.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:91bd7d1773e64019f9288b7a5101f3ae50d3d8e6b1de7edee9c2ccc1d32f0c0a"}, + {file = "wrapt-1.17.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:bb90fb8bda722a1b9d48ac1e6c38f923ea757b3baf8ebd0c82e09c5c1a0e7a04"}, + {file = "wrapt-1.17.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:08e7ce672e35efa54c5024936e559469436f8b8096253404faeb54d2a878416f"}, + {file = "wrapt-1.17.2-cp38-cp38-win32.whl", hash = "sha256:410a92fefd2e0e10d26210e1dfb4a876ddaf8439ef60d6434f21ef8d87efc5b7"}, + {file = "wrapt-1.17.2-cp38-cp38-win_amd64.whl", hash = "sha256:95c658736ec15602da0ed73f312d410117723914a5c91a14ee4cdd72f1d790b3"}, + {file = "wrapt-1.17.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:99039fa9e6306880572915728d7f6c24a86ec57b0a83f6b2491e1d8ab0235b9a"}, + {file = "wrapt-1.17.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2696993ee1eebd20b8e4ee4356483c4cb696066ddc24bd70bcbb80fa56ff9061"}, + {file = "wrapt-1.17.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:612dff5db80beef9e649c6d803a8d50c409082f1fedc9dbcdfde2983b2025b82"}, + {file = "wrapt-1.17.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62c2caa1585c82b3f7a7ab56afef7b3602021d6da34fbc1cf234ff139fed3cd9"}, + {file = "wrapt-1.17.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c958bcfd59bacc2d0249dcfe575e71da54f9dcf4a8bdf89c4cb9a68a1170d73f"}, + {file = "wrapt-1.17.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc78a84e2dfbc27afe4b2bd7c80c8db9bca75cc5b85df52bfe634596a1da846b"}, + {file = "wrapt-1.17.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ba0f0eb61ef00ea10e00eb53a9129501f52385c44853dbd6c4ad3f403603083f"}, + {file = "wrapt-1.17.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1e1fe0e6ab7775fd842bc39e86f6dcfc4507ab0ffe206093e76d61cde37225c8"}, + {file = "wrapt-1.17.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c86563182421896d73858e08e1db93afdd2b947a70064b813d515d66549e15f9"}, + {file = "wrapt-1.17.2-cp39-cp39-win32.whl", hash = "sha256:f393cda562f79828f38a819f4788641ac7c4085f30f1ce1a68672baa686482bb"}, + {file = "wrapt-1.17.2-cp39-cp39-win_amd64.whl", hash = "sha256:36ccae62f64235cf8ddb682073a60519426fdd4725524ae38874adf72b5f2aeb"}, + {file = "wrapt-1.17.2-py3-none-any.whl", hash = "sha256:b18f2d1533a71f069c7f82d524a52599053d4c7166e9dd374ae2136b7f40f7c8"}, + {file = "wrapt-1.17.2.tar.gz", hash = "sha256:41388e9d4d1522446fe79d3213196bd9e3b301a336965b9e27ca2788ebd122f3"}, +] + +[[package]] +name = "yarl" +version = "1.20.1" +description = "Yet another URL library" +optional = false +python-versions = ">=3.9" +files = [ + {file = "yarl-1.20.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6032e6da6abd41e4acda34d75a816012717000fa6839f37124a47fcefc49bec4"}, + {file = "yarl-1.20.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2c7b34d804b8cf9b214f05015c4fee2ebe7ed05cf581e7192c06555c71f4446a"}, + {file = "yarl-1.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0c869f2651cc77465f6cd01d938d91a11d9ea5d798738c1dc077f3de0b5e5fed"}, + {file = "yarl-1.20.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62915e6688eb4d180d93840cda4110995ad50c459bf931b8b3775b37c264af1e"}, + {file = "yarl-1.20.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:41ebd28167bc6af8abb97fec1a399f412eec5fd61a3ccbe2305a18b84fb4ca73"}, + {file = "yarl-1.20.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:21242b4288a6d56f04ea193adde174b7e347ac46ce6bc84989ff7c1b1ecea84e"}, + {file = "yarl-1.20.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bea21cdae6c7eb02ba02a475f37463abfe0a01f5d7200121b03e605d6a0439f8"}, + {file = "yarl-1.20.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f8a891e4a22a89f5dde7862994485e19db246b70bb288d3ce73a34422e55b23"}, + {file = "yarl-1.20.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dd803820d44c8853a109a34e3660e5a61beae12970da479cf44aa2954019bf70"}, + {file = "yarl-1.20.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b982fa7f74c80d5c0c7b5b38f908971e513380a10fecea528091405f519b9ebb"}, + {file = "yarl-1.20.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:33f29ecfe0330c570d997bcf1afd304377f2e48f61447f37e846a6058a4d33b2"}, + {file = "yarl-1.20.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:835ab2cfc74d5eb4a6a528c57f05688099da41cf4957cf08cad38647e4a83b30"}, + {file = "yarl-1.20.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:46b5e0ccf1943a9a6e766b2c2b8c732c55b34e28be57d8daa2b3c1d1d4009309"}, + {file = "yarl-1.20.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:df47c55f7d74127d1b11251fe6397d84afdde0d53b90bedb46a23c0e534f9d24"}, + {file = "yarl-1.20.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:76d12524d05841276b0e22573f28d5fbcb67589836772ae9244d90dd7d66aa13"}, + {file = "yarl-1.20.1-cp310-cp310-win32.whl", hash = "sha256:6c4fbf6b02d70e512d7ade4b1f998f237137f1417ab07ec06358ea04f69134f8"}, + {file = "yarl-1.20.1-cp310-cp310-win_amd64.whl", hash = "sha256:aef6c4d69554d44b7f9d923245f8ad9a707d971e6209d51279196d8e8fe1ae16"}, + {file = "yarl-1.20.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:47ee6188fea634bdfaeb2cc420f5b3b17332e6225ce88149a17c413c77ff269e"}, + {file = "yarl-1.20.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d0f6500f69e8402d513e5eedb77a4e1818691e8f45e6b687147963514d84b44b"}, + {file = "yarl-1.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7a8900a42fcdaad568de58887c7b2f602962356908eedb7628eaf6021a6e435b"}, + {file = "yarl-1.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bad6d131fda8ef508b36be3ece16d0902e80b88ea7200f030a0f6c11d9e508d4"}, + {file = "yarl-1.20.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:df018d92fe22aaebb679a7f89fe0c0f368ec497e3dda6cb81a567610f04501f1"}, + {file = "yarl-1.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f969afbb0a9b63c18d0feecf0db09d164b7a44a053e78a7d05f5df163e43833"}, + {file = "yarl-1.20.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:812303eb4aa98e302886ccda58d6b099e3576b1b9276161469c25803a8db277d"}, + {file = "yarl-1.20.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98c4a7d166635147924aa0bf9bfe8d8abad6fffa6102de9c99ea04a1376f91e8"}, + {file = "yarl-1.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:12e768f966538e81e6e7550f9086a6236b16e26cd964cf4df35349970f3551cf"}, + {file = "yarl-1.20.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fe41919b9d899661c5c28a8b4b0acf704510b88f27f0934ac7a7bebdd8938d5e"}, + {file = "yarl-1.20.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:8601bc010d1d7780592f3fc1bdc6c72e2b6466ea34569778422943e1a1f3c389"}, + {file = "yarl-1.20.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:daadbdc1f2a9033a2399c42646fbd46da7992e868a5fe9513860122d7fe7a73f"}, + {file = "yarl-1.20.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:03aa1e041727cb438ca762628109ef1333498b122e4c76dd858d186a37cec845"}, + {file = "yarl-1.20.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:642980ef5e0fa1de5fa96d905c7e00cb2c47cb468bfcac5a18c58e27dbf8d8d1"}, + {file = "yarl-1.20.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:86971e2795584fe8c002356d3b97ef6c61862720eeff03db2a7c86b678d85b3e"}, + {file = "yarl-1.20.1-cp311-cp311-win32.whl", hash = "sha256:597f40615b8d25812f14562699e287f0dcc035d25eb74da72cae043bb884d773"}, + {file = "yarl-1.20.1-cp311-cp311-win_amd64.whl", hash = "sha256:26ef53a9e726e61e9cd1cda6b478f17e350fb5800b4bd1cd9fe81c4d91cfeb2e"}, + {file = "yarl-1.20.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdcc4cd244e58593a4379fe60fdee5ac0331f8eb70320a24d591a3be197b94a9"}, + {file = "yarl-1.20.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b29a2c385a5f5b9c7d9347e5812b6f7ab267193c62d282a540b4fc528c8a9d2a"}, + {file = "yarl-1.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1112ae8154186dfe2de4732197f59c05a83dc814849a5ced892b708033f40dc2"}, + {file = "yarl-1.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90bbd29c4fe234233f7fa2b9b121fb63c321830e5d05b45153a2ca68f7d310ee"}, + {file = "yarl-1.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:680e19c7ce3710ac4cd964e90dad99bf9b5029372ba0c7cbfcd55e54d90ea819"}, + {file = "yarl-1.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4a979218c1fdb4246a05efc2cc23859d47c89af463a90b99b7c56094daf25a16"}, + {file = "yarl-1.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255b468adf57b4a7b65d8aad5b5138dce6a0752c139965711bdcb81bc370e1b6"}, + {file = "yarl-1.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a97d67108e79cfe22e2b430d80d7571ae57d19f17cda8bb967057ca8a7bf5bfd"}, + {file = "yarl-1.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8570d998db4ddbfb9a590b185a0a33dbf8aafb831d07a5257b4ec9948df9cb0a"}, + {file = "yarl-1.20.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:97c75596019baae7c71ccf1d8cc4738bc08134060d0adfcbe5642f778d1dca38"}, + {file = "yarl-1.20.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1c48912653e63aef91ff988c5432832692ac5a1d8f0fb8a33091520b5bbe19ef"}, + {file = "yarl-1.20.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4c3ae28f3ae1563c50f3d37f064ddb1511ecc1d5584e88c6b7c63cf7702a6d5f"}, + {file = "yarl-1.20.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c5e9642f27036283550f5f57dc6156c51084b458570b9d0d96100c8bebb186a8"}, + {file = "yarl-1.20.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2c26b0c49220d5799f7b22c6838409ee9bc58ee5c95361a4d7831f03cc225b5a"}, + {file = "yarl-1.20.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:564ab3d517e3d01c408c67f2e5247aad4019dcf1969982aba3974b4093279004"}, + {file = "yarl-1.20.1-cp312-cp312-win32.whl", hash = "sha256:daea0d313868da1cf2fac6b2d3a25c6e3a9e879483244be38c8e6a41f1d876a5"}, + {file = "yarl-1.20.1-cp312-cp312-win_amd64.whl", hash = "sha256:48ea7d7f9be0487339828a4de0360d7ce0efc06524a48e1810f945c45b813698"}, + {file = "yarl-1.20.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:0b5ff0fbb7c9f1b1b5ab53330acbfc5247893069e7716840c8e7d5bb7355038a"}, + {file = "yarl-1.20.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:14f326acd845c2b2e2eb38fb1346c94f7f3b01a4f5c788f8144f9b630bfff9a3"}, + {file = "yarl-1.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f60e4ad5db23f0b96e49c018596707c3ae89f5d0bd97f0ad3684bcbad899f1e7"}, + {file = "yarl-1.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:49bdd1b8e00ce57e68ba51916e4bb04461746e794e7c4d4bbc42ba2f18297691"}, + {file = "yarl-1.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:66252d780b45189975abfed839616e8fd2dbacbdc262105ad7742c6ae58f3e31"}, + {file = "yarl-1.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59174e7332f5d153d8f7452a102b103e2e74035ad085f404df2e40e663a22b28"}, + {file = "yarl-1.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e3968ec7d92a0c0f9ac34d5ecfd03869ec0cab0697c91a45db3fbbd95fe1b653"}, + {file = "yarl-1.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1a4fbb50e14396ba3d375f68bfe02215d8e7bc3ec49da8341fe3157f59d2ff5"}, + {file = "yarl-1.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11a62c839c3a8eac2410e951301309426f368388ff2f33799052787035793b02"}, + {file = "yarl-1.20.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:041eaa14f73ff5a8986b4388ac6bb43a77f2ea09bf1913df7a35d4646db69e53"}, + {file = "yarl-1.20.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:377fae2fef158e8fd9d60b4c8751387b8d1fb121d3d0b8e9b0be07d1b41e83dc"}, + {file = "yarl-1.20.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1c92f4390e407513f619d49319023664643d3339bd5e5a56a3bebe01bc67ec04"}, + {file = "yarl-1.20.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d25ddcf954df1754ab0f86bb696af765c5bfaba39b74095f27eececa049ef9a4"}, + {file = "yarl-1.20.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:909313577e9619dcff8c31a0ea2aa0a2a828341d92673015456b3ae492e7317b"}, + {file = "yarl-1.20.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:793fd0580cb9664548c6b83c63b43c477212c0260891ddf86809e1c06c8b08f1"}, + {file = "yarl-1.20.1-cp313-cp313-win32.whl", hash = "sha256:468f6e40285de5a5b3c44981ca3a319a4b208ccc07d526b20b12aeedcfa654b7"}, + {file = "yarl-1.20.1-cp313-cp313-win_amd64.whl", hash = "sha256:495b4ef2fea40596bfc0affe3837411d6aa3371abcf31aac0ccc4bdd64d4ef5c"}, + {file = "yarl-1.20.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:f60233b98423aab21d249a30eb27c389c14929f47be8430efa7dbd91493a729d"}, + {file = "yarl-1.20.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6f3eff4cc3f03d650d8755c6eefc844edde99d641d0dcf4da3ab27141a5f8ddf"}, + {file = "yarl-1.20.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:69ff8439d8ba832d6bed88af2c2b3445977eba9a4588b787b32945871c2444e3"}, + {file = "yarl-1.20.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cf34efa60eb81dd2645a2e13e00bb98b76c35ab5061a3989c7a70f78c85006d"}, + {file = "yarl-1.20.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8e0fe9364ad0fddab2688ce72cb7a8e61ea42eff3c7caeeb83874a5d479c896c"}, + {file = "yarl-1.20.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f64fbf81878ba914562c672024089e3401974a39767747691c65080a67b18c1"}, + {file = "yarl-1.20.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6342d643bf9a1de97e512e45e4b9560a043347e779a173250824f8b254bd5ce"}, + {file = "yarl-1.20.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56dac5f452ed25eef0f6e3c6a066c6ab68971d96a9fb441791cad0efba6140d3"}, + {file = "yarl-1.20.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7d7f497126d65e2cad8dc5f97d34c27b19199b6414a40cb36b52f41b79014be"}, + {file = "yarl-1.20.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:67e708dfb8e78d8a19169818eeb5c7a80717562de9051bf2413aca8e3696bf16"}, + {file = "yarl-1.20.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:595c07bc79af2494365cc96ddeb772f76272364ef7c80fb892ef9d0649586513"}, + {file = "yarl-1.20.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7bdd2f80f4a7df852ab9ab49484a4dee8030023aa536df41f2d922fd57bf023f"}, + {file = "yarl-1.20.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c03bfebc4ae8d862f853a9757199677ab74ec25424d0ebd68a0027e9c639a390"}, + {file = "yarl-1.20.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:344d1103e9c1523f32a5ed704d576172d2cabed3122ea90b1d4e11fe17c66458"}, + {file = "yarl-1.20.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:88cab98aa4e13e1ade8c141daeedd300a4603b7132819c484841bb7af3edce9e"}, + {file = "yarl-1.20.1-cp313-cp313t-win32.whl", hash = "sha256:b121ff6a7cbd4abc28985b6028235491941b9fe8fe226e6fdc539c977ea1739d"}, + {file = "yarl-1.20.1-cp313-cp313t-win_amd64.whl", hash = "sha256:541d050a355bbbc27e55d906bc91cb6fe42f96c01413dd0f4ed5a5240513874f"}, + {file = "yarl-1.20.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e42ba79e2efb6845ebab49c7bf20306c4edf74a0b20fc6b2ccdd1a219d12fad3"}, + {file = "yarl-1.20.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:41493b9b7c312ac448b7f0a42a089dffe1d6e6e981a2d76205801a023ed26a2b"}, + {file = "yarl-1.20.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f5a5928ff5eb13408c62a968ac90d43f8322fd56d87008b8f9dabf3c0f6ee983"}, + {file = "yarl-1.20.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30c41ad5d717b3961b2dd785593b67d386b73feca30522048d37298fee981805"}, + {file = "yarl-1.20.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:59febc3969b0781682b469d4aca1a5cab7505a4f7b85acf6db01fa500fa3f6ba"}, + {file = "yarl-1.20.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d2b6fb3622b7e5bf7a6e5b679a69326b4279e805ed1699d749739a61d242449e"}, + {file = "yarl-1.20.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:749d73611db8d26a6281086f859ea7ec08f9c4c56cec864e52028c8b328db723"}, + {file = "yarl-1.20.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9427925776096e664c39e131447aa20ec738bdd77c049c48ea5200db2237e000"}, + {file = "yarl-1.20.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff70f32aa316393eaf8222d518ce9118148eddb8a53073c2403863b41033eed5"}, + {file = "yarl-1.20.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c7ddf7a09f38667aea38801da8b8d6bfe81df767d9dfc8c88eb45827b195cd1c"}, + {file = "yarl-1.20.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:57edc88517d7fc62b174fcfb2e939fbc486a68315d648d7e74d07fac42cec240"}, + {file = "yarl-1.20.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:dab096ce479d5894d62c26ff4f699ec9072269d514b4edd630a393223f45a0ee"}, + {file = "yarl-1.20.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:14a85f3bd2d7bb255be7183e5d7d6e70add151a98edf56a770d6140f5d5f4010"}, + {file = "yarl-1.20.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2c89b5c792685dd9cd3fa9761c1b9f46fc240c2a3265483acc1565769996a3f8"}, + {file = "yarl-1.20.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:69e9b141de5511021942a6866990aea6d111c9042235de90e08f94cf972ca03d"}, + {file = "yarl-1.20.1-cp39-cp39-win32.whl", hash = "sha256:b5f307337819cdfdbb40193cad84978a029f847b0a357fbe49f712063cfc4f06"}, + {file = "yarl-1.20.1-cp39-cp39-win_amd64.whl", hash = "sha256:eae7bfe2069f9c1c5b05fc7fe5d612e5bbc089a39309904ee8b829e322dcad00"}, + {file = "yarl-1.20.1-py3-none-any.whl", hash = "sha256:83b8eb083fe4683c6115795d9fc1cfaf2cbbefb19b3a1cb68f6527460f483a77"}, + {file = "yarl-1.20.1.tar.gz", hash = "sha256:d017a4997ee50c91fd5466cef416231bb82177b93b029906cefc542ce14c35ac"}, +] + +[package.dependencies] +idna = ">=2.0" +multidict = ">=4.0" +propcache = ">=0.2.1" + +[[package]] +name = "zipp" +version = "3.23.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +optional = false +python-versions = ">=3.9" +files = [ + {file = "zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e"}, + {file = "zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166"}, +] + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more_itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] +type = ["pytest-mypy"] + +[metadata] +lock-version = "2.0" +python-versions = "^3.11" +content-hash = "4cd10a7ded9e15de48c8786ab2753a366106f4930f07124996c0c4716e8eff04" diff --git a/vector/pyproject.toml b/vector/pyproject.toml new file mode 100644 index 0000000..efee533 --- /dev/null +++ b/vector/pyproject.toml @@ -0,0 +1,41 @@ +[tool.poetry] +name = "vector-api" +version = "1.0.0" +description = "Vector DB API with AI/ML capabilities" +authors = ["Developer "] +packages = [{include = "app"}] + +[tool.poetry.dependencies] +python = "^3.11" +fastapi = "0.115.9" +uvicorn = {extras = ["standard"], version = "^0.34.3"} +pydantic = "^2.11.7" +python-dotenv = "^1.1.0" +python-multipart = "^0.0.20" +aiohttp = "^3.12.13" +requests = "^2.32.4" +numpy = "^2.3.0" +pandas = "^2.3.0" +tokenizers = "^0.21.1" +transformers = "^4.52.4" +huggingface-hub = "^0.33.0" +sentence-transformers = "^4.1.0" +chromadb = "^1.0.12" +hnswlib = "^0.8.0" +duckdb = "^1.3.0" +anthropic = "^0.54.0" +typing-extensions = "^4.14.0" +sqlalchemy = "^2.0.41" +torch = {version = "^2.7.1+cpu", source = "pytorch-cpu"} +torchvision = {version = "^0.22.1+cpu", source = "pytorch-cpu"} +torchaudio = {version = "^2.7.1+cpu", source = "pytorch-cpu"} +starlette = ">=0.40.0,<0.46.0" + +[[tool.poetry.source]] +name = "pytorch-cpu" +url = "https://download.pytorch.org/whl/cpu" +priority = "supplemental" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/vector/setup.sh b/vector/setup.sh new file mode 100755 index 0000000..502da6d --- /dev/null +++ b/vector/setup.sh @@ -0,0 +1,376 @@ +#!/bin/bash +# setup.sh - Poetry 기반 Vector DB API 의존성 설치 스크립트 (홈 디렉토리 사용) + +set -e + +# 색상 설정 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +NC='\033[0m' + +# 옵션 기본값 +SKIP_POETRY_INSTALL=false +SKIP_PYTHON311_CHECK=false +FORCE_REINSTALL=false +RUN_AFTER_INSTALL=false + +# 함수 정의 +log_info() { echo -e "${CYAN}ℹ️ $1${NC}"; } +log_success() { echo -e "${GREEN}✅ $1${NC}"; } +log_warning() { echo -e "${YELLOW}⚠️ $1${NC}"; } +log_error() { echo -e "${RED}❌ $1${NC}"; } +log_step() { echo -e "\n${BLUE}🔧 $1${NC}"; } + +# 사용법 표시 +show_usage() { + echo "사용법: $0 [옵션]" + echo "" + echo "옵션:" + echo " --skip-poetry-install Poetry 설치 건너뛰기" + echo " --skip-python311-check Python 3.11 확인 건너뛰기" + echo " --force-reinstall 기존 환경 제거 후 재설치" + echo " --run-after-install 설치 후 앱 자동 실행" + echo " --help 이 도움말 표시" +} + +# 파라미터 파싱 +while [[ $# -gt 0 ]]; do + case $1 in + --skip-poetry-install) + SKIP_POETRY_INSTALL=true + shift + ;; + --skip-python311-check) + SKIP_PYTHON311_CHECK=true + shift + ;; + --force-reinstall) + FORCE_REINSTALL=true + shift + ;; + --run-after-install) + RUN_AFTER_INSTALL=true + shift + ;; + --help) + show_usage + exit 0 + ;; + *) + log_error "알 수 없는 옵션: $1" + show_usage + exit 1 + ;; + esac +done + +echo "========================================================" +echo "🚀 Poetry 기반 Vector DB API 의존성 설치 (홈 디렉토리)" +echo "========================================================" +echo "설치 시작: $(date)" +echo "" + +INSTALL_START=$(date +%s) + +# ============================================================================= +# 1단계: Poetry 설치 확인 +# ============================================================================= +log_step "1단계: Poetry 설치 확인" + +if [ "$SKIP_POETRY_INSTALL" = true ]; then + log_info "Poetry 설치 확인을 건너뜁니다." +elif command -v poetry &> /dev/null; then + POETRY_VERSION=$(poetry --version) + log_success "Poetry 이미 설치됨: $POETRY_VERSION" +else + if [ -f /.dockerenv ]; then + log_error "Docker 환경에서 Poetry를 찾을 수 없습니다." + log_error "--skip-poetry-install 옵션을 제거하세요." + exit 1 + fi + + log_info "Poetry 설치 중..." + sudo apt update + sudo apt install -y python3-poetry + + # PATH 확인 + export PATH="$HOME/.local/bin:$PATH" + + if command -v poetry &> /dev/null; then + log_success "Poetry 설치 완료: $(poetry --version)" + else + log_error "Poetry 설치 실패" + exit 1 + fi +fi + +# ============================================================================= +# 2단계: Python 3.11 확인 +# ============================================================================= +log_step "2단계: Python 3.11 환경 확인" + +if [ "$SKIP_PYTHON311_CHECK" = false ]; then + if command -v python3.11 &> /dev/null; then + PYTHON311_VERSION=$(python3.11 --version) + log_success "Python 3.11 발견: $PYTHON311_VERSION" + else + log_warning "Python 3.11이 설치되어 있지 않습니다." + log_info "Python 3.11 설치 중..." + sudo apt update + sudo apt install -y python3.11 python3.11-venv python3.11-dev + + if command -v python3.11 &> /dev/null; then + log_success "Python 3.11 설치 완료: $(python3.11 --version)" + else + log_error "Python 3.11 설치 실패" + exit 1 + fi + fi +fi + +# ============================================================================= +# 3단계: Poetry 프로젝트 초기화 +# ============================================================================= +log_step "3단계: Poetry 프로젝트 초기화" + +# 기존 환경 제거 (강제 재설치 옵션) +if [ "$FORCE_REINSTALL" = true ]; then + log_info "기존 Poetry 환경 제거 중..." + poetry env remove --all 2>/dev/null || true + rm -f poetry.lock pyproject.toml 2>/dev/null || true +fi + +# pyproject.toml이 없는 경우 초기화 +if [ ! -f "pyproject.toml" ]; then + log_info "Poetry 프로젝트 초기화 중..." + + # 비대화형 초기화 + cat > pyproject.toml << 'EOF' +[tool.poetry] +name = "vector-api" +version = "1.0.0" +description = "Vector DB API with AI/ML capabilities" +authors = ["Developer "] +packages = [{include = "app"}] + +[tool.poetry.dependencies] +python = "^3.11" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" +EOF + + log_success "pyproject.toml 생성 완료" +else + log_success "기존 pyproject.toml 사용" +fi + +# ============================================================================= +# 4단계: Poetry 가상환경 설정 (홈 디렉토리 사용) +# ============================================================================= +log_step "4단계: Poetry 가상환경 설정 (홈 디렉토리 사용)" + +log_info "Poetry 가상환경을 홈 디렉토리로 설정 중..." + +# 홈 디렉토리에 Poetry 관련 디렉토리 생성 +mkdir -p ~/.cache/pypoetry/venvs ~/.cache/pypoetry/cache + +# Poetry 설정 - 가상환경을 홈 디렉토리로 이동 +poetry config virtualenvs.in-project false +poetry config virtualenvs.create true +poetry config virtualenvs.path ~/.cache/pypoetry/venvs +poetry config cache-dir ~/.cache/pypoetry/cache + +# 설정 확인 +log_info "Poetry 설정 확인:" +poetry config --list | grep -E "(virtualenvs|cache)" + +# 권한 확인 +log_info "디렉토리 권한 확인:" +ls -la ~/.cache/pypoetry/ 2>/dev/null || log_info "디렉토리가 아직 생성되지 않음" + +log_success "Poetry 가상환경 설정 완료" + +# ============================================================================= +# 5단계: Python 3.11 환경 설정 +# ============================================================================= +log_step "5단계: Python 3.11 환경 설정" + +log_info "Poetry가 Python 3.11을 사용하도록 설정 중..." +poetry env use python3.11 + +# 환경 정보 확인 +POETRY_ENV_INFO=$(poetry config virtualenvs.path) +log_success "Poetry 가상환경 기본 경로: $POETRY_ENV_INFO" + +# ============================================================================= +# 6단계: 소스 우선순위 사전 설정 (중요!) +# ============================================================================= +log_step "6단계: 소스 우선순위 사전 설정" + +log_info "PyPI를 기본 소스로 확인 중..." +# PyPI는 기본적으로 존재하므로 확인만 함 +if poetry source show | grep -q "pypi"; then + log_success "PyPI 소스 확인됨" +else + log_warning "PyPI 소스가 없습니다 (비정상)" +fi + +log_success "소스 우선순위 사전 설정 완료" + +# ============================================================================= +# 7단계: 기본 웹 프레임워크 설치 +# ============================================================================= +log_step "7단계: 기본 웹 프레임워크 설치" + +log_info "FastAPI 및 관련 패키지 설치 중..." +poetry add "fastapi==0.115.9" +poetry add uvicorn[standard] pydantic python-dotenv python-multipart + +log_success "웹 프레임워크 설치 완료" + +# ============================================================================= +# 8단계: HTTP 클라이언트 설치 +# ============================================================================= +log_step "8단계: HTTP 클라이언트 설치" + +log_info "HTTP 클라이언트 설치 중..." +poetry add aiohttp requests + +log_success "HTTP 클라이언트 설치 완료" + +# ============================================================================= +# 9단계: 데이터 처리 라이브러리 설치 +# ============================================================================= +log_step "9단계: 데이터 처리 라이브러리 설치" + +log_info "NumPy, Pandas 설치 중..." +poetry add numpy pandas + +log_success "데이터 처리 라이브러리 설치 완료" + +# ============================================================================= +# 10단계: AI/ML 기초 라이브러리 설치 +# ============================================================================= +log_step "10단계: AI/ML 기초 라이브러리 설치" + +log_info "Tokenizers, Transformers 설치 중..." +poetry add tokenizers transformers huggingface-hub + +log_success "AI/ML 기초 라이브러리 설치 완료" + +# ============================================================================= +# 11단계: Sentence Transformers 설치 +# ============================================================================= +log_step "11단계: Sentence Transformers 설치" + +log_info "Sentence Transformers 설치 중..." +poetry add sentence-transformers + +log_success "Sentence Transformers 설치 완료" + +# ============================================================================= +# 12단계: Vector DB 라이브러리 설치 +# ============================================================================= +log_step "12단계: Vector DB 라이브러리 설치" + +log_info "ChromaDB, HNSW, DuckDB 설치 중..." +poetry add chromadb hnswlib duckdb + +log_success "Vector DB 라이브러리 설치 완료" + +# ============================================================================= +# 13단계: Claude API 라이브러리 설치 +# ============================================================================= +log_step "13단계: Claude API 라이브러리 설치" + +log_info "Anthropic Claude API 라이브러리 설치 중..." +poetry add anthropic + +log_success "Claude API 라이브러리 설치 완료" + +# ============================================================================= +# 14단계: 기타 필수 라이브러리 설치 +# ============================================================================= +log_step "14단계: 기타 필수 라이브러리 설치" + +log_info "기타 필수 라이브러리 설치 중..." +poetry add typing-extensions sqlalchemy + +log_success "기타 필수 라이브러리 설치 완료" + +# ============================================================================= +# 15단계: PyTorch CPU 버전 설치 +# ============================================================================= +log_step "15단계: PyTorch CPU 버전 설치" + +log_info "PyTorch CPU 버전 소스 추가 중..." +poetry source add pytorch-cpu https://download.pytorch.org/whl/cpu --priority=supplemental + +log_info "PyTorch CPU 버전 설치 중..." +poetry add torch --source pytorch-cpu +poetry add torchvision --source pytorch-cpu +poetry add torchaudio --source pytorch-cpu + +log_success "PyTorch CPU 버전 설치 완료" + +# ============================================================================= +# 16단계: Starlette 버전 호환성 확인 +# ============================================================================= +log_step "16단계: Starlette 버전 호환성 확인" + +log_info "Starlette 호환 버전 설치 중..." +poetry add "starlette>=0.40.0,<0.46.0" + +log_success "Starlette 호환성 확인 완료" + +# 종료 시간 계산 +INSTALL_END=$(date +%s) +INSTALL_TIME=$((INSTALL_END - INSTALL_START)) +MINUTES=$((INSTALL_TIME / 60)) +SECONDS=$((INSTALL_TIME % 60)) + +echo "" +echo "🎉 Poetry 기반 Vector DB API 설치 완료!" +echo "========================================================" +echo "⏱️ 총 설치 시간: ${MINUTES}분 ${SECONDS}초" +echo "📦 설치된 패키지 수: $(poetry show | wc -l)" +echo "🐍 Python 환경: $(poetry run python --version)" +echo "📁 환경 경로: $(poetry env info --path)" +echo "" + +echo "🚀 다음 단계:" +echo " 1. 가상환경 활성화:" +echo " poetry shell" +echo "" +echo " 2. 애플리케이션 실행:" +echo " poetry run python app/main.py" +echo "" +echo " 3. 개발 서버 실행:" +echo " poetry run uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload" +echo "" + +echo "💡 유용한 명령어:" +echo " - poetry show # 설치된 패키지 확인" +echo " - poetry show --tree # 의존성 트리 확인" +echo " - poetry add [패키지명] # 새 패키지 추가" +echo " - poetry update # 패키지 업데이트" +echo " - poetry env info # 환경 정보 확인" +echo "" + +# 자동 실행 옵션 +if [ "$RUN_AFTER_INSTALL" = true ]; then + if [ -f "app/main.py" ]; then + log_info "앱 자동 실행 중..." + poetry run python app/main.py + else + log_warning "app/main.py 파일이 없어 자동 실행을 건너뜁니다." + fi +fi + +log_success "설치 스크립트 완료! 🎊" +