diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000000000000000000000000000000000..6593ed7c26f44e10d5236712ec3a9c977b524c37 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,23 @@ +.git +.github +.venv +.vscode +.claude +.pytest_cache +__pycache__ +**/__pycache__ +*.py[oc] + +.env +.env.* +**/.env +**/.env.* +scam-detector-ocr-*.json +credentials.json + +tests +scripts +backend/models +data/chroma + +AGENTS.md diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000000000000000000000000000000000..73ec6726f463d9e24e33fb003100686298d93fce --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +README.md merge=ours \ No newline at end of file diff --git a/.github/workflows/check_size.yaml b/.github/workflows/check_size.yaml new file mode 100644 index 0000000000000000000000000000000000000000..b1875a2c6b7ba55f74e933ab738faf418caf8b3c --- /dev/null +++ b/.github/workflows/check_size.yaml @@ -0,0 +1,16 @@ +name: Check file size +on: # or directly `on: [push]` to run the action on every push on any branch + pull_request: + branches: [space-deploy] + + # to run this workflow manually from the Actions tab + workflow_dispatch: + +jobs: + sync-to-hub: + runs-on: ubuntu-latest + steps: + - name: Check large files + uses: ActionsDesk/lfs-warning@v2.0 + with: + filesizelimit: 10485760 # this is 10MB so we can sync to HF Spaces diff --git a/.github/workflows/sync_to_hf.yaml b/.github/workflows/sync_to_hf.yaml new file mode 100644 index 0000000000000000000000000000000000000000..e6bdc92e0ee3740adc4ac01dca9d0aac9fd214b9 --- /dev/null +++ b/.github/workflows/sync_to_hf.yaml @@ -0,0 +1,28 @@ +name: Sync to Hugging Face hub +on: + push: + branches: [space-deploy] + + # to run this workflow manually from the Actions tab + workflow_dispatch: + +jobs: + sync-to-hub: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 1 + lfs: true + # 只推送不含歷史的快照,避免 tests/ 內的二進位檔(及其歷史)被 HF 拒絕 + - name: Push to hub + env: + HF_TOKEN: ${{ secrets.HF_TOKEN }} + run: | + git config user.name "github-actions" + git config user.email "github-actions@users.noreply.github.com" + git checkout --orphan hf-snapshot + git rm -r -q --cached --ignore-unmatch tests + git commit -q -m "deploy: ${GITHUB_SHA::7}" + git remote add hf https://kko12:$HF_TOKEN@huggingface.co/spaces/kko12/spam-detector + git push -f hf hf-snapshot:main diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..ea4db864531f7ca8917536fe847d9f2ff8164314 --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +# Python-generated files +__pycache__/ +*.py[oc] +build/ +dist/ +wheels/ +*.egg-info + +# Virtual environments +.venv +.vscode + +backend/models +.env +.env.* +backend/.env +backend/.env.* +!.env.example + +data/ +db/ + +dev.* + +scam-detector-ocr-*.json +credentials.json \ No newline at end of file diff --git a/.python-version b/.python-version new file mode 100644 index 0000000000000000000000000000000000000000..6324d401a069f4020efcf0ff07442724b52f47c2 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.14 diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000000000000000000000000000000000..aee9b9050228d0873c0aa4bf49118f86b0264eb1 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,77 @@ +## Code style guidelines + +### 1. 實作前先產生計畫 + +在進行任何非瑣碎的程式碼變更前(新增功能、重構、涉及多個檔案的 bug 修復), +先以下列格式產出計畫,再開始實作: + +1. **目標**:一句話描述要達成什麼 +2. **影響範圍**:會修改/新增哪些檔案或模組 +3. **拆分步驟**:條列式列出實作順序(每一步應是可獨立驗證的最小單位) +4. **風險/取捨**:是否有需要注意的邊界情況、相容性、效能影響 +5. **驗證方式**:完成後如何確認正確(跑哪個測試、哪個指令) +計畫確認後才開始寫程式碼。若使用者未回覆確認,預設視為同意; +但遇到以下情況,必須停下來等待明確確認才可繼續: + +- 會刪除或覆寫現有邏輯 +- 影響資料庫 schema 或外部 API 合約 +- 實作方式有多種合理選項,需要使用者取捨判斷 +單一函式修正、拼字/格式修正等瑣碎變更可略過此流程,直接實作。 + +### 2. 巢狀與職責集中度 + +- 函式巢狀深度盡量不超過 2 層;超過時優先用 early return/guard clause 打平, + 而非直接視為「必須拆函式」的訊號 +- 函式長度沒有硬性上限;只有當同一函式混雜多種職責 + (例如同時做 I/O 呼叫、資料轉換、商業邏輯判斷)時才需要拆分 +- 一個檔案/模組只負責單一領域(例如「搜尋」「總結」「快取」分開放) + +### 3. 函式拆分準則(避免過度拆分) + +只有符合以下任一條件,才拆成獨立函式: + +1. 邏輯會被複用 ≥ 2 次 +2. 該段邏輯是獨立語意單元,可脫離上下文理解,適合單獨寫測試 +3. 屬於不同關注點(I/O /純運算/格式化/驗證) +不要拆分的情況: + +- 只執行一次、線性依序執行的步驟,硬切成多個小函式 +- 拆分後函式間需要傳遞大量參數或共享可變狀態(代表本質上是同一件事) +- 函式名稱與內容幾乎一比一翻譯,沒有抽象價值(例如 `add_one(x): return x + 1`) +拆分的目的是讓呼叫端讀起來像文件,而不是讓每個函式變短。 + +### 4. 命名與抽象層級 + +- 禁止使用空泛命名:`data`/`process`/`manager`/`util`, + 需具體對應業務語意(例如 `fetch_price_from_serp_api`,而非 `process_data`)。 + `handle` 僅允許用於最外層協調邏輯(見第 5 節動詞詞彙表),其餘情況視為空泛命名 +- 不要引入未被使用的抽象層(interface/abstract class/factory/strategy), + 除非確實存在 ≥ 2 種具體實作 +- 避免多層 wrapper/callback 包裝簡單邏輯 + +### 5. 命名長度控制 + +- 函式名應盡量控制在 2–4 個單字(動詞 + 名詞,必要時加修飾詞) +- 不要在函式名重複「模組名/類別名/檔案名」已表達的語意 + (例如在 `groq_summarizer.py` 內,函式命名為 `summarize()` 而非 `summarize_with_groq()`) +- 使用固定動詞詞彙表:`get`/`fetch`(讀取)、`list`(多筆)、`build`/`make`(組裝)、 + `validate`(驗證)、`normalize`(格式化)、`handle`(僅限最外層協調邏輯) +- 過多細節(如條件、延遲、重試策略)用參數表達,不要塞進函式名稱 +- 只允許業界通用縮寫(`api`, `url`, `id`, `config`),業務核心詞彙不縮寫 + +### 6. 換行與行長度 + +- 單行程式碼上限為 100–120 字元(依語言慣例調整,Python 可用 100) +- 函式呼叫參數、import、字典/物件字面量等,只有在超過行長度上限時才拆成多行, + 未超過時保持單行 +- 不要僅因為「參數較多」或「表達式較長」就主動換行,以實際字元數為唯一判斷依據 +- 若專案已有 formatter 設定(如 black/prettier 的 line-length), + 以該設定為準,不要用更保守的預設值 + +### 7. 完成後自我審查 + +完成程式碼後,請自問: + +1. 是否有函式混雜多種職責?→ 拆分(對照第 2、3 節) +2. 是否有只被呼叫一次、拆分後反而要跳來跳去才看得懂的函式?→ 合併回去 +3. 是否有命名空泛、語意抽象、無法一眼看出用途的變數/函式?→ 重新命名(對照第 4、5 節) \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..9abffca4518f2d453d694d4d278397b3be8b0a8c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,27 @@ +# 建立docker image(鏡像) + +# 選擇base image(需符合 pyproject.toml 的 requires-python) +FROM python:3.14-slim + +# 安裝 uv +COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv + +# HF Space 以 uid 1000 執行,工作目錄需屬於該使用者才能寫入模型與資料 +RUN useradd -m -u 1000 user && mkdir -p /home/user/app && chown user:user /home/user/app +USER user +WORKDIR /home/user/app + +ENV UV_COMPILE_BYTECODE=1 \ + UV_LINK_MODE=copy \ + UV_PYTHON_DOWNLOADS=never \ + PATH="/home/user/app/.venv/bin:$PATH" + +# 安裝套件依賴(先只複製依賴檔,讓此層可被快取) +COPY --chown=user pyproject.toml uv.lock ./ +RUN uv sync --frozen --no-dev --no-install-project + +# 搬入原始碼 +COPY --chown=user . . + +# 執行cmd +CMD ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "7860"] diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..16d532811909c5cb80c806724ec43d9c33380442 --- /dev/null +++ b/README.md @@ -0,0 +1,8 @@ +--- +title: Scam Detection +emoji: 🔍 +colorFrom: red +colorTo: blue +sdk: docker +sdk_version: "2.0" +--- diff --git a/backend/__init__.py b/backend/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..d9bae88f4b24183a119a42c61be1e74646585d84 --- /dev/null +++ b/backend/__init__.py @@ -0,0 +1 @@ +"""Scam Detection Backend Module""" diff --git a/backend/api/__init__.py b/backend/api/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/backend/api/exception_handler.py b/backend/api/exception_handler.py new file mode 100644 index 0000000000000000000000000000000000000000..37e7b998cf60e6d6cf22a465f00c178db073f25e --- /dev/null +++ b/backend/api/exception_handler.py @@ -0,0 +1,42 @@ +import logging +from typing import cast + +from fastapi import FastAPI, HTTPException, Request +from fastapi.encoders import jsonable_encoder +from fastapi.exceptions import RequestValidationError +from fastapi.responses import JSONResponse +from starlette.types import ExceptionHandler + +from backend.api.response import ApiErrorResponse + +logger = logging.getLogger(__name__) + +def make_error_response(status_code: int, message: object | None = None) -> JSONResponse: + """建立統一的錯誤回傳格式""" + response_content = ApiErrorResponse( + success=False, + version="1.0", + error_code=status_code, + error_message=message + ) + return JSONResponse(status_code=status_code, content=jsonable_encoder(response_content)) + +async def handle_global_exception(req: Request, exc: Exception) -> JSONResponse: + """處理全域錯誤""" + logger.error(f"Unhandled Exception: {str(exc)}", exc_info=True) + return make_error_response(status_code=500, message="Internal Server Error") + +async def handle_http_exception(req: Request, exc: HTTPException) -> JSONResponse: + """處理 HTTP 錯誤""" + logger.error(f"HTTP Exception: {str(exc.detail)}") + return make_error_response(status_code=exc.status_code, message=exc.detail) + +async def handle_validation_exception(req: Request, exc: RequestValidationError) -> JSONResponse: + """處理資料驗證錯誤""" + logger.error(f"Validation Error: {exc.errors()}") + return make_error_response(status_code=422, message=exc.errors()) + +def register_exception_handlers(app: FastAPI) -> None: + app.add_exception_handler(Exception, handle_global_exception) + app.add_exception_handler(HTTPException, cast(ExceptionHandler, handle_http_exception)) + app.add_exception_handler(RequestValidationError, cast(ExceptionHandler, handle_validation_exception)) diff --git a/backend/api/response.py b/backend/api/response.py new file mode 100644 index 0000000000000000000000000000000000000000..d08e155e5583659d3c6f419aaf77baf0bea3bb2f --- /dev/null +++ b/backend/api/response.py @@ -0,0 +1,27 @@ +"""建立後端統一回傳的格式""" +from typing import Generic, TypeVar + +from pydantic import BaseModel, ConfigDict +from pydantic.alias_generators import to_camel + +ResponseData = TypeVar("ResponseData") + +class ApiResponse(BaseModel, Generic[ResponseData]): + """統一回傳格式""" + success: bool = True + version: str = "1.0" + data: ResponseData | None = None + +class ApiErrorResponse(BaseModel): + model_config = ConfigDict( + alias_generator=to_camel, + populate_by_name=True, + serialize_by_alias=True, + ) + + """統一回傳錯誤格式""" + success: bool = False + version: str = "1.0" + error_code: int + error_message: object | None = None + \ No newline at end of file diff --git a/backend/api/routers/__init__.py b/backend/api/routers/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/backend/api/routers/image_price_validation.py b/backend/api/routers/image_price_validation.py new file mode 100644 index 0000000000000000000000000000000000000000..4d5e6cb3eee0340c757c026822231ffd295df66c --- /dev/null +++ b/backend/api/routers/image_price_validation.py @@ -0,0 +1,130 @@ +"""商品圖片上傳、價格風險分析與 Google OCR 驗證 API。 + +正式價格端點只負責 HTTP 輸入驗證、呼叫 image_price_service,並將完整 Service +結果封裝成 ImagePriceResponse;OCR、商品辨識、價格統計與風險決策不在本模組。 +""" + +from typing import Annotated + +from fastapi import APIRouter, File, HTTPException, Query, UploadFile +from starlette.concurrency import run_in_threadpool + +from backend.api.response import ApiResponse +from backend.api.schemas.image import (ImagePriceDebugInfo, ImagePriceResponse, + ImageUploadResponse, + MarketPriceEstimateResponse) +from backend.core.config import settings +from backend.services.image_price_service.image_price_analyzer import \ + image_price_analyzer + +router = APIRouter(prefix="/v1", tags=["image-inference"]) + + +@router.post( + "/image/upload", + response_model=ApiResponse[ImageUploadResponse], + summary="圖片上傳測試", + description="用於測試圖片上傳是否成功。", +) +async def upload_image(file: UploadFile = File(...)) -> ApiResponse[ImageUploadResponse]: + """驗證圖片 MIME type、非空內容與大小,回傳上傳資訊但不執行分析。""" + content_type = file.content_type or "" + if content_type not in settings.ALLOWED_IMAGE_TYPES: + raise HTTPException( + status_code=400, + detail="僅支援 JPG、PNG、WEBP 圖片格式", + ) + + data = await file.read() + if not data: + raise HTTPException(status_code=400, detail="上傳檔案不可為空") + + if len(data) > settings.MAX_IMAGE_BYTES: + raise HTTPException(status_code=400, detail="圖片大小不可超過 20MB") + + data = ImageUploadResponse( + filename=file.filename or "unknown", + content_type=content_type, + size_bytes=len(data), + message="圖片已成功上傳,可進入下一步辨識流程", + ) + + return ApiResponse(data=data) + + +@router.post( + "/price/analyze", + response_model=ApiResponse[ImagePriceResponse], + summary="價格驗證風險分析", + description="使用 OCR 分析圖片商品價格。加上 `?debug=true` 可取得 OCR 與市場價格來源的除錯資訊。", +) +async def analyze_image_price( + file: UploadFile = File(...), + include_debug: Annotated[bool | None, Query(alias="debug")] = None, +) -> ApiResponse[ImagePriceResponse]: + """驗證商品圖片 HTTP 輸入並封裝 Service 分析結果。 + + Router 僅驗證 MIME type、非空內容與大小,呼叫 image_price_detector 後以 + DTO 欄位建立 ImagePriceResponse,不計算或覆寫任何風險結果。 + """ + try: + content_type = file.content_type or "" + if content_type not in settings.ALLOWED_IMAGE_TYPES: + raise HTTPException(status_code=400, detail="僅支援 JPG、PNG、WEBP 圖片格式") + + data = await file.read() + if not data: + raise HTTPException(status_code=400, detail="上傳檔案不可為空") + if len(data) > settings.MAX_IMAGE_BYTES: + raise HTTPException(status_code=400, detail="圖片大小不可超過 20MB") + + result = await run_in_threadpool( + image_price_analyzer.image_price_detector, + data=data, + filename=file.filename or "unknown", + content_type=content_type, + ) + warnings = list(result.extraction_warnings) + if not result.success and result.message and result.message not in warnings: + warnings.append(result.message) + + debug_info = None + if include_debug: + debug_info = ImagePriceDebugInfo( + error_code=result.error_code, + search_tools=result.search_tools, + market_price_source=result.market_price_source, + market_price_estimates=tuple( + MarketPriceEstimateResponse( + **estimate.model_dump(exclude={"candidates", "search_tools"}) + ) + for estimate in result.market_price_estimates + ), + condition_source_text=result.condition_source_text, + condition_extraction_confidence=result.condition_extraction_confidence, + price_source_text=result.price_source_text, + extraction_confidence=result.extraction_confidence if result.success else None, + decision_layer=result.decision_layer, + warnings=warnings, + ) + + data=ImagePriceResponse( + product_name=result.product_name if result.success else None, + listed_price=result.listed_price if result.success else None, + market_price=(result.market_price if result.success and result.market_price > 0 else None), + seller_name=result.seller_name if result.success else None, + risk_label=result.risk_label, + condition=result.condition, + risk_score=result.score, + result=result.reason, + debug=debug_info, + ) + + return ApiResponse(data=data) + + except HTTPException: + raise + except Exception as e: + if settings.DEBUG: + raise HTTPException(status_code=500, detail=str(e)) + raise HTTPException(status_code=500, detail="Internal Error") diff --git a/backend/api/routers/phone_detection.py b/backend/api/routers/phone_detection.py new file mode 100644 index 0000000000000000000000000000000000000000..97da22ded8514882f5f038c82bbdf24b155e0c38 --- /dev/null +++ b/backend/api/routers/phone_detection.py @@ -0,0 +1,95 @@ +from fastapi import APIRouter, HTTPException + +from backend.api.response import ApiResponse +from backend.api.schemas.phone import (PhoneQueryRequest, PhoneQueryResponse, + PhoneReportError, PhoneReportRequest, + PhoneReportResponse) +from backend.services.phone_service import phone_service + +# 服務層為同步 MySQL I/O,路由用 def 讓 FastAPI 放進 threadpool 執行,避免阻塞 event loop +router = APIRouter(prefix="/v1", tags=["phone"]) + + +@router.post( + "/phones/search", + response_model=ApiResponse[PhoneQueryResponse], + summary="電話號碼查詢", + description="查詢電話號碼是否已存在於黑白名單。若結果為 unknown,可提示使用者進行回報。", + openapi_extra={ + "requestBody": { + "content": { + "application/json": { + "example": { + "phone_number": "0422255141" + } + } + } + } + }, +) +def query_phone(req: PhoneQueryRequest) -> ApiResponse[PhoneQueryResponse]: + try: + result = phone_service.query_phone(req.phone_number) + data = PhoneQueryResponse(**result) + return ApiResponse(data=data) + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@router.post( + "/phones/report", + response_model=ApiResponse[PhoneReportResponse], + summary="可疑號碼回報", + description=( + "當查詢結果為 unknown 時,可回報該號碼為可疑名單。" + " 若選擇「其他」,請一併填寫 other_type 欄位。" + ), + openapi_extra={ + "requestBody": { + "content": { + "application/json": { + "examples": { + "standard": { + "summary": "回報常見類別", + "value": { + "phoneNumber": "0987654321", + "phoneType": "假投資", + "reporterPhone": "0912345678", + "transferType": 1, + "transferContent": "0912345678" + } + }, + "other": { + "summary": "回報其他類別", + "value": { + "phoneNumber": "0987654321", + "phoneType": "其他", + "otherType": "疑似詐騙廣告", + "reporterPhone": "0912345678", + "transferType": 3, + "transferContent": "https://example.com/fake" + } + } + } + } + } + } + }, +) +def report_phone(req: PhoneReportRequest) -> ApiResponse[PhoneReportResponse]: + try: + result = phone_service.report_suspicious( + req.phone_number, + req.phone_type, + req.other_type, + None, + req.reporter_phone, + req.transfer_type, + req.transfer_content, + ) + data = PhoneReportResponse(**result) + return ApiResponse(data=data) + except PhoneReportError as e: + raise HTTPException(status_code=409, detail=str(e)) + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) diff --git a/backend/api/routers/text_inference.py b/backend/api/routers/text_inference.py new file mode 100644 index 0000000000000000000000000000000000000000..42ad5d9d984090ef275e715079e5717b72dd22d5 --- /dev/null +++ b/backend/api/routers/text_inference.py @@ -0,0 +1,46 @@ +from fastapi import APIRouter, HTTPException + +from backend.api.response import ApiResponse +from backend.api.schemas.text import TextRequest, TextResponse +from backend.core.config import settings +from backend.services.text_service.text_analyzer import text_analyzer +from backend.utils.text_cleaner import normalize_text + +router = APIRouter(prefix="/v1", tags=["text-inference"]) + + +@router.post("/text/analyze", response_model=ApiResponse[TextResponse], summary="文字詐騙風險分析", description="使用規則、多模型做混合判斷") +async def analyze_text(req: TextRequest) -> ApiResponse[TextResponse]: + try: + text = normalize_text(req.text) + if not text: + raise HTTPException(status_code=400, detail="text 不可為空") + + result = await text_analyzer.hybrid_detector(text) + data = TextResponse(**result) + return ApiResponse(data=data) + except HTTPException: + raise + except Exception as e: + if settings.DEBUG: + raise HTTPException(status_code=500, detail=str(e)) + raise HTTPException(status_code=500, detail="Internal Error") + + + +@router.post("/text/analyze/base", response_model=ApiResponse[TextResponse], summary="純模型文字風險分析", description="僅使用文字分類模型") +def model_analyze_text(req: TextRequest) -> ApiResponse[TextResponse]: + try: + text = normalize_text(req.text) + if not text: + raise HTTPException(status_code=400, detail="text 不可為空") + + result = text_analyzer.model_detector(text) + data = TextResponse(**result) + return ApiResponse(data=data) + except HTTPException: + raise + except Exception as e: + if settings.DEBUG: + raise HTTPException(status_code=500, detail=str(e)) + raise HTTPException(status_code=500, detail="Internal Error") diff --git a/backend/api/routers/url_detection.py b/backend/api/routers/url_detection.py new file mode 100644 index 0000000000000000000000000000000000000000..95811ce7ad3be594915024990f4aedd1e1ed6938 --- /dev/null +++ b/backend/api/routers/url_detection.py @@ -0,0 +1,22 @@ +from fastapi import APIRouter, HTTPException + +from backend.api.response import ApiResponse +from backend.api.schemas.url import UrlRequest, UrlResponse +from backend.core.config import settings +from backend.services.url_service.url_analyzer import url_detector + +router = APIRouter(prefix="/v1", tags=["url-detector"]) + +# url網址分析 +@router.post("/url/analyze", response_model=ApiResponse[UrlResponse], summary="執行url分析") +def analyze_url(req: UrlRequest) -> ApiResponse[UrlResponse]: + try: + url = str(req.url) + result = url_detector.analyze(url) + data = UrlResponse(**result) + return ApiResponse(data=data) + except Exception as e: + if settings.DEBUG: + raise HTTPException(status_code=500, detail=str(e)) + raise HTTPException(status_code=500, detail="Internal Error") + diff --git a/backend/api/schemas/__init__.py b/backend/api/schemas/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/backend/api/schemas/image.py b/backend/api/schemas/image.py new file mode 100644 index 0000000000000000000000000000000000000000..9273d6f0394718270a45cc10f9c44bd97b79aa62 --- /dev/null +++ b/backend/api/schemas/image.py @@ -0,0 +1,71 @@ +from typing import Literal + +from pydantic import BaseModel, ConfigDict, Field +from pydantic.alias_generators import to_camel + +from backend.services.dto.price_analysis import ( + DecisionLayer, + MarketPriceReferenceMode, + MarketPriceSource, + MarketPriceStatus, + SearchTool, +) +from backend.services.image_price_service.domain.models import MarketplaceCondition + + +class ImageUploadResponse(BaseModel): + filename: str = Field(..., description="上傳檔名") + content_type: str = Field(..., description="檔案 MIME 類型") + size_bytes: int = Field(..., ge=1, description="檔案大小(byte)") + message: str = Field(..., description="處理結果訊息") + + +class MarketPriceEstimateResponse(BaseModel): + """對外回傳且不含市場候選明細的價格區間。""" + + model_config = ConfigDict(extra="forbid") + + status: MarketPriceStatus + condition: MarketplaceCondition + reference_mode: MarketPriceReferenceMode + median_price: int = Field(ge=0) + low_price: int = Field(ge=0) + high_price: int = Field(ge=0) + sample_count: int = Field(ge=0) + site_count: int = Field(ge=0) + source: MarketPriceSource + confidence: float = Field(ge=0, le=1) + + +class ImagePriceDebugInfo(BaseModel): + model_config = ConfigDict(extra="forbid") + + error_code: str | None = None + search_tools: list[SearchTool] = Field(default_factory=list) + market_price_source: MarketPriceSource = "not_evaluated" + market_price_estimates: tuple[MarketPriceEstimateResponse, ...] = () + condition_source_text: str = "" + condition_extraction_confidence: float = Field(default=0.0, ge=0, le=1) + price_source_text: str | None = None + extraction_confidence: float | None = Field(default=None, ge=0, le=1) + decision_layer: DecisionLayer + warnings: list[str] = Field(default_factory=list) + + +class ImagePriceResponse(BaseModel): + model_config = ConfigDict( + extra="forbid", + alias_generator=to_camel, + populate_by_name=True, + serialize_by_alias=True, + ) + + product_name: str | None = None + condition: MarketplaceCondition | None = None + listed_price: int | None = Field(default=None, ge=0) + market_price: int | None = Field(default=None, ge=0) + seller_name: str | None = None + risk_label: Literal["LOW", "MEDIUM", "HIGH", "UNKNOWN"] + risk_score: str | float | None = None + result: str | None = None + debug: ImagePriceDebugInfo | None = None diff --git a/backend/api/schemas/phone.py b/backend/api/schemas/phone.py new file mode 100644 index 0000000000000000000000000000000000000000..c3d15db54000102d2106ee168d8c54f85f2395f0 --- /dev/null +++ b/backend/api/schemas/phone.py @@ -0,0 +1,124 @@ +import re + +from pydantic import BaseModel, ConfigDict, Field, model_validator +from pydantic.alias_generators import to_camel + + +class PhoneQueryRequest(BaseModel): + model_config = ConfigDict( + alias_generator=to_camel, + populate_by_name=True, + serialize_by_alias=True, + ) + + phone_number: str = Field(..., min_length=1, max_length=20) + + @model_validator(mode="after") + def validate_phone_number_format(self): + if not re.fullmatch(r"[0-9]{8,15}", self.phone_number): + raise ValueError("電話號碼格式錯誤,請輸入 8~15 碼數字") + return self + + +VALID_PHONE_TYPES = [ + "約會交友", + "假投資", + "假信貸", + "假冒公務", + "假包裹釣魚", + "假求職", + "商業騷擾", + "假冒電商", + "其他", +] + + +class PhoneReportRequest(BaseModel): + model_config = ConfigDict( + alias_generator=to_camel, + populate_by_name=True, + serialize_by_alias=True, + ) + + phone_number: str = Field(..., min_length=1, max_length=20) + phone_type: str = Field(..., min_length=1, max_length=50) + other_type: str | None = Field(None, max_length=100) + reporter_phone: str | None = Field(None, max_length=20) + transfer_type: int | None = Field(default=0) + transfer_content: str | None = Field(None, max_length=500) + + @model_validator(mode="after") + def validate_phone_number_format(self): + if not re.fullmatch(r"[0-9]{8,15}", self.phone_number): + raise ValueError("電話號碼格式錯誤,請輸入 8~15 碼數字") + if self.reporter_phone and not re.fullmatch(r"[0-9]{8,15}", self.reporter_phone): + raise ValueError("回報者電話格式錯誤,請輸入 8~15 碼數字") + if self.transfer_type == 1 and self.transfer_content and not re.fullmatch(r"[0-9]{8,15}", self.transfer_content): + raise ValueError("轉介類型為電話號碼時,轉介內容必須為 8~15 碼數字") + return self + + @model_validator(mode="after") + def validate_phone_type(self): + if self.phone_type not in VALID_PHONE_TYPES: + raise ValueError( + "phone_type 必須為其中之一:約會交友、假投資、假信貸、假冒公務、假包裹釣魚、假求職、商業騷擾、假冒電商、其他" + ) + if self.phone_type == "其他" and not self.other_type: + raise ValueError("phone_type 為 '其他' 時,必須提供 other_type") + if self.transfer_type not in (0, 1, 2, 3, 4, 9): + raise ValueError("transfer_type 只能為 0、1、2、3、4、9,分別代表 無 / 電話 / LINE / URL / TG / 其他") + if self.transfer_type == 0 and self.transfer_content: + self.transfer_content = None + return self + + +class FamilyStaticItems(BaseModel): + """電話家族的直接關聯證據""" + model_config = ConfigDict( + alias_generator=to_camel, + populate_by_name=True, + serialize_by_alias=True, + ) + + related_phone: str = Field(..., description="關聯電話號碼") + weight: int = Field(..., ge=0, description="關聯強度分數") + reason: str = Field(..., description="關聯原因") + target_phone_type: str = Field(..., description="查詢號碼的詐騙類型") + + +class PhoneQueryResponse(BaseModel): + model_config = ConfigDict( + alias_generator=to_camel, + populate_by_name=True, + serialize_by_alias=True, + ) + + phone_number: str + status: str | None = None + phone_type: str | None = None + total_reports: int | None = None + first_reported_at: str | None = None + last_reported_at: str | None = None + owner_name: str | None = None + can_report: bool = False + # family analysis results (minimal but useful evidence) + family_static: list[FamilyStaticItems] | None = Field( + None, + description="直接關聯證據列表;如果沒有關聯,則為空陣列。", + ) + +class PhoneReportResponse(BaseModel): + model_config = ConfigDict( + alias_generator=to_camel, + populate_by_name=True, + serialize_by_alias=True, + ) + phone_number: str + status: str + total_reports: int + report_time: str + message: str + connection_reason: str | None = None + +class PhoneReportError(Exception): + pass \ No newline at end of file diff --git a/backend/api/schemas/text.py b/backend/api/schemas/text.py new file mode 100644 index 0000000000000000000000000000000000000000..9f9eca292f07200f11a599ea50bd853a0cbb6e1b --- /dev/null +++ b/backend/api/schemas/text.py @@ -0,0 +1,18 @@ +from pydantic import BaseModel, ConfigDict, Field +from pydantic.alias_generators import to_camel + + +class TextRequest(BaseModel): + text: str = Field(..., min_length=1, max_length=10000) + + +class TextResponse(BaseModel): + model_config = ConfigDict( + alias_generator=to_camel, + populate_by_name=True, + serialize_by_alias=True, + ) + label: str + score: str | float | None = None + reason: str | None = None + cls_model_confidence: float | None = None \ No newline at end of file diff --git a/backend/api/schemas/url.py b/backend/api/schemas/url.py new file mode 100644 index 0000000000000000000000000000000000000000..292acb3f830a72619867b0f8319b360eec1bff63 --- /dev/null +++ b/backend/api/schemas/url.py @@ -0,0 +1,10 @@ +from pydantic import BaseModel, HttpUrl + + +class UrlRequest(BaseModel): + url: HttpUrl + +class UrlResponse(BaseModel): + label: str + score: str | float | None = None + reason: str \ No newline at end of file diff --git a/backend/core/config.py b/backend/core/config.py new file mode 100644 index 0000000000000000000000000000000000000000..f8f19f9f0642ef37eda53e8a3d5026c6eac9f7f1 --- /dev/null +++ b/backend/core/config.py @@ -0,0 +1,121 @@ +import os +from pathlib import Path + +from pydantic import SecretStr, field_validator +from pydantic_settings import BaseSettings, SettingsConfigDict + +CORE_DIR = Path(__file__).resolve().parent +BACKEND_DIR = CORE_DIR.parent +ROOT_DIR = BACKEND_DIR.parent + + +class Settings(BaseSettings): + APP_NAME: str = "Scam detetcor" + DEBUG: bool = True + + HF_TEXT_REPO_ID: str = "scam-project/spam-detector-chinese" + HF_URL_REPO_ID: str = "scam-project/url-detector" + HF_PERSIST_REPO_ID: str = "scam-project/scam-rag-db" + HF_TOKEN: str = "" + + REGEX_WEIGHT: float = 0.55 + MODEL_WEIGHT: float = 0.45 + + HIGH_THRESHOLD: float = 0.5 + MEDIUM_THRESHOLD: float = 0.6 + UNKNOWN_THRESHOLD: float = 0.7 + + URL_HIGH_THRESHOLD: float = 0.5 + URL_MEDIUM_THRESHOLD: float = 0.3 + URL_MODEL_NAME: str = "url_scam_classifier.joblib" + URL_TW_RISK_BONUS: float = 0.3 + URL_EXPAND_SHORTENED: bool = True + URL_EXPAND_TIMEOUT_SECONDS: float = 3.0 + URL_EXPAND_MAX_REDIRECTS: int = 5 + + DEVICE: str = "cpu" + BASE_MODEL_PATH: str = str(BACKEND_DIR / "models") + + CHUNCK_SIZE: int = 500 + CHUNCK_OVERLAP: int = 50 + + RAG_MODEL_NAME: str = "openai/gpt-oss-20b" + RAG_ENABLED: bool = True + RAG_TOP_K: int = 3 + RAG_RECORD_LIMIT: int = 2100 + RAG_DATASET_PATH: str = str(ROOT_DIR / "data" / "raw" / "scam-dataset.json") + RAG_PERSIST_DIR: str = str(ROOT_DIR / "data" / "chroma") + EMBED_MODEL: str = "BAAI/bge-small-zh-v1.5" + + NORMALIZER_MODEL: str = "openai/gpt-oss-120b" + PRICE_MODEL: str = "openai/gpt-oss-120b" + REVIEW_MODEL: str = "qwen/qwen3.8-27b" + # 空字串代表不指定推理強度;gpt-oss 系列可用 low/medium/high + NORMALIZER_REASONING_EFFORT: str = "low" + PRICE_REASONING_EFFORT: str = "low" + PRICE_LLM_TIMEOUT_SECONDS: float = 10.0 + PRICE_LLM_MAX_RETRIES: int = 1 + TAVILY_SEARCH_API_KEY: SecretStr = SecretStr("") + SERP_API_KEY: SecretStr = SecretStr("") + SEARCH_COUNTRY: str = "taiwan" + + ALLOWED_IMAGE_TYPES : set[str] = {"image/jpeg", "image/png", "image/webp"} + MAX_IMAGE_BYTES : int = 20 * 1024 * 1024 # 20MB + MARKET_PRICE_DB_PATH: str = str(ROOT_DIR / "data" / "market_prices_tw.json") + ONLINE_PRICE_ENABLED: bool = True + ONLINE_PRICE_MAX_RESULTS: int = 20 + SEARCH_DOMAIN: list[str] = ["biggo.com.tw", "momo.com.tw", "pchome.com.tw", "shopee.tw", "ruten.com.tw", "tw.carousell.com"] + EXCLUDE_DOMAIN: list[str] = ["zh.wikipedia.org"] + SEARCH_TIMEOUT_SECONDS: float = 10.0 + GROQ_FALLBACK_RETRY_DELAY_SECONDS: float = 2.0 + GROQ_RATE_LIMIT_MAX_WAIT_SECONDS: float = 30.0 + PRODUCT_MATCH_FUZZY_MIN_SCORE: int = 86 + ENABLE_INTELLIGENT_LAYER: bool = True + CASE_LOG_PATH: str = str(ROOT_DIR / "data" / "cases" / "image_analysis_cases.jsonl") + CASE_MEMORY_ENABLED: bool = True + OCR_PROVIDER: str = "google_vision" + GCV_LANGUAGE_HINTS: str = "zh-TW,en" + GCP_OCR_SERVICE_ACCOUNT_JSON : str = "" + + DB_USERNAME: str = "" + DB_PASSWORD: str = "" + DB_HOST: str = "" + DB_PORT: int | None = None + DB_NAME: str = "" + DB_TIMEOUT: int = 10 + DB_SSL_CA: str = "" + + GROQ_API_KEY: SecretStr = SecretStr("") + model_config = SettingsConfigDict( + env_file=[os.path.join(BACKEND_DIR, ".env"), + os.path.join(ROOT_DIR, ".env")], + extra="ignore", + ) + + @field_validator("DEBUG", mode="before") + @classmethod + def parse_debug(cls, value: object) -> object: + if isinstance(value, str): + normalized = value.strip().lower() + if normalized in {"release", "prod", "production"}: + return False + if normalized in {"debug", "dev", "development"}: + return True + return value + + @field_validator("DB_PORT", mode="before") + @classmethod + def parse_db_port(cls, value: object) -> object: + if isinstance(value, str) and not value.strip(): + return None + return value + + @field_validator("DB_SSL_CA") + @classmethod + def resolve_db_ssl_ca(cls, value: str) -> str: + if value and not os.path.isabs(value): + return str(ROOT_DIR / value) + return value + + +settings = Settings() diff --git a/backend/core/lifespan.py b/backend/core/lifespan.py new file mode 100644 index 0000000000000000000000000000000000000000..d7b6a4ecb3f1ee17afe8036f846c42dc96cf344e --- /dev/null +++ b/backend/core/lifespan.py @@ -0,0 +1,51 @@ +import logging +from contextlib import asynccontextmanager + +from fastapi import FastAPI +from langchain_chroma import Chroma +from langchain_huggingface import HuggingFaceEmbeddings + +from backend.core.config import settings +from backend.rag.rag_context import RAGContext +from backend.rag.rag_reasoner import get_rag_chain +from backend.rag.rag_retriever import is_rag_ready +from backend.services.text_service.base_classifier import base_classifier +from backend.services.url_service.url_analyzer import url_detector + +logger = logging.getLogger(__name__) + + +@asynccontextmanager +async def lifespan(app: FastAPI): + print("應用環境啟動") + + base_classifier.load_model() + url_detector.load_model() + + # 預熱:ONNX session 首次推論與 tldextract 後綴表載入(可能需連網)都很慢, + # 在啟動時先跑一次,避免由第一個使用者請求承擔 + base_classifier.predict_text("預熱") + url_detector.analyze("https://www.example.com/") + + print("初始化 RAG") + RAGContext.set_app(app) + if not is_rag_ready(): + RAGContext.load_vectorstore() + + embeddings = HuggingFaceEmbeddings(model_name=settings.EMBED_MODEL) + embeddings.embed_query("warmup") + app.state.embeddings = embeddings + app.state.vectorstore = Chroma( + persist_directory=settings.RAG_PERSIST_DIR, embedding_function=embeddings + ) + + if is_rag_ready(): + try: + get_rag_chain() + except Exception: + # 例如尚未設定 GROQ_API_KEY;保留延遲建立,第一次 RAG 請求時再嘗試 + logger.warning("RAG chain 預先建立失敗,改為首次請求時建立", exc_info=True) + print("RAG 已初始化") + + yield + print("應用關閉") diff --git a/backend/core/logging.py b/backend/core/logging.py new file mode 100644 index 0000000000000000000000000000000000000000..0dea4f80f1b3218c0d1924ac58ee651898247132 --- /dev/null +++ b/backend/core/logging.py @@ -0,0 +1,10 @@ +import logging + + +def configure_logging(): + """配置日誌系統""" + logging.basicConfig( + level=logging.INFO, + format="%(levelname)s:%(name)s:%(message)s", + force=True +) \ No newline at end of file diff --git a/backend/main.py b/backend/main.py new file mode 100644 index 0000000000000000000000000000000000000000..b230685259f61606f659a679f83915b577696a54 --- /dev/null +++ b/backend/main.py @@ -0,0 +1,40 @@ +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +from backend.api.exception_handler import register_exception_handlers +from backend.api.routers import (image_price_validation, phone_detection, + text_inference, url_detection) +from backend.core.lifespan import lifespan +from backend.core.logging import configure_logging + +configure_logging() + +# 主入口 +app = FastAPI( + lifespan=lifespan, + title="詐騙風險偵測API", + description="用於檢測詐騙風險的API", + version="1.0.0" +) + +# 註冊全域例外處理 +register_exception_handlers(app) + +# CORS middleware +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=False, + allow_methods=["*"], + allow_headers=["*"], +) + +# 掛載router +app.include_router(text_inference.router, prefix="/api") +app.include_router(url_detection.router, prefix="/api") +app.include_router(image_price_validation.router, prefix="/api") +app.include_router(phone_detection.router, prefix="/api") + +@app.get("/") +async def root(): + return {"message": "AI API is running..."} diff --git a/backend/persistence/mysql_connection.py b/backend/persistence/mysql_connection.py new file mode 100644 index 0000000000000000000000000000000000000000..8974255bc7981b12a5a6d1b78df7be597167319c --- /dev/null +++ b/backend/persistence/mysql_connection.py @@ -0,0 +1,52 @@ +from typing import Any + +from sqlalchemy import create_engine +from sqlalchemy.engine import URL +from sqlalchemy.orm import sessionmaker + +from backend.core.config import settings + +host_name = settings.DB_HOST.strip() +port_number = settings.DB_PORT +if host_name and ":" in host_name and host_name.count(":") == 1: + host_candidate, port_candidate = host_name.rsplit(":", 1) + if port_candidate.isdigit(): + host_name = host_candidate + port_number = int(port_candidate) + +database_url = URL.create( + drivername="mysql+pymysql", + username=settings.DB_USERNAME, + password=settings.DB_PASSWORD, + host=host_name, + port=port_number, + database=settings.DB_NAME, + query={"charset": "utf8mb4"}, +) + +ssl_config = {"ca": settings.DB_SSL_CA} if settings.DB_SSL_CA else {} + +connect_args: dict[str, Any] = {"connect_timeout": settings.DB_TIMEOUT} +if ssl_config: + connect_args["ssl"] = ssl_config + +engine = create_engine( + database_url, + pool_pre_ping=True, + pool_recycle=3600, + connect_args=connect_args, +) + +SessionLocal = sessionmaker( + autocommit=False, + autoflush=False, + bind=engine, +) + + +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() diff --git a/backend/providers/__init__.py b/backend/providers/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..33632008217fdaa72f35e070d1ea41c78a8e2063 --- /dev/null +++ b/backend/providers/__init__.py @@ -0,0 +1,5 @@ +"""外部模型與服務供應者。""" + +from backend.providers.groq import GroqProvider, groq_provider + +__all__ = ["GroqProvider", "groq_provider"] diff --git a/backend/providers/groq.py b/backend/providers/groq.py new file mode 100644 index 0000000000000000000000000000000000000000..f2fc44fc389ad6e9b7a7735edba5061e851fc9df --- /dev/null +++ b/backend/providers/groq.py @@ -0,0 +1,83 @@ +"""集中建立 Groq 聊天模型。""" + +import os +from typing import Any + +from langchain_core.language_models.chat_models import BaseChatModel +from pydantic import SecretStr + +from backend.core.config import settings + + +class GroqProvider: + """建立具有一致 API key 處理方式的 Groq 聊天模型。""" + + @staticmethod + def create( + *, + model: str, + temperature: float = 0, + api_key: str | SecretStr | None = None, + reasoning_effort: str | None = None, + timeout: float | None = None, + max_retries: int | None = None, + model_kwargs: dict[str, Any] | None = None, + ) -> BaseChatModel: + """依用途建立 Groq 模型,僅傳入有設定的選用參數。""" + from langchain_groq import ChatGroq + + kwargs: dict[str, Any] = { + "model": model, + "temperature": temperature, + } + secret = GroqProvider.resolve_api_key(api_key) + if secret is not None: + kwargs["api_key"] = secret + if reasoning_effort: + kwargs["reasoning_effort"] = reasoning_effort + if timeout is not None: + kwargs["timeout"] = timeout + if max_retries is not None: + kwargs["max_retries"] = max_retries + if model_kwargs: + kwargs["model_kwargs"] = dict(model_kwargs) + + return ChatGroq(**kwargs) + + @classmethod + def resolve_api_key( + cls, + value: str | SecretStr | None = None, + ) -> SecretStr | None: + """依序解析指定值、應用設定與環境變數中的 Groq API key。""" + secret = cls._secret(value) + if secret is not None: + return secret + + secret = cls._secret(settings.GROQ_API_KEY) + if secret is not None: + return secret + + return cls._secret(os.getenv("GROQ_API_KEY")) + + @classmethod + def is_configured( + cls, + value: str | SecretStr | None = None, + ) -> bool: + """確認是否有可用的 Groq API key。""" + return cls.resolve_api_key(value) is not None + + @staticmethod + def _secret(value: str | SecretStr | None) -> SecretStr | None: + """將單一非空 API key 正規化為 SecretStr。""" + if isinstance(value, SecretStr): + return value if value.get_secret_value() else None + if isinstance(value, str) and value.strip(): + return SecretStr(value.strip()) + return None + + +groq_provider = GroqProvider() + +__all__ = ["GroqProvider", "groq_provider"] diff --git a/backend/rag/__init__.py b/backend/rag/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e4dcf57ab1b90a41fd439935c96c8625337e7709 --- /dev/null +++ b/backend/rag/__init__.py @@ -0,0 +1 @@ +"""RAG (Retrieval-Augmented Generation) module""" diff --git a/backend/rag/dto/__init__.py b/backend/rag/dto/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/backend/rag/dto/rag_analysis.py b/backend/rag/dto/rag_analysis.py new file mode 100644 index 0000000000000000000000000000000000000000..f6995e5c442f5a5ed66bbdec45193de655d7d87b --- /dev/null +++ b/backend/rag/dto/rag_analysis.py @@ -0,0 +1,21 @@ +from pydantic import BaseModel, Field + + +class RagEvidence(BaseModel): + used: bool + label: str | None = None + score: float | None = None + reason: str | None = None + raw_response: str | None = None + + +class RagResponse(BaseModel): + urgency: float = Field(ge=0, le=1, description="催促語氣") + money_related: float = Field(ge=0, le=1, description="金錢獲利") + baiting: float = Field(ge=0, le=1, description="異常誘導") + asks_for_personal_info: float = Field(ge=0, le=1, description="索取個資") + reputation: float = Field(ge=0, le=1, description="不可信度") + is_anti_fraud: float = Field(ge=0, le=1, description="是否為反詐騙") + social_engineering: float = Field(default=0.0, ge=0, le=1, description="社交工程和情緒操控") + reason: str = Field(..., min_length=50, max_length=300, description="原因") + suggestion: str = Field(..., min_length=10, max_length=30, description="建議") diff --git a/backend/rag/rag_context.py b/backend/rag/rag_context.py new file mode 100644 index 0000000000000000000000000000000000000000..447ac8c65b990aa6ab2e2048cfac96f670b6e9d4 --- /dev/null +++ b/backend/rag/rag_context.py @@ -0,0 +1,52 @@ +import os +from typing import Any + +from huggingface_hub import snapshot_download + +from backend.core.config import settings + + +class RAGContext: + """全局 RAG 上下文,存儲 app 實例""" + + _app: Any = None + + @staticmethod + def load_vectorstore() -> None: + """從 HuggingFace 加載向量資料庫""" + print("加載向量資料庫...") + snapshot_download( + repo_id=settings.HF_PERSIST_REPO_ID, + repo_type="dataset", + local_dir=settings.RAG_PERSIST_DIR, + token=settings.HF_TOKEN or None + ) + print("向量資料庫加載完畢") + + @classmethod + def set_app(cls, app: Any) -> None: + """設置 FastAPI app 實例""" + cls._app = app + + @classmethod + def get_app(cls) -> Any: + """取得 FastAPI app 實例""" + if cls._app is None: + raise RuntimeError("RAG Context 未初始化,請先呼叫 set_app()") + return cls._app + + @classmethod + def get_vectorstore(cls) -> Any: + """取得 vectorstore""" + app = cls.get_app() + if not hasattr(app.state, 'vectorstore'): + raise RuntimeError("vectorstore 尚未初始化") + return app.state.vectorstore + + @classmethod + def get_embeddings(cls) -> Any: + """取得 embeddings""" + app = cls.get_app() + if not hasattr(app.state, 'embeddings'): + raise RuntimeError("embeddings 尚未初始化") + return app.state.embeddings diff --git a/backend/rag/rag_reasoner.py b/backend/rag/rag_reasoner.py new file mode 100644 index 0000000000000000000000000000000000000000..cb6c5d1ce73d003b8a9b70938806d44913985dbd --- /dev/null +++ b/backend/rag/rag_reasoner.py @@ -0,0 +1,178 @@ +import json +from typing import Any + +from backend.core.config import settings +from backend.providers import groq_provider +from backend.rag.dto.rag_analysis import RagEvidence, RagResponse +from backend.rag.rag_retriever import (format_context, get_retriever, + is_rag_ready, normalize_query_text) +from backend.utils.text_cleaner import normalize_escape_sequences + +_rag_chain : Any | None = None + +def _require_langchain() -> tuple[Any, Any, Any]: + from langchain_core.output_parsers import JsonOutputParser + from langchain_core.prompts import ChatPromptTemplate + from langchain_core.runnables import RunnablePassthrough + + return ChatPromptTemplate, RunnablePassthrough, JsonOutputParser + + +def _get_prompt() -> Any: + ChatPromptTemplate, _, _ = _require_langchain() + template = """ + 你是一個防詐風險分析專家,請根據訊息內容分析不同風險特徵的符合程度。 + + 【參考案例 - 僅供理解詐騙模式,禁止複製其中文字到 reason】 + {context} + 【參考案例結束】 + + 【待分析訊息 - 僅供理解內容,禁止複製其中文字到 reason】 + {question} + 【待分析訊息結束】 + + 請依序完成以下判斷: + 第一步:判斷這則訊息的主要目的 + - A:提醒他人避免詐騙、警示風險、安全建議 + - B:可疑訊息(詐騙、誘導、索資、施壓、領錢、假冒通知) + - C:一般安全訊息(正常日常對話、正常通知) + - D:對話類互動異常(對話中出現一方威脅、一方示弱、且具有情緒操控) + + 第二步: + - 若第一步為 A,分析以下維度(0~1): + - is_anti_fraud:是否具有反詐騙、教育勸導 + 直接輸出下方 JSON,reason,其他欄位全部填 0.0,用繁體中文說明,禁止複製原文 + {format_instructions} + + - 若第一步為 B 或 C,再分析以下維度(0~1): + - urgency:催促、限時、要求立即操作 + - money_related:誘導金錢交易、投資、匯款,通知領錢 + - baiting:不合理優惠、保證獲利、隱藏交易 + - asks_for_personal_info:要求個資、帳號、驗證碼 + - reputation:來源可疑、假冒權威、誇大承諾 + - reason:禁止複製原文,用繁體中文說明訊息中出現哪些疑點 + - suggestion:針對此風險給出一句具體的下一步建議,以「建議」開頭 + + - 若第一步為 D,分析以下維度(0~1): + - urgency:是否出現威脅、限時、製造恐慌 + - money_related:是否涉及借錢、投資、轉帳請求,通知領錢 + - baiting:是否以感情、同情、利益逐步誘導 + - asks_for_personal_info:是否套問個資、帳號、行蹤 + - reputation:身份是否可疑、前後矛盾、來路不明 + - social_engineering:是否存在情緒操控、威脅(0~1) + - reason:禁止複製原文,用繁體中文具體說明對話中出現哪些可疑行為或話術 + - suggestion:針對此風險給出一句具體的下一步建議,以「建議」開頭 + 輸出格式: + {format_instructions} + """ + + parser = _get_parser() + prompt = ChatPromptTemplate.from_template( + template, + partial_variables={"format_instructions": parser.get_format_instructions()} + ) + return prompt + +def _get_llm() -> Any: + return groq_provider.create( + model=settings.RAG_MODEL_NAME, + temperature=0.1, + model_kwargs={ + "top_p": 0.7, + } + ) + + +def _get_parser() -> Any: + _, _, JsonOutputParser = _require_langchain() + parser = JsonOutputParser(pydantic_object=RagResponse) + return parser + +def get_rag_chain() -> Any: + global _rag_chain + if _rag_chain is None: + _rag_chain = build_rag_chain() + return _rag_chain + +def build_rag_chain() -> Any: + _, RunnablePassthrough, _ = _require_langchain() + + prompt = _get_prompt() + retriever = get_retriever() + llm = _get_llm() + parser = _get_parser() + + return ( + { + "context": retriever | format_context, + "question": RunnablePassthrough() | normalize_query_text, + } + | prompt + | llm + | parser + ) + + +def _to_float(v: Any) -> float: + """安全轉換任意值為浮點數,預設為 0.0""" + try: + return float(v) + except (TypeError, ValueError): + return 0.0 + + +def parse_structured_response(text: str) -> tuple[str, float, str]: + payload = json.loads(text) + if not isinstance(payload, dict): + return "回傳錯誤", -1, "" + money = _to_float(payload.get("money_related")) + urgency = _to_float(payload.get("urgency")) + baiting = _to_float(payload.get("baiting")) + personal_info = _to_float(payload.get("asks_for_personal_info")) + is_anti_fraud = _to_float(payload.get("is_anti_fraud")) + social_engineering = _to_float(payload.get("social_engineering")) + reputation = _to_float(payload.get("reputation")) + + print(money, urgency, baiting, personal_info, is_anti_fraud, social_engineering, reputation, end='\n') + + score = min((money * 0.25 + + urgency * 0.30 + + baiting * 0.25 + + personal_info * 0.25 + + social_engineering * 0.30 - + is_anti_fraud * 1 + + reputation * 0.30) * 100, 100) + + score = max(score, 5) + suggestion = normalize_escape_sequences(str(payload.get("suggestion") or "")) + reason = normalize_escape_sequences(str(payload.get("reason") or "")) + + label = "低風險" + if score >= 80: + label = "高風險" + elif score >= 40: + label = "中等風險" + + return label, score, f"{reason}{suggestion}" + + +async def analyze_with_rag(message: str) -> RagEvidence: + if not is_rag_ready(): + return RagEvidence(used=False) + + rag_chain = get_rag_chain() + # 避免被同步請求卡住 + chain_output = await rag_chain.ainvoke(message) + if isinstance(chain_output, dict): + raw_response = json.dumps(chain_output, ensure_ascii=False) + else: + raw_response = chain_output + label, score, reason = parse_structured_response(raw_response) + + return RagEvidence( + used=True, + label=label, + score=score, + reason=reason, + raw_response=raw_response, + ) diff --git a/backend/rag/rag_retriever.py b/backend/rag/rag_retriever.py new file mode 100644 index 0000000000000000000000000000000000000000..1d2314f5185e5891e7b2131f9aeb857643da1278 --- /dev/null +++ b/backend/rag/rag_retriever.py @@ -0,0 +1,43 @@ +from pathlib import Path +from typing import Any + +from backend.core.config import settings +from backend.rag.rag_context import RAGContext +from backend.utils.text_cleaner import normalize_text + + +def _get_vectorstore() -> Any: + try: + return RAGContext.get_vectorstore() + except RuntimeError as e: + print(f"取得 vectorstore 失敗: {e}") + raise + + +def get_retriever() -> Any: + vectorstore = _get_vectorstore() + return vectorstore.as_retriever(search_kwargs={"k": settings.RAG_TOP_K}) + + +def format_context(docs: list[Any]) -> str: + blocks: list[str] = [] + for idx, doc in enumerate(docs, start=1): + blocks.append(f"[相似案例 {idx}]\n{doc.page_content[:190]}") # 限制每句參考在190以內 + return "\n\n".join(blocks) + + +def normalize_query_text(message: str) -> str: + cleaned = normalize_text(message) + return f"待分析訊息: {cleaned}" + + +def retrieve_similar_cases(message: str) -> list[Any]: + if not is_rag_ready(): + return [] + return get_retriever().invoke(normalize_query_text(message)) + + +def is_rag_ready() -> bool: + if not settings.RAG_ENABLED: + return False + return Path(settings.RAG_PERSIST_DIR).exists() diff --git a/backend/repository/__init__.py b/backend/repository/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/backend/repository/case_repository.py b/backend/repository/case_repository.py new file mode 100644 index 0000000000000000000000000000000000000000..9a35b4e14ed58ae884080daa50bb6d403bdf84ea --- /dev/null +++ b/backend/repository/case_repository.py @@ -0,0 +1,28 @@ +import json +from datetime import datetime, timezone +from pathlib import Path +from uuid import uuid4 + +from backend.core.config import settings + + +class CaseRepository: + def __init__(self, path: str | None = None): + self._path = Path(path or settings.CASE_LOG_PATH) + + def save(self, payload: dict) -> str: + case_id = str(uuid4()) + envelope = { + "case_id": case_id, + "created_at": datetime.now(timezone.utc).isoformat(), + "payload": payload, + } + + self._path.parent.mkdir(parents=True, exist_ok=True) + with self._path.open("a", encoding="utf-8") as f: + f.write(json.dumps(envelope, ensure_ascii=False) + "\n") + + return case_id + + +case_repository = CaseRepository() diff --git a/backend/repository/family_repository.py b/backend/repository/family_repository.py new file mode 100644 index 0000000000000000000000000000000000000000..3ff68fcdfdaf25eae23abfe57378fedc62d8f9b9 --- /dev/null +++ b/backend/repository/family_repository.py @@ -0,0 +1,172 @@ +from __future__ import annotations + +from collections.abc import Iterable +from typing import Any + +from sqlalchemy import bindparam, text + +from backend.persistence.mysql_connection import engine as db_engine +from backend.utils.phone_normalizer import normalize, normalize_db_phone + +_SELF_ROW_QUERY = text( + """ + SELECT phone_number, + dominant_tag, + first_seen_at, + last_seen_at, + referral_type, + referral_info + FROM blacklist + WHERE phone_number IN (:phone_db, :phone_full) + LIMIT 1 + """ +) + +_RELATED_ROWS_QUERY = text( + """ + SELECT DISTINCT phone_number, + dominant_tag AS phone_type, + first_seen_at AS first_reported_at, + last_seen_at AS last_reported_at, + referral_type AS transfer_type, + referral_info AS transfer_content + FROM blacklist + WHERE phone_number NOT IN (:phone_db, :phone_full) + AND ( + referral_info IN (:phone_db, :phone_full) + OR LEFT(phone_number, 6) = LEFT(:phone_db, 6) + OR dominant_tag = :phone_type + ) + ORDER BY + CASE + WHEN referral_info IN (:phone_db, :phone_full) THEN 0 + WHEN LEFT(phone_number, 6) = LEFT(:phone_db, 6) THEN 1 + ELSE 2 + END + LIMIT 20 + """ +) + +_REPORTER_PHONES_QUERY = text( + """ + SELECT DISTINCT reporter_phone + FROM fraud_reports + WHERE phone_number IN (:phone_db, :phone_full) + LIMIT 10 + """ +) + +_REPORTER_ROWS_QUERY = text( + """ + SELECT DISTINCT fr.phone_number, + fr.tag AS phone_type, + fr.report_time AS first_reported_at, + fr.report_time AS last_reported_at, + fr.referral_type AS transfer_type, + fr.referral_info AS transfer_content + FROM fraud_reports fr + WHERE fr.reporter_phone IN :reporter_phone_list + LIMIT 20 + """ +).bindparams(bindparam("reporter_phone_list", expanding=True)) + +# 只撈指定號碼的通報事件;原本每次查詢都整張表載入,資料量越大越慢 +_REPORT_EVENTS_QUERY = text( + """ + SELECT reporter_phone, phone_number, report_time + FROM fraud_reports + WHERE phone_number IN :phone_list + AND reporter_phone IS NOT NULL + AND report_time IS NOT NULL + """ +).bindparams(bindparam("phone_list", expanding=True)) + + +class FamilyRepository: + def __init__(self, engine: Any | None = None): + self._engine = engine or db_engine + + def list_fraud_report_events( + self, phones: Iterable[str], engine: Any | None = None + ) -> dict[str, list[tuple[str, Any]]]: + """回傳 {通報者: [(被通報號碼, 通報時間), ...]},只包含 phones 內號碼的事件。 + + 號碼同時以完整格式與資料庫格式(去前導 0)查詢,與本模組其他查詢的假設一致。 + """ + active_engine = engine or self._engine + phone_list = sorted( + {form for phone in phones for form in (phone, normalize_db_phone(phone)) if form} + ) + if active_engine is None or not phone_list: + return {} + + with active_engine.connect() as conn: + rows = conn.execute(_REPORT_EVENTS_QUERY, {"phone_list": phone_list}).mappings().all() + + events: dict[str, list[tuple[str, Any]]] = {} + for row in rows: + reporter = normalize(row.get("reporter_phone")) + phone = normalize(row.get("phone_number")) + reported_at = row.get("report_time") + if reporter and phone and reported_at: + events.setdefault(reporter, []).append((phone, reported_at)) + return events + + def list_family_records( + self, phone_number: str, engine: Any | None = None + ) -> list[dict[str, Any]]: + """撈出查詢號碼本身、可能相關的黑名單號碼與同通報者通報過的號碼,作為家族分析輸入。""" + active_engine = engine or self._engine + normalized_phone = normalize(phone_number) + if active_engine is None or not normalized_phone: + return [] + + phone_params = { + "phone_db": normalize_db_phone(phone_number), + "phone_full": normalized_phone, + } + with active_engine.connect() as conn: + self_row = conn.execute(_SELF_ROW_QUERY, phone_params).mappings().first() + phone_type = self_row["dominant_tag"] if self_row else "其他" + related_rows = conn.execute( + _RELATED_ROWS_QUERY, {**phone_params, "phone_type": phone_type} + ).mappings().all() + + reporter_phones = [ + row["reporter_phone"] + for row in conn.execute(_REPORTER_PHONES_QUERY, phone_params).mappings().all() + if row.get("reporter_phone") + ] + reporter_rows = [] + if reporter_phones: + reporter_rows = conn.execute( + _REPORTER_ROWS_QUERY, {"reporter_phone_list": reporter_phones} + ).mappings().all() + + records: list[dict[str, Any]] = [] + if self_row: + records.append({ + "電話號碼": self_row["phone_number"], + "通報標籤": self_row["dominant_tag"] or "其他", + "首次通報時間": self_row["first_seen_at"], + "最後通報時間": self_row["last_seen_at"], + "轉介類型": self_row["referral_type"] or 0, + "轉介內容": self_row["referral_info"] or "", + "通報者識別碼": "", + "通報次數": 1, + }) + for entry in [*related_rows, *reporter_rows]: + records.append({ + "電話號碼": entry["phone_number"], + "通報標籤": entry["phone_type"] or "其他", + "首次通報時間": entry["first_reported_at"], + "最後通報時間": entry["last_reported_at"], + "轉介類型": entry["transfer_type"] or 0, + "轉介內容": entry["transfer_content"] or "", + "通報者識別碼": "", + "通報次數": 1, + }) + return records + + +family_repository = FamilyRepository() diff --git a/backend/repository/market_price_repository.py b/backend/repository/market_price_repository.py new file mode 100644 index 0000000000000000000000000000000000000000..bc214df21d5237672fce070c3068c5168214cd75 --- /dev/null +++ b/backend/repository/market_price_repository.py @@ -0,0 +1,125 @@ +import json +import re +import unicodedata +from pathlib import Path +from typing import TypedDict + +from rapidfuzz import fuzz + +from backend.core.config import settings + + +class MarketPriceRecord(TypedDict): + aliases: list[str] + product_name: str + brand_model: str + market_price: int + + +DEFAULT_MARKET_PRICE_RECORDS: list[MarketPriceRecord] = [ + { + "aliases": [ + "Panasonic EH-NE11", + "國際牌 EH-NE11", + "EH-NE11", + "Panasonic 國際牌 1200W 負離子速乾型冷熱吹風機", + ], + "product_name": "Panasonic 國際牌 1200W 負離子速乾型冷熱吹風機", + "brand_model": "Panasonic EH-NE11", + "market_price": 1290, + }, + { + "aliases": ["iPhone 15", "Apple iPhone 15"], + "product_name": "Apple iPhone 15", + "brand_model": "Apple iPhone 15", + "market_price": 27900, + }, +] + + +class MarketPriceRepository: + def __init__(self, source_path: str | None = None): + self._source_path = Path(source_path or settings.MARKET_PRICE_DB_PATH) + self._records = self._load_records() + self._alias_index = self._build_alias_index(self._records) + + @classmethod + def _build_alias_index( + cls, records: list[MarketPriceRecord] + ) -> list[tuple[MarketPriceRecord, str, str]]: + """預先算好每個別名的精確比對與模糊比對正規化結果,避免每次查詢重算。""" + return [ + (item, cls._normalize(alias), cls._normalize_for_fuzzy(alias)) + for item in records + for alias in item["aliases"] + ] + + @staticmethod + def _normalize(text: str) -> str: + text = unicodedata.normalize("NFKC", text).lower() + return re.sub(r"[^a-z0-9\u4e00-\u9fff]+", "", text) + + @staticmethod + def _normalize_for_fuzzy(text: str) -> str: + text = unicodedata.normalize("NFKC", text).lower() + text = re.sub(r"[^a-z0-9\u4e00-\u9fff]+", " ", text) + return re.sub(r"\s+", " ", text).strip() + + def _load_records(self) -> list[MarketPriceRecord]: + if not self._source_path.exists(): + return DEFAULT_MARKET_PRICE_RECORDS.copy() + + with self._source_path.open("r", encoding="utf-8") as f: + raw = json.load(f) + + records: list[MarketPriceRecord] = [] + for row in raw: + aliases = [str(a) for a in row.get("aliases", []) if str(a).strip()] + if not aliases: + continue + + market_price = int(row.get("market_price", 0) or 0) + records.append( + { + "aliases": aliases, + "product_name": str(row.get("product_name", "未知商品")), + "brand_model": str(row.get("brand_model", "未知型號")), + "market_price": market_price, + } + ) + + return records + + def reload(self) -> None: + self._records = self._load_records() + self._alias_index = self._build_alias_index(self._records) + + def find_by_text(self, text: str) -> tuple[str, str, int]: + normalized = self._normalize(text) + for item, alias_exact, _ in self._alias_index: + if alias_exact in normalized: + return item["product_name"], item["brand_model"], item["market_price"] + + # OCR 容錯:若有輕微辨識錯字,使用模糊比對兜底。 + fuzzy_text = self._normalize_for_fuzzy(text) + best_score = 0.0 + best_item: MarketPriceRecord | None = None + + for item, _, alias_fuzzy in self._alias_index: + if not alias_fuzzy: + continue + + score = max( + float(fuzz.partial_ratio(alias_fuzzy, fuzzy_text)), + float(fuzz.token_set_ratio(alias_fuzzy, fuzzy_text)), + ) + if score > best_score: + best_score = score + best_item = item + + if best_item and best_score >= settings.PRODUCT_MATCH_FUZZY_MIN_SCORE: + return best_item["product_name"], best_item["brand_model"], best_item["market_price"] + + return "未知商品", "未知型號", 0 + +market_price_repository = MarketPriceRepository() diff --git a/backend/repository/phone_repository.py b/backend/repository/phone_repository.py new file mode 100644 index 0000000000000000000000000000000000000000..822f167bc88bcbba5b9487a463f41cd32611ed67 --- /dev/null +++ b/backend/repository/phone_repository.py @@ -0,0 +1,162 @@ +from __future__ import annotations + +from datetime import datetime +from typing import Any + +from sqlalchemy import text +from sqlalchemy.exc import OperationalError + +from backend.persistence.mysql_connection import engine as db_engine +from backend.utils.phone_normalizer import normalize, normalize_db_phone + + +class PhoneRepository: + def __init__(self, engine: Any | None = None): + self._engine = engine or db_engine + + def get_phone_record(self, phone_number: str) -> dict[str, Any] | None: + if self._engine is None: + return None + + normalized_phone = normalize(phone_number) + db_phone = normalize_db_phone(phone_number) + query = text( + """ + SELECT p.phone_number, + p.tags, + b.report_count AS total_reports, + b.first_seen_at AS first_reported_at, + b.last_seen_at AS last_reported_at, + b.dominant_tag AS phone_type, + w.org_name AS owner_name + FROM phone p + LEFT JOIN blacklist b ON p.phone_number = b.phone_number + LEFT JOIN whitelist w ON p.phone_number = w.phone_number + WHERE p.phone_number IN (:phone_db, :phone_full) + """ + ) + + try: + with self._engine.begin() as conn: + return conn.execute( + query, + {"phone_db": db_phone, "phone_full": normalized_phone}, + ).mappings().first() + except OperationalError: + return None + + def report_suspicious( + self, + phone_number: str, + phone_type: str, + reporter_phone: str | None, + transfer_type: int | None = 0, + transfer_content: str | None = None, + ) -> dict[str, Any]: + if self._engine is None: + raise RuntimeError("資料庫連線失敗:database engine unavailable") + + normalized_phone = normalize(phone_number) + db_phone = normalize_db_phone(phone_number) + reporter_phone = normalize(reporter_phone) if reporter_phone else None + transfer_content = ( + normalize(transfer_content) + if transfer_type == 1 and transfer_content + else transfer_content + ) + now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + try: + with self._engine.begin() as conn: + phone_row = conn.execute( + text( + "SELECT phone_number, tags FROM phone WHERE phone_number IN (:phone_db, :phone_full)" + ), + {"phone_db": db_phone, "phone_full": normalized_phone}, + ).mappings().first() + + if phone_row and phone_row["tags"] == "White": + raise ValueError("該號碼已存在於白名單,無法回報為可疑號碼。") + + if phone_row: + conn.execute( + text( + "UPDATE phone SET tags = 'Black' WHERE phone_number IN (:phone_db, :phone_full)" + ), + {"phone_db": db_phone, "phone_full": normalized_phone}, + ) + else: + conn.execute( + text( + "INSERT INTO phone(phone_number, tags) VALUES (:phone_number, 'Black')" + ), + {"phone_number": db_phone}, + ) + + blacklist_row = conn.execute( + text( + "SELECT report_count, dominant_tag FROM blacklist WHERE phone_number IN (:phone_db, :phone_full)" + ), + {"phone_db": db_phone, "phone_full": normalized_phone}, + ).mappings().first() + + if blacklist_row: + conn.execute( + text( + "UPDATE blacklist " + "SET report_count = report_count + 1, last_seen_at = :last_seen_at, " + "dominant_tag = :phone_type, referral_type = :referral_type, referral_info = :referral_info " + "WHERE phone_number = :phone_number" + ), + { + "last_seen_at": now, + "phone_type": phone_type, + "referral_type": transfer_type or 0, + "referral_info": transfer_content, + "phone_number": db_phone, + }, + ) + total_reports = int(blacklist_row["report_count"]) + 1 + else: + conn.execute( + text( + "INSERT INTO blacklist(phone_number, report_count, first_seen_at, last_seen_at, dominant_tag, referral_type, referral_info) " + "VALUES (:phone_number, 1, :first_seen_at, :last_seen_at, :phone_type, :referral_type, :referral_info)" + ), + { + "phone_number": db_phone, + "first_seen_at": now, + "last_seen_at": now, + "phone_type": phone_type, + "referral_type": transfer_type or 0, + "referral_info": transfer_content, + }, + ) + total_reports = 1 + + conn.execute( + text( + "INSERT INTO fraud_reports(phone_number, reporter_phone, report_time, tag, referral_type, referral_info) " + "VALUES (:phone_number, :reporter_phone, :report_time, :tag, :referral_type, :referral_info)" + ), + { + "phone_number": db_phone, + "reporter_phone": reporter_phone and normalize_db_phone(reporter_phone), + "report_time": now, + "tag": phone_type, + "referral_type": transfer_type or 0, + "referral_info": transfer_content, + }, + ) + except OperationalError as exc: + raise RuntimeError(f"資料庫連線失敗:{exc}") from exc + + return { + "phone_number": normalized_phone, + "status": "black", + "total_reports": total_reports, + "report_time": now, + } + + +phone_repository = PhoneRepository() diff --git a/backend/repository/rag_repository.py b/backend/repository/rag_repository.py new file mode 100644 index 0000000000000000000000000000000000000000..bad84e29e8d65ebf13ac6b9e462229254e2e4302 --- /dev/null +++ b/backend/repository/rag_repository.py @@ -0,0 +1,70 @@ +from pathlib import Path +from typing import Any + +from backend.core.config import settings +from backend.rag.rag_context import RAGContext + + +def _require_langchain() -> tuple[Any]: + from langchain_chroma import Chroma + + return (Chroma,) + + +def get_vectorstore() -> Any: + """取得向量資料庫實例""" + (Chroma,) = _require_langchain() + return Chroma( + persist_directory=settings.RAG_PERSIST_DIR, + embedding_function=RAGContext.get_embeddings(), + ) + + +def query_documents(query_text: str, k: int | None = None) -> list[Any]: + """查詢向量庫中相似的文檔 + + Args: + query_text: 查詢文本 + k: 返回結果數量,默认使用配置中的 RAG_TOP_K + + Returns: + 相似文檔列表 + """ + if k is None: + k = settings.RAG_TOP_K + + vectorstore = get_vectorstore() + retriever = vectorstore.as_retriever(search_kwargs={"k": k}) + return retriever.invoke(query_text) + + +def add_documents(texts: list[str], metadatas: list[dict[str, Any]] | None = None) -> list[str]: + """新增文檔到向量庫 + + Args: + texts: 文本列表 + metadatas: 元數據列表 + + Returns: + 新增文檔的 ID 列表 + """ + vectorstore = get_vectorstore() + return vectorstore.add_texts(texts=texts, metadatas=metadatas) + + +def delete_documents(ids: list[str]) -> bool: + """從向量庫刪除文檔 + + Args: + ids: 文檔 ID 列表 + + Returns: + 是否刪除成功 + """ + vectorstore = get_vectorstore() + try: + vectorstore.delete(ids=ids) + return True + except Exception as e: + print(f"Error deleting documents: {e}") + return False diff --git a/backend/services/__init__.py b/backend/services/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/backend/services/dto/__init__.py b/backend/services/dto/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/backend/services/dto/price_analysis.py b/backend/services/dto/price_analysis.py new file mode 100644 index 0000000000000000000000000000000000000000..c6a898d21ff199e4aac7dabde8d8fafb5bea220b --- /dev/null +++ b/backend/services/dto/price_analysis.py @@ -0,0 +1,150 @@ +from typing import Literal + +from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator + +from backend.services.image_price_service.domain.models import ( + MarketplaceCondition, + MarketplaceLayout, +) +from backend.services.image_price_service.domain.policy import ( + DEFAULT_PRICE_RISK_POLICY, +) + +RiskLabel = Literal["LOW", "MEDIUM", "HIGH", "UNKNOWN"] +DecisionLayer = Literal["fast", "llm", "llm_simulated", "decision_error"] +MarketPriceSource = Literal["online", "fallback_local", "not_evaluated"] +SearchTool = Literal["serp_api", "tavily", "ddgs"] +MarketPriceStatus = Literal["success", "insufficient", "not_found"] +MarketPriceReferenceMode = Literal["iqr", "median_low_sample"] + + +class MarketPriceCandidateEvidence(BaseModel): + """保留已驗證市場候選的來源與價格證據。""" + + model_config = ConfigDict(extra="forbid") + + candidate_id: str + title: str + price: int = Field(gt=0) + condition: MarketplaceCondition + evidence: str + + +class MarketPriceEstimate(BaseModel): + """保留市場價格區間、資料量、來源與可信度。""" + + model_config = ConfigDict(extra="forbid") + + status: MarketPriceStatus + condition: MarketplaceCondition + reference_mode: MarketPriceReferenceMode + median_price: int = Field(ge=0) + low_price: int = Field(ge=0) + high_price: int = Field(ge=0) + sample_count: int = Field(ge=0) + site_count: int = Field(ge=0) + source: MarketPriceSource + confidence: float = Field(ge=0.0, le=1.0) + search_tools: list[SearchTool] = Field(default_factory=list) + candidates: tuple[MarketPriceCandidateEvidence, ...] = () + + +class DeepAnalysisReview(BaseModel): + """LLM 對有限商品狀態原文所做的結構化複核。""" + + model_config = ConfigDict(extra="forbid") + + reviewed_condition: MarketplaceCondition + condition_detail: str = "" + condition_evidence: str = "" + reason: str + review_confidence: float = Field(ge=0.0, le=1.0) + + @model_validator(mode="after") + def validate_review_consistency(self) -> "DeepAnalysisReview": + """拒絕缺少證據或與 UNKNOWN 狀態矛盾的複核結果。""" + if ( + self.reviewed_condition != MarketplaceCondition.UNKNOWN + and not self.condition_evidence.strip() + ): + raise ValueError("確認商品狀態時必須提供可追溯的狀態原文證據") + + if ( + self.reviewed_condition == MarketplaceCondition.UNKNOWN + and self.condition_detail.strip() + ): + raise ValueError("商品狀態為 UNKNOWN 時不得提供已確認的狀態細節") + + return self + + +class ImagePriceAnalysisResult(BaseModel): + """圖片價格分析的內部完整結果""" + + model_config = ConfigDict(extra="forbid") + + filename: str + content_type: str + extracted_text: str + success: bool = True + error_code: str | None = None + message: str | None = None + product_name: str | None = None + brand_model: str | None = None + listed_price: int | None = Field(default=None, ge=0) + market_price: int = Field(default=0, ge=0) + market_price_source: MarketPriceSource = "not_evaluated" + market_price_estimates: tuple[MarketPriceEstimate, ...] = () + risk_label: RiskLabel = "UNKNOWN" + score: str | float | None = None + reason: str | None = None + evidence: list[str] = Field(default_factory=list) + confidence: float | None = Field(default=None, ge=0, le=1) + decision_layer: DecisionLayer = "fast" + search_tools: list[SearchTool] = Field(default_factory=list) + marketplace_layout: MarketplaceLayout = MarketplaceLayout.UNKNOWN + marketplace_confidence: float = Field(default=0.0, ge=0, le=1) + extraction_confidence: float | None = Field(default=None, ge=0, le=1) + price_source_text: str | None = None + price_extraction_reason: str | None = None + seller_name: str | None = None + condition: MarketplaceCondition = MarketplaceCondition.UNKNOWN + condition_detail: str = "" + condition_source_text: str = "" + condition_extraction_confidence: float = Field( + default=0.0, + ge=0.0, + le=1.0, + description=( + "只代表 extractor 擷取商品狀態的可信度,不代表商品匹配、" + "市場資料或價格風險可信度" + ), + ) + extraction_warnings: list[str] = Field(default_factory=list) + + @field_validator("listed_price") + @classmethod + def validate_supported_listing_price(cls, value: int | None) -> int | None: + """拒絕超出服務支援上限的刊登價格。""" + if ( + value is not None + and value > DEFAULT_PRICE_RISK_POLICY.maximum_supported_price + ): + raise ValueError( + "PRICE_OUT_OF_SUPPORTED_RANGE:刊登價格超出支援範圍" + ) + return value + + +class ProductIdentification(BaseModel): + """商品辨識階段輸出的正規化資訊、查價搜尋詞與本地參考價格。""" + + product_name: str + brand_model: str + known_specs: list[str] = Field(default_factory=list) + search_query: str = "" + market_price: int = Field(default=0, ge=0) + + +class OCRResponse(BaseModel): + extracted_text: str = Field(..., description="OCR 萃取文字") diff --git a/backend/services/dto/text_analysis.py b/backend/services/dto/text_analysis.py new file mode 100644 index 0000000000000000000000000000000000000000..640f3456b689a1c322324bc82589c4995d5343ba --- /dev/null +++ b/backend/services/dto/text_analysis.py @@ -0,0 +1,12 @@ +from pydantic import BaseModel + + +class BaseEvidence(BaseModel): + text: str + rule_score: int + rule_reason: str + rule_hits: list[str] + model_label: str + model_confidence: float | None + model_margin: float | None + chunk_consistent: bool \ No newline at end of file diff --git a/backend/services/image_price_service/__init__.py b/backend/services/image_price_service/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/backend/services/image_price_service/__init__.py @@ -0,0 +1 @@ + diff --git a/backend/services/image_price_service/case_recorder.py b/backend/services/image_price_service/case_recorder.py new file mode 100644 index 0000000000000000000000000000000000000000..56818fb2f23391dddf4b70ac2fac02e8a4e5cd03 --- /dev/null +++ b/backend/services/image_price_service/case_recorder.py @@ -0,0 +1,58 @@ +"""將圖片價格分析結果轉成案例資料並以 best-effort 方式保存。""" + +import logging +from typing import Any + +from backend.core.config import settings +from backend.services.dto.price_analysis import ImagePriceAnalysisResult + +logger = logging.getLogger(__name__) + + +def record_case( + case_repo: Any, + result: ImagePriceAnalysisResult, +) -> str | None: + """依設定保存分析案例;寫入失敗時維持原分析結果。""" + if not settings.CASE_MEMORY_ENABLED: + return None + + try: + return case_repo.save( + { + "filename": result.filename, + "content_type": result.content_type, + "product_name": result.product_name, + "brand_model": result.brand_model, + "selling_price": result.listed_price, + "market_price": result.market_price, + "market_price_source": result.market_price_source, + "market_price_estimates": [ + estimate.model_dump(exclude={"candidates"}, mode="json") + for estimate in result.market_price_estimates + ], + "risk_label": result.risk_label, + "risk_score": result.score, + "reason": result.reason, + "evidence": result.evidence, + "confidence": result.confidence, + "decision_layer": result.decision_layer, + "error_code": result.error_code, + "extracted_text": result.extracted_text, + "marketplace_layout": result.marketplace_layout, + "marketplace_confidence": result.marketplace_confidence, + "price_source_text": result.price_source_text, + "price_extraction_reason": result.price_extraction_reason, + "seller_name": result.seller_name, + "condition": result.condition, + "condition_detail": result.condition_detail, + "condition_source_text": result.condition_source_text, + "condition_extraction_confidence": ( + result.condition_extraction_confidence + ), + "extraction_warnings": result.extraction_warnings, + } + ) + except Exception: + logger.warning("圖片價格分析案例保存失敗", exc_info=True) + return None diff --git a/backend/services/image_price_service/domain/__init__.py b/backend/services/image_price_service/domain/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..d001c2c89fc304963f866aec80d08fb148da311d --- /dev/null +++ b/backend/services/image_price_service/domain/__init__.py @@ -0,0 +1 @@ +"""商品價格驗證的 domain models 與 policy。""" diff --git a/backend/services/image_price_service/domain/models.py b/backend/services/image_price_service/domain/models.py new file mode 100644 index 0000000000000000000000000000000000000000..ffd75b9b52da2271738c0d1e16049ed3000780b5 --- /dev/null +++ b/backend/services/image_price_service/domain/models.py @@ -0,0 +1,152 @@ +"""定義 OCR、商品頁驗證與刊登欄位抽取共用的資料模型。""" + +from enum import Enum +from typing import Any + +from pydantic import BaseModel, Field, model_validator + +from backend.services.image_price_service.domain.policy import ( + DEFAULT_PRICE_RISK_POLICY, +) + + +class MarketplaceCondition(str, Enum): + """刊登商品的使用狀況。""" + + NEW = "new" + USED = "used" + UNKNOWN = "unknown" + + +class MarketplaceLayout(str, Enum): + """商品頁截圖的版面類型。""" + + MOBILE = "mobile" + DESKTOP = "desktop" + UNKNOWN = "unknown" + + +class PriceSection(str, Enum): + """價格候選值所在的商品頁區段。""" + + MAIN_PRICE = "main_price" + OFFER_RANGE = "offer_range" + DESCRIPTION = "description" + SELLER_INFO = "seller_info" + MESSAGE_BOX = "message_box" + DETAIL = "detail" + UNKNOWN = "unknown" + + +class OCRTextBlock(BaseModel): + """保存單一 OCR 文字區塊及其座標。""" + + text: str + x: float | None = None + y: float | None = None + width: float | None = None + height: float | None = None + confidence: float | None = None + + +class OCRDocument(BaseModel): + """保存 OCR 全文、文字區塊與頁面尺寸。""" + + text: str + blocks: list[OCRTextBlock] = Field(default_factory=list) + width: float | None = None + height: float | None = None + + @property + def has_coordinates(self) -> bool: + """判斷所有文字區塊是否都有座標。""" + return bool(self.blocks) and all( + block.x is not None and block.y is not None + for block in self.blocks + ) + + +class ProductAgentResult(BaseModel): + """保存模型輸出、搜尋結果與工具錯誤。""" + + output: dict[str, Any] + tool_results: list[dict[str, Any]] + tool_errors: list[str] + +class DetectionResult(BaseModel): + """商品頁來源驗證與版型推定結果。""" + + is_marketplace: bool + layout: MarketplaceLayout + confidence: float + evidence: list[str] = Field(default_factory=list) + reason: str | None = None + + +class PriceCandidate(BaseModel): + """從 OCR 文字擷取的單一價格候選及其判定資訊。""" + + amount: int + currency: str + source_text: str + block_index: int + x: float | None + y: float | None + context_before: str | None + context_after: str | None + section: PriceSection + confidence: float + reject_reason: str | None = None + + +class MainPriceExtractionResult(BaseModel): + """保存 Marketplace 刊登欄位的抽取結果。""" + + price: int | None + currency: str | None + confidence: float + source_text: str | None + layout: MarketplaceLayout + candidates: list[PriceCandidate] = Field(default_factory=list) + rejected_candidates: list[PriceCandidate] = Field(default_factory=list) + error_code: str | None = None + message: str | None = None + reason: str | None = None + product_name: str | None = None + seller_name: str | None = None + condition: MarketplaceCondition = MarketplaceCondition.UNKNOWN + condition_detail: str = "" + condition_source_text: str = "" + condition_extraction_confidence: float = Field( + default=0.0, + ge=0.0, + le=1.0, + description=( + "只代表 extractor 擷取商品狀態的可信度,不代表商品匹配、" + "市場資料或價格風險可信度" + ), + ) + warnings: list[str] = Field(default_factory=list) + + @model_validator(mode="after") + def validate_supported_listing_price(self) -> "MainPriceExtractionResult": + """將超出服務上限的刊登價格標記為明確的擷取錯誤。""" + if ( + self.price is not None + and self.price + > DEFAULT_PRICE_RISK_POLICY.maximum_supported_price + ): + self.error_code = "PRICE_OUT_OF_SUPPORTED_RANGE" + self.message = "刊登價格超出商品價格驗證服務支援範圍" + return self + +class MainPriceExtractionError(ValueError): + """表示主價格不存在或擷取信心不足。""" + + def __init__(self, result: MainPriceExtractionResult): + """建立錯誤並保留原始擷取結果。""" + if not result.error_code: + raise ValueError("MainPriceExtractionError 必須包含 error_code") + self.result = result + self.error_code = result.error_code + super().__init__(result.message or result.error_code) diff --git a/backend/services/image_price_service/domain/policy/__init__.py b/backend/services/image_price_service/domain/policy/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..a7a50a37de5b573fc72ce73564d5bbdee9e263d7 --- /dev/null +++ b/backend/services/image_price_service/domain/policy/__init__.py @@ -0,0 +1,11 @@ +"""商品價格風險的 domain policy。""" + +from .price_risk_policy import ( + DEFAULT_PRICE_RISK_POLICY, + PriceRiskPolicy, +) + +__all__ = [ + "DEFAULT_PRICE_RISK_POLICY", + "PriceRiskPolicy", +] diff --git a/backend/services/image_price_service/domain/policy/price_risk_policy.py b/backend/services/image_price_service/domain/policy/price_risk_policy.py new file mode 100644 index 0000000000000000000000000000000000000000..b45bb375a095f96c01d5ab2947c193b9353720fd --- /dev/null +++ b/backend/services/image_price_service/domain/policy/price_risk_policy.py @@ -0,0 +1,159 @@ +"""集中定義商品價格風險使用的所有數字門檻。""" + +from math import isinf, isnan + +from pydantic import BaseModel, ConfigDict, Field, model_validator + + +class PriceRiskPolicy(BaseModel): + """不可變且不依賴環境設定的價格風險數字門檻。""" + + model_config = ConfigDict(frozen=True, extra="forbid") + + underprice_relative_bands: tuple[tuple[float, int], ...] + overprice_relative_bands: tuple[tuple[float, int], ...] + absolute_gap_bands: tuple[tuple[int, int], ...] + maximum_supported_price: int = Field(gt=0) + low_score_max: int = Field(ge=0, le=100) + medium_score_max: int = Field(ge=0, le=100) + maximum_score: int = Field(ge=0, le=100) + overprice_score_cap: int = Field(ge=0, le=100) + minimum_market_confidence: float = Field(ge=0.0, le=1.0) + minimum_market_samples: int = Field(ge=3) + minimum_market_sites: int = Field(ge=1) + minimum_iqr_samples: int = Field(ge=5) + preflight_multiplier: int = Field(default=2, ge=1) + small_sample_relative_tolerance: float = Field(gt=0.0, lt=1.0) + small_sample_score_cap: int = Field(ge=0, le=100) + llm_review_min_confidence: float = Field(ge=0.0, le=1.0) + condition_llm_correction_max_confidence: float = Field( + ge=0.0, + le=1.0, + ) + + @staticmethod + def _validate_relative_bands( + bands: tuple[tuple[float, int], ...], + band_name: str, + maximum_score: int, + ) -> None: + if not bands: + raise ValueError(f"{band_name}不得為空") + + previous_threshold = 0.0 + for index, (threshold, score) in enumerate(bands): + if isnan(threshold) or threshold <= previous_threshold: + raise ValueError(f"{band_name}門檻必須嚴格遞增且大於零") + if isinf(threshold) and index != len(bands) - 1: + raise ValueError(f"{band_name}只有最後一個門檻可以是無限大") + if not 0 <= score <= maximum_score: + raise ValueError( + f"{band_name}分數必須介於 0 與 maximum_score 之間" + ) + previous_threshold = threshold + + @staticmethod + def _validate_absolute_gap_bands( + bands: tuple[tuple[int, int], ...], + maximum_supported_price: int, + maximum_score: int, + ) -> None: + if not bands: + raise ValueError("絕對差額級距不得為空") + + previous_threshold = -1 + for threshold, score in bands: + if threshold <= previous_threshold: + raise ValueError("絕對差額級距門檻必須嚴格遞增") + if not 0 <= score <= maximum_score: + raise ValueError( + "絕對差額級距分數必須介於 0 與 maximum_score 之間" + ) + previous_threshold = threshold + + if bands[-1][0] != maximum_supported_price: + raise ValueError( + "絕對差額最後一個門檻必須等於 maximum_supported_price" + ) + + @model_validator(mode="after") + def validate_policy(self) -> "PriceRiskPolicy": + """驗證所有跨欄位門檻關係。""" + if not ( + self.low_score_max + < self.medium_score_max + < self.maximum_score + ): + raise ValueError( + "風險分數邊界必須符合 LOW 小於 MEDIUM 小於 maximum" + ) + + if self.overprice_score_cap > self.maximum_score: + raise ValueError("高於行情的分數上限不得超過最高分") + + if self.minimum_iqr_samples < self.minimum_market_samples: + raise ValueError("IQR 最低樣本數不得少於市場最低樣本數") + + if self.small_sample_score_cap > self.medium_score_max: + raise ValueError("小樣本分數上限不得超過 MEDIUM") + + self._validate_relative_bands( + self.underprice_relative_bands, + "低於行情相對差距級距", + self.maximum_score, + ) + self._validate_relative_bands( + self.overprice_relative_bands, + "高於行情相對差距級距", + self.maximum_score, + ) + self._validate_absolute_gap_bands( + self.absolute_gap_bands, + self.maximum_supported_price, + self.maximum_score, + ) + return self + + +DEFAULT_PRICE_RISK_POLICY = PriceRiskPolicy( + underprice_relative_bands=( + (0.10, 10), + (0.20, 25), + (0.35, 45), + (0.50, 65), + (float("inf"), 75), + ), + overprice_relative_bands=( + (0.15, 10), + (0.30, 25), + (0.60, 45), + (1.00, 65), + (float("inf"), 75), + ), + absolute_gap_bands=( + (499, 5), + (1_999, 10), + (4_999, 20), + (9_999, 30), + (100_000, 50), + ), + maximum_supported_price=100_000, + low_score_max=39, + medium_score_max=79, + maximum_score=100, + overprice_score_cap=80, + minimum_market_confidence=0.60, + minimum_market_samples=3, + minimum_market_sites=1, + minimum_iqr_samples=5, + preflight_multiplier=2, + small_sample_relative_tolerance=0.25, + small_sample_score_cap=79, + llm_review_min_confidence=0.80, + condition_llm_correction_max_confidence=0.80, +) + +__all__ = [ + "DEFAULT_PRICE_RISK_POLICY", + "PriceRiskPolicy", +] diff --git a/backend/services/image_price_service/image_price_analyzer.py b/backend/services/image_price_service/image_price_analyzer.py new file mode 100644 index 0000000000000000000000000000000000000000..dcb912123a6bdc71a5a02a070221a55f506907b1 --- /dev/null +++ b/backend/services/image_price_service/image_price_analyzer.py @@ -0,0 +1,398 @@ +"""協調商品圖片的來源驗證、價格抽取與風險分析流程。""" + +import logging +import time +from concurrent.futures import ThreadPoolExecutor +from typing import Any, cast + +from backend.core.config import settings +from backend.repository.case_repository import case_repository +from backend.services.dto.price_analysis import (ImagePriceAnalysisResult, + MarketPriceEstimate, + SearchTool) +from backend.services.image_price_service.case_recorder import record_case +from backend.services.image_price_service.domain.models import ( + MainPriceExtractionError, MainPriceExtractionResult, MarketplaceCondition, + MarketplaceLayout) +from backend.services.image_price_service.domain.policy import \ + DEFAULT_PRICE_RISK_POLICY +from backend.services.image_price_service.ocr.ocr_service import \ + ocr_service as default_ocr_service +from backend.services.image_price_service.page_analysis.fb_marketplace.fb_marketplace_detector import \ + fb_marketplace_detector +from backend.services.image_price_service.page_analysis.fb_marketplace.fb_marketplace_extractor import \ + fb_marketplace_price_extractor +from backend.services.image_price_service.pricing.market_price_resolver import \ + resolve_market_price +from backend.services.image_price_service.pricing.online_marketprice_service import \ + OnlineMarketPriceService +from backend.services.image_price_service.product.product_identifier import \ + product_identifier as default_product_identifier +from backend.services.image_price_service.risk.condition_reviewer import \ + ConditionReviewer +from backend.services.image_price_service.risk.fusion_decision_engine import \ + FusionDecisionEngine + +logger = logging.getLogger(__name__) + + +def _log_elapsed(stage: str, started: float) -> float: + """記錄單一階段耗時,並回傳下一階段的起始時間。""" + now = time.perf_counter() + logger.info("圖片查價階段耗時 階段=%s 耗時=%.2f 秒", stage, now - started) + return now + +default_online_price_service = OnlineMarketPriceService( + policy=DEFAULT_PRICE_RISK_POLICY, +) +default_decision_engine = FusionDecisionEngine( + policy=DEFAULT_PRICE_RISK_POLICY, + condition_reviewer=ConditionReviewer( + model_name=settings.REVIEW_MODEL, + ), +) + + +class ImagePriceAnalyzer: + """協調圖片 OCR、商品查價與風險判定流程。""" + + def __init__( + self, + *, + ocr_service: Any | None = None, + product_identifier: Any | None = None, + online_price_service: Any | None = None, + decision_engine: Any | None = None, + case_repo: Any | None = None, + price_extractor: Any | None = None, + marketplace_detector: Any | None = None, + ) -> None: + """建立分析器並注入各階段使用的服務。""" + self._ocr_service = ( + ocr_service if ocr_service is not None else default_ocr_service + ) + self._product_identifier = ( + product_identifier + if product_identifier is not None + else default_product_identifier + ) + self._online_price_service = ( + online_price_service + if online_price_service is not None + else default_online_price_service + ) + self._decision_engine = ( + decision_engine + if decision_engine is not None + else default_decision_engine + ) + self._case_repo = case_repo if case_repo is not None else case_repository + self._price_extractor = ( + price_extractor + if price_extractor is not None + else fb_marketplace_price_extractor + ) + self._marketplace_detector = ( + marketplace_detector + if marketplace_detector is not None + else fb_marketplace_detector + ) + + @staticmethod + def _error_result( + *, + filename: str, + content_type: str, + text: str, + error_code: str, + message: str, + layout: MarketplaceLayout | Any, + evidence: list[str], + marketplace_confidence: float = 0.0, + extraction: MainPriceExtractionResult | None = None + ) -> ImagePriceAnalysisResult: + """建立圖片分析失敗時的統一回應。""" + return ImagePriceAnalysisResult( + filename=filename or "unknown", + content_type=content_type, + success=False, + error_code=error_code, + message=message, + extracted_text=text, + product_name=None, + brand_model=None, + listed_price=None, + market_price=0, + market_price_source="not_evaluated", + risk_label="UNKNOWN", + score="未知", + reason=message, + confidence=0.0, + evidence=evidence, + decision_layer="decision_error", + search_tools=[], + marketplace_layout=layout, + marketplace_confidence=marketplace_confidence, + extraction_confidence=extraction.confidence if extraction else None, + price_source_text=extraction.source_text if extraction else None, + price_extraction_reason=extraction.reason if extraction else None, + seller_name=extraction.seller_name if extraction else None, + condition=( + extraction.condition + if extraction + else MarketplaceCondition.UNKNOWN + ), + condition_detail=extraction.condition_detail if extraction else "", + condition_source_text=( + extraction.condition_source_text if extraction else "" + ), + condition_extraction_confidence=( + extraction.condition_extraction_confidence + if extraction + else 0.0 + ), + extraction_warnings=extraction.warnings if extraction else [], + ) + + @staticmethod + def _without_market_candidates( + estimates: tuple[MarketPriceEstimate, ...], + ) -> tuple[MarketPriceEstimate, ...]: + """將候選明細留在查價服務邊界內。""" + return tuple( + estimate.model_copy(update={"candidates": ()}) + for estimate in estimates + ) + + @staticmethod + def _search_tools( + estimates: tuple[MarketPriceEstimate, ...], + ) -> list[SearchTool]: + """依實際呼叫順序彙整查價使用過的搜尋工具。""" + search_tools: list[SearchTool] = [] + for estimate in estimates: + for search_tool in estimate.search_tools: + if search_tool not in search_tools: + search_tools.append(search_tool) + return search_tools + + @staticmethod + def _reference_market_price( + estimates: tuple[MarketPriceEstimate, ...], + ) -> int: + """取得單一成功市場區間的中位數,供介面顯示參考。""" + if len(estimates) != 1 or estimates[0].status != "success": + return 0 + return estimates[0].median_price + + def image_price_detector( + self, + data: bytes, + filename: str = "unknown", + content_type: str = "", + ) -> ImagePriceAnalysisResult: + """分析 Marketplace 截圖並回傳價格風險結果。""" + request_started = stage_started = time.perf_counter() + document = self._ocr_service.extract_document(data) + stage_started = _log_elapsed("OCR", stage_started) + text = document.text + detection = self._marketplace_detector.detect(document) + if not detection.is_marketplace: + error_code = ( + "UNKNOWN_LAYOUT" + if detection.confidence >= self._marketplace_detector.threshold + and detection.layout == "unknown" + else "INVALID_IMAGE_SOURCE" + ) + message = ( + "無法辨識 FB Marketplace 商品頁版型" + if error_code == "UNKNOWN_LAYOUT" + else "圖片格式錯誤,來源需為 FB Marketplace 商品頁截圖" + ) + return self._error_result( + filename=filename, + content_type=content_type, + text=text, + error_code=error_code, + message=message, + layout=detection.layout, + marketplace_confidence=detection.confidence, + evidence=detection.evidence, + ) + + try: + extraction = self._price_extractor.extract(document, detection) + # 相容以回傳值表示失敗、而非直接拋出例外的舊版或自訂抽取器。 + if extraction.price is None or extraction.error_code: + extraction.error_code = extraction.error_code or "MAIN_PRICE_NOT_FOUND" + extraction.message = extraction.message or "找不到 FB Marketplace 商品主價格" + raise MainPriceExtractionError(extraction) + except MainPriceExtractionError as e: + extraction = e.result + logger.warning( + "主價格抽取失敗 error_code=%s confidence=%s candidates=%s rejected=%s", + e.error_code, + extraction.confidence, + [ + { + "amount": item.amount, + "source_text": item.source_text, + "confidence": item.confidence, + } + for item in extraction.candidates + ], + [ + { + "amount": item.amount, + "source_text": item.source_text, + "section": item.section, + "confidence": item.confidence, + "reject_reason": item.reject_reason, + } + for item in extraction.rejected_candidates + ], + ) + return self._error_result( + filename=filename, + content_type=content_type, + text=text, + error_code=e.error_code, + message=str(e), + layout=detection.layout, + marketplace_confidence=detection.confidence, + evidence=detection.evidence, + extraction=extraction, + ) + + if not extraction.product_name: + extraction.error_code = "LOW_CONFIDENCE_PRODUCT_EXTRACTION" + extraction.message = "找不到商品主價格上方的 Marketplace 商品標題" + return self._error_result( + filename=filename, + content_type=content_type, + text=text, + error_code=extraction.error_code, + message=extraction.message, + layout=detection.layout, + marketplace_confidence=detection.confidence, + evidence=detection.evidence, + extraction=extraction, + ) + + product_name = extraction.product_name + # 商品正規化與品況複核都只依賴 OCR 抽取結果,彼此獨立,並行呼叫以縮短等待 + with ThreadPoolExecutor(max_workers=2) as executor: + product_future = executor.submit( + self._product_identifier.identify, + product_name, + ) + review_future = executor.submit( + self._decision_engine.review_condition, + product_name=product_name, + text=text, + condition=extraction.condition, + condition_detail=extraction.condition_detail, + condition_source_text=extraction.condition_source_text, + condition_extraction_confidence=extraction.condition_extraction_confidence, + ) + product = product_future.result() + condition_review = review_future.result() + stage_started = _log_elapsed("商品正規化與品況複核", stage_started) + + market_estimates = resolve_market_price( + self._online_price_service, + product_name, + product.brand_model, + product.market_price, + product.search_query, + condition=condition_review.condition, + condition_text=condition_review.condition_detail, + ) + search_tools = self._search_tools(market_estimates) + active_market_estimates = self._without_market_candidates(market_estimates) + stage_started = _log_elapsed("市場查價", stage_started) + + selling_price = cast(int, extraction.price) + decision = self._decision_engine.evaluate( + product_name=product_name, + selling_price=selling_price, + market_estimates=active_market_estimates, + condition=condition_review.condition, + condition_detail=condition_review.condition_detail, + condition_review=condition_review, + ) + decision_error_code = decision.get("error_code") + if not isinstance(decision_error_code, str): + decision_error_code = None + + decision_condition = decision.get("condition", condition_review.condition) + if isinstance(decision_condition, MarketplaceCondition): + resolved_condition = decision_condition + elif isinstance(decision_condition, str): + try: + resolved_condition = MarketplaceCondition(decision_condition) + except ValueError: + resolved_condition = condition_review.condition + else: + resolved_condition = condition_review.condition + resolved_condition_detail = str( + decision.get( + "condition_detail", + condition_review.condition_detail, + ) + or "" + ) + market_price = self._reference_market_price( + active_market_estimates, + ) + decision_evidence = decision.get("evidence") + evidence = ( + [str(item) for item in decision_evidence] + if isinstance(decision_evidence, list) + else [] + ) + result = ImagePriceAnalysisResult( + filename=filename or "unknown", + content_type=content_type, + extracted_text=text, + success=True, + error_code=decision_error_code, + message=None, + product_name=product_name, + brand_model=product.brand_model, + listed_price=selling_price, + market_price=market_price, + market_price_source=decision.get( + "market_price_source", + "not_evaluated", + ), + market_price_estimates=active_market_estimates, + risk_label=decision.get("risk_label", "UNKNOWN"), + score=decision.get("risk_score"), + reason=str(decision.get("reason") or ""), + evidence=evidence, + confidence=float(decision.get("confidence") or 0.0), + decision_layer=decision.get("decision_layer", "fast"), + search_tools=search_tools, + marketplace_layout=detection.layout, + marketplace_confidence=detection.confidence, + extraction_confidence=extraction.confidence, + price_source_text=extraction.source_text, + price_extraction_reason=extraction.reason, + seller_name=extraction.seller_name, + condition=resolved_condition, + condition_detail=resolved_condition_detail, + condition_source_text=extraction.condition_source_text, + condition_extraction_confidence=( + extraction.condition_extraction_confidence + ), + extraction_warnings=extraction.warnings, + ) + + record_case(self._case_repo, result) + _log_elapsed("風險決策與紀錄", stage_started) + _log_elapsed("總計", request_started) + return result + + +image_price_analyzer = ImagePriceAnalyzer() diff --git a/backend/services/image_price_service/ocr/__init__.py b/backend/services/image_price_service/ocr/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..a16c9a1a01b7c2fca53339a03046a1ab2e31f2fb --- /dev/null +++ b/backend/services/image_price_service/ocr/__init__.py @@ -0,0 +1 @@ +"""提供商品圖片的 OCR 與版面文字排序功能。""" diff --git a/backend/services/image_price_service/ocr/ocr_service.py b/backend/services/image_price_service/ocr/ocr_service.py new file mode 100644 index 0000000000000000000000000000000000000000..feb2cf7395c5938af1fb20520f3816360b6aac58 --- /dev/null +++ b/backend/services/image_price_service/ocr/ocr_service.py @@ -0,0 +1,165 @@ +"""將商品圖片送往 Google Cloud Vision,並整理 OCR 文字與版面資訊。""" + +import json +import os +import re +import threading +import unicodedata +from typing import Any, cast + +from google.oauth2 import service_account + +from backend.core.config import settings +from backend.services.image_price_service.domain.models import (OCRDocument, + OCRTextBlock) + + +class OCRService: + """透過 Google Vision 擷取文字與版面資訊。""" + + def __init__(self) -> None: + # gRPC client 可跨執行緒共用;重複建立會重新解析憑證並重建連線,每次請求多花數百毫秒 + self._client: Any | None = None + self._client_lock = threading.Lock() + + def _get_client(self) -> Any: + """延遲建立並重用 Google Vision client。""" + if self._client is not None: + return self._client + with self._client_lock: + if self._client is None: + from google.cloud import vision + + key_json_str = ( + settings.GCP_OCR_SERVICE_ACCOUNT_JSON + or os.getenv("GCP_OCR_SERVICE_ACCOUNT_JSON") + ) + key_info = json.loads(key_json_str) if key_json_str else None + credentials = service_account.Credentials.from_service_account_info(key_info) + self._client = vision.ImageAnnotatorClient(credentials=credentials) + return self._client + + @staticmethod + def _clean_ocr_text(text: str, *, preserve_lines: bool = False) -> str: + """整理 OCR 空白與常見英數辨識錯字。""" + text = unicodedata.normalize("NFKC", text) + text = re.sub(r"[\r\t]+", " ", text) + if preserve_lines: + text = "\n".join( + re.sub(r"\s+", " ", line).strip() + for line in text.splitlines() + if line.strip() + ) + else: + text = re.sub(r"\s+", " ", text).strip() + + # 修正商品名稱與型號中常見的英數字元誤判。 + replacements = { + "iph0ne": "iphone", + "ga1axy": "galaxy", + "airp0ds": "airpods", + "rnacbook": "macbook", + } + cleaned = text + for src, dst in replacements.items(): + cleaned = re.sub(src, dst, cleaned, flags=re.IGNORECASE) + return cleaned + + @staticmethod + def _extract_paragraph_text(paragraph) -> str: + """依符號與單字順序組合 Google Vision 段落文字。""" + words: list[str] = [] + for word in paragraph.words: + word_text = "".join(symbol.text for symbol in word.symbols) + if word_text: + words.append(word_text) + return " ".join(words).strip() + + @staticmethod + def _bbox_from_vertices(vertices) -> tuple[float, float, float, float]: + """將邊界框頂點換算為最小與最大座標。""" + xs = [float(v.x or 0) for v in vertices] + ys = [float(v.y or 0) for v in vertices] + return min(xs), min(ys), max(xs), max(ys) + + def extract_document(self, data: bytes) -> OCRDocument: + """從圖片建立含版面資訊的 OCR 文件。""" + provider = settings.OCR_PROVIDER.strip().lower() + if provider not in {"google", "google_vision", "gcv"}: + raise RuntimeError("OCR_PROVIDER 僅支援 google_vision") + + return self._extract_document_with_google_vision(data) + + def _extract_document_with_google_vision(self, data: bytes) -> OCRDocument: + """呼叫 Google Vision 並整理 OCR 文件。""" + try: + from google.cloud import vision + except ImportError as e: + raise RuntimeError("Google Cloud Vision 套件未安裝,請安裝 google-cloud-vision") from e + + try: + client = self._get_client() + image = vision.Image(content=data) + language_hints = [h.strip() for h in settings.GCV_LANGUAGE_HINTS.split(",") if h.strip()] + image_context = vision.ImageContext(language_hints=language_hints) + + # 文件文字偵測能保留段落結構,較適合包含多行刊登資訊的截圖。 + doc_response = cast(Any, client).document_text_detection(image=image, image_context=image_context) + if doc_response.error.message: + raise RuntimeError(f"Google Vision OCR 錯誤: {doc_response.error.message}") + doc_text = (doc_response.full_text_annotation.text or "").strip() + if doc_text: + pages = doc_response.full_text_annotation.pages + blocks: list[OCRTextBlock] = [] + page_width = 0.0 + page_height = 0.0 + + for page in pages: + page_width = max(page_width, float(page.width or 0)) + page_height = max(page_height, float(page.height or 0)) + for block in page.blocks: + for paragraph in block.paragraphs: + paragraph_text = self._extract_paragraph_text(paragraph) + if not paragraph_text: + continue + x0, y0, x1, y1 = self._bbox_from_vertices( + paragraph.bounding_box.vertices + ) + confidence = getattr(paragraph, "confidence", None) + blocks.append(OCRTextBlock( + text=self._clean_ocr_text(paragraph_text), + x=x0, + y=y0, + width=x1 - x0, + height=y1 - y0, + confidence=float(confidence) if confidence is not None else None, + )) + + blocks.sort(key=lambda item: ( + item.y if item.y is not None else float("inf"), + item.x if item.x is not None else float("inf"), + )) + return OCRDocument( + text=self._clean_ocr_text(doc_text, preserve_lines=True), + blocks=blocks, + width=page_width or None, + height=page_height or None, + ) + + response = cast(Any, client).text_detection(image=image, image_context=image_context) + except Exception as exc: + raise RuntimeError(f"Google Vision OCR 執行失敗: {exc}") from exc + + if response.error.message: + raise RuntimeError(f"Google Vision OCR 錯誤: {response.error.message}") + + if not response.text_annotations: + return OCRDocument(text="") + + return OCRDocument( + text=self._clean_ocr_text( + response.text_annotations[0].description, + preserve_lines=True, + ) + ) +ocr_service = OCRService() diff --git a/backend/services/image_price_service/ocr/ocr_text_utils.py b/backend/services/image_price_service/ocr/ocr_text_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..8ecd6987d9f08241b1c9c2a4d4351fbc713a3de4 --- /dev/null +++ b/backend/services/image_price_service/ocr/ocr_text_utils.py @@ -0,0 +1,97 @@ +"""OCR 文字區塊與座標處理的共用函式。""" + +import re + +from backend.services.image_price_service.domain.models import ( + OCRDocument, + OCRTextBlock, +) + + +def expand_blocks(document: OCRDocument) -> list[OCRTextBlock]: + """將 OCR 內容整理成單行文字區塊。""" + if document.blocks: + expanded: list[OCRTextBlock] = [] + for block in document.blocks: + lines = [ + line.strip() + for line in block.text.splitlines() + if line.strip() + ] + if len(lines) <= 1: + expanded.append(block) + continue + for line in lines: + expanded.append( + OCRTextBlock( + text=line, + x=block.x, + y=block.y, + width=block.width, + height=block.height, + confidence=block.confidence, + ) + ) + if expanded: + return expanded + return [ + OCRTextBlock(text=line.strip()) + for line in document.text.splitlines() + if line.strip() + ] + + +def normalized_lines(document: OCRDocument) -> list[str]: + """回傳移除多餘空白與空行的 OCR 全文行。""" + return [ + re.sub(r"\s+", " ", line).strip() + for line in document.text.splitlines() + if line.strip() + ] + + +def normalize_display_text(text: str) -> str: + """整理顯示文字中的空白、斜線與連字號。""" + text = re.sub(r"\s+", " ", text).strip(" ·|\t\r\n") + text = re.sub(r"\s*/\s*", "/", text) + text = re.sub(r"\s*-\s*", "-", text) + text = re.sub(r"\s+([,,.。;;::!?!?)])", r"\1", text) + text = re.sub(r"([((])\s+", r"\1", text) + return text + + +def are_adjacent_title_lines( + document: OCRDocument, + earlier: OCRTextBlock, + later: OCRTextBlock, +) -> bool: + """判斷兩個 OCR 文字區塊是否為相鄰標題行。""" + if ( + earlier.x is None + or earlier.y is None + or later.x is None + or later.y is None + ): + return True + + horizontal_tolerance = max( + earlier.width or 0.0, + later.width or 0.0, + 120.0, + ) + if abs(earlier.x - later.x) > horizontal_tolerance: + return False + + if earlier.y > later.y + max(later.height or 0.0, 8.0): + return False + + vertical_gap = later.y - ( + earlier.y + (earlier.height or 0.0) + ) + maximum_gap = max( + (document.height or 0.0) * 0.04, + (earlier.height or 0.0) * 2, + (later.height or 0.0) * 2, + 36.0, + ) + return vertical_gap <= maximum_gap diff --git a/backend/services/image_price_service/page_analysis/__init__.py b/backend/services/image_price_service/page_analysis/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e48d4b7b307533b4169c9c76964fa475a847946c --- /dev/null +++ b/backend/services/image_price_service/page_analysis/__init__.py @@ -0,0 +1 @@ +"""Marketplace 頁面偵測與商品資訊抽取。""" diff --git a/backend/services/image_price_service/page_analysis/common/__init__.py b/backend/services/image_price_service/page_analysis/common/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..816afea720c8432e482b99c1a01f999825a9ad2b --- /dev/null +++ b/backend/services/image_price_service/page_analysis/common/__init__.py @@ -0,0 +1 @@ +"""跨頁面分析共用元件。""" diff --git a/backend/services/image_price_service/page_analysis/common/price_candidate_selector.py b/backend/services/image_price_service/page_analysis/common/price_candidate_selector.py new file mode 100644 index 0000000000000000000000000000000000000000..09dcb76d2180e5a974ecefcf9d28b82eed006c5a --- /dev/null +++ b/backend/services/image_price_service/page_analysis/common/price_candidate_selector.py @@ -0,0 +1,118 @@ +"""價格候選資料與跨頁面分析的主價格選擇邏輯。""" + +import re +from dataclasses import dataclass, field + +from backend.services.image_price_service.domain.models import ( + OCRDocument, + OCRTextBlock, + PriceCandidate, +) + + +@dataclass +class CandidateCollection: + """保存價格候選及選擇主價格所需的附加資訊。""" + + candidates: list[PriceCandidate] = field(default_factory=list) + rejected: list[PriceCandidate] = field(default_factory=list) + titles: dict[int, str] = field(default_factory=dict) + positions: dict[ + int, + tuple[float, float | None, float | None], + ] = field(default_factory=dict) + + +def candidate_horizontal_position( + block: OCRTextBlock, + text: str, + match: re.Match[str], +) -> float: + """計算同列多價格比較時使用的水平位置。""" + if block.x is None: + return float(match.start()) + horizontal_position = block.x + if block.width and text: + horizontal_position += ( + block.width * match.start() / len(text) + ) + return horizontal_position + + +class PriceCandidateSelector: + """依信心與同列最左價格規則選出商品主價格。""" + + def __init__(self, minimum_confidence: float) -> None: + self.minimum_confidence = minimum_confidence + + @staticmethod + def _same_row_candidates( + document: OCRDocument, + selected: PriceCandidate, + accepted: list[PriceCandidate], + positions: dict[ + int, + tuple[float, float | None, float | None], + ], + ) -> list[PriceCandidate]: + """找出與最高信心候選位於同一列的所有價格。""" + _, selected_y, selected_height = positions[id(selected)] + same_row: list[PriceCandidate] = [] + for candidate in accepted: + _, candidate_y, candidate_height = positions[id(candidate)] + if candidate.block_index == selected.block_index: + same_row.append(candidate) + continue + if selected_y is None or candidate_y is None: + continue + row_tolerance = max( + selected_height or 0.0, + candidate_height or 0.0, + (document.height or 0.0) * 0.01, + 8.0, + ) + if abs(candidate_y - selected_y) <= row_tolerance: + same_row.append(candidate) + return same_row + + def select( + self, + document: OCRDocument, + collection: CandidateCollection, + *, + minimum_confidence: float | None = None, + ) -> tuple[PriceCandidate | None, bool]: + """回傳主價格候選及是否同列存在多個價格。""" + threshold = ( + self.minimum_confidence + if minimum_confidence is None + else minimum_confidence + ) + accepted = [ + candidate + for candidate in collection.candidates + if candidate.confidence >= threshold + ] + if not accepted: + return None, False + + accepted.sort( + key=lambda item: (-item.confidence, item.block_index) + ) + selected = accepted[0] + same_row = self._same_row_candidates( + document, + selected, + accepted, + collection.positions, + ) + has_multiple_prices_on_row = len(same_row) > 1 + if has_multiple_prices_on_row: + selected = min( + same_row, + key=lambda item: ( + collection.positions[id(item)][0], + item.block_index, + ), + ) + return selected, has_multiple_prices_on_row diff --git a/backend/services/image_price_service/page_analysis/fb_marketplace/__init__.py b/backend/services/image_price_service/page_analysis/fb_marketplace/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..ac4b431a107f5ccb042dedbb038370f96a85c058 --- /dev/null +++ b/backend/services/image_price_service/page_analysis/fb_marketplace/__init__.py @@ -0,0 +1 @@ +"""Facebook Marketplace 頁面分析元件。""" diff --git a/backend/services/image_price_service/page_analysis/fb_marketplace/fb_marketplace_detector.py b/backend/services/image_price_service/page_analysis/fb_marketplace/fb_marketplace_detector.py new file mode 100644 index 0000000000000000000000000000000000000000..42ddf440a150457101413f87b18902f85ad839bc --- /dev/null +++ b/backend/services/image_price_service/page_analysis/fb_marketplace/fb_marketplace_detector.py @@ -0,0 +1,143 @@ +"""驗證 OCR 內容是否具備 Facebook Marketplace 商品頁的必要欄位。""" + +import re + +from backend.services.image_price_service.domain.models import ( + DetectionResult, + MarketplaceLayout, + OCRDocument, +) + +_PRICE_RE = re.compile( + r"(?:NT\s*\$|NTD|TWD|台幣|\$)\s*[1-9]\d{0,6}(?:\s*[,,]\s*\d{3})*", + flags=re.IGNORECASE, +) + + +class FBMarketplaceDetector: + """驗證 Marketplace 商品頁並判斷版型。""" + + threshold = 0.60 + + @staticmethod + def _compact_line(text: str) -> str: + """移除空白及常見區段標題裝飾字元。""" + return re.sub(r"\s+", "", text).strip("·::>›|") + + @staticmethod + def _has_title_near_price(document: OCRDocument) -> bool: + """檢查價格同一行或前兩行是否存在可能的商品標題。""" + lines = [line.strip() for line in document.text.splitlines() if line.strip()] + for index, line in enumerate(lines): + match = _PRICE_RE.search(line) + if not match: + continue + + # OCR 可能將商品標題和價格合併在同一行或同一文字區塊。 + if len(line[:match.start()].strip()) >= 4: + return True + + for prior in lines[max(0, index - 2):index]: + if len(prior) >= 4 and not _PRICE_RE.search(prior): + return True + + for block in document.blocks: + match = _PRICE_RE.search(block.text) + if match and len(block.text[:match.start()].strip()) >= 4: + return True + return False + + @classmethod + def _basic_sections(cls, document: OCRDocument) -> tuple[bool, bool, bool]: + """檢查詳細資料、商品狀況與賣家資訊區段是否存在。""" + compact_lines = [ + cls._compact_line(line) + for line in document.text.splitlines() + if line.strip() + ] + has_detail = any(line in {"詳細內容", "詳細資料"} for line in compact_lines) + has_condition = any("狀況" in line for line in compact_lines) + has_seller = any( + line in {"賣家", "賣家資訊", "賣家詳細資料"} + for line in compact_lines + ) + return has_detail, has_condition, has_seller + + @staticmethod + def _infer_layout( + document: OCRDocument, + has_detail: bool, + has_seller: bool, + ) -> MarketplaceLayout: + """優先依頁面比例判斷版型,缺少尺寸時再使用區段名稱推定。""" + if document.width and document.height: + if document.width > document.height * 1.25: + return MarketplaceLayout.DESKTOP + if document.height > document.width * 1.15: + return MarketplaceLayout.MOBILE + + compact = re.sub(r"\s+", "", document.text) + if "詳細資料" in compact or "賣家資訊" in compact: + return MarketplaceLayout.DESKTOP + if has_detail or has_seller or "狀況" in compact: + return MarketplaceLayout.MOBILE + return MarketplaceLayout.UNKNOWN + + def detect(self, document: OCRDocument) -> DetectionResult: + """判斷 OCR 文件是否為 Marketplace 商品頁。""" + text = document.text.strip() + if not text: + return DetectionResult( + is_marketplace=False, + layout=MarketplaceLayout.UNKNOWN, + confidence=0.0, + reason="OCR 未辨識到足以分析的商品頁文字", + ) + + has_price = bool(_PRICE_RE.search(text)) + has_title = self._has_title_near_price(document) + has_detail, has_condition, has_seller = self._basic_sections(document) + has_structured_section = has_detail or has_condition or has_seller + layout = self._infer_layout(document, has_detail, has_seller) + + confidence = 0.0 + evidence: list[str] = [] + if has_title: + confidence += 0.25 + evidence.append("價格附近有商品標題") + if has_price: + confidence += 0.25 + evidence.append("偵測到商品價格") + if has_detail: + confidence += 0.15 + evidence.append("偵測到詳細資料區段") + if has_condition: + confidence += 0.15 + evidence.append("偵測到商品狀況") + if has_seller: + confidence += 0.15 + evidence.append("偵測到賣家資訊") + confidence = round(min(confidence, 1.0), 2) + + is_marketplace = ( + has_title + and has_price + and has_structured_section + and confidence >= self.threshold + ) + reason = ( + "商品標題、價格與基本詳細資訊足夠,可進行價格分析" + if is_marketplace + else "商品頁缺少標題、價格或詳細資料/狀況/賣家資訊" + ) + + return DetectionResult( + is_marketplace=is_marketplace, + layout=layout if is_marketplace else MarketplaceLayout.UNKNOWN, + confidence=confidence, + evidence=evidence, + reason=reason, + ) + + +fb_marketplace_detector = FBMarketplaceDetector() diff --git a/backend/services/image_price_service/page_analysis/fb_marketplace/fb_marketplace_extractor.py b/backend/services/image_price_service/page_analysis/fb_marketplace/fb_marketplace_extractor.py new file mode 100644 index 0000000000000000000000000000000000000000..318c7086684202d3968dedc83fc016f5c6111bf3 --- /dev/null +++ b/backend/services/image_price_service/page_analysis/fb_marketplace/fb_marketplace_extractor.py @@ -0,0 +1,408 @@ +"""抽取 Facebook Marketplace 刊登頁的商品資訊。""" + +import re +from typing import NoReturn + +from backend.services.image_price_service.domain.models import ( + DetectionResult, MainPriceExtractionError, MainPriceExtractionResult, + MarketplaceLayout, OCRDocument, OCRTextBlock, PriceCandidate, PriceSection) +from backend.services.image_price_service.ocr.ocr_text_utils import ( + expand_blocks, normalized_lines) +from backend.services.image_price_service.page_analysis.common.price_candidate_selector import ( + CandidateCollection, PriceCandidateSelector, candidate_horizontal_position) +from backend.services.image_price_service.page_analysis.fb_marketplace.fb_marketplace_rules import ( + NT_MAIN_PRICE_RE, OFFER_TERMS, PRICE_RE, FBMarketplaceRules) +from backend.services.image_price_service.page_analysis.fb_marketplace.layout_extractors import ( + DesktopLayoutExtractor, LayoutTitleExtractor, MobileLayoutExtractor) + + +class FBMarketplacePriceExtractor: + """協調 FB Marketplace 版型、規則及價格候選元件。""" + + minimum_confidence = 0.65 + + def __init__( + self, + *, + rules: FBMarketplaceRules | None = None, + mobile_layout: LayoutTitleExtractor | None = None, + desktop_layout: LayoutTitleExtractor | None = None, + candidate_selector: PriceCandidateSelector | None = None, + ) -> None: + """建立可替換版型策略與共用候選選擇器的抽取器。""" + self._rules: FBMarketplaceRules = ( + rules + if rules is not None + else FBMarketplaceRules() + ) + self._mobile_layout = ( + mobile_layout + or MobileLayoutExtractor(self._rules) + ) + self._desktop_layout = ( + desktop_layout + or DesktopLayoutExtractor(self._rules) + ) + self._candidate_selector = ( + candidate_selector + or PriceCandidateSelector(self.minimum_confidence) + ) + + def _collect_candidates( + self, + document: OCRDocument, + detection: DetectionResult, + blocks: list[OCRTextBlock], + layout_extractor: LayoutTitleExtractor, + ) -> CandidateCollection: + """依指定版型策略建立 FB Marketplace 價格候選。""" + collection = CandidateCollection() + current_section: PriceSection = PriceSection.UNKNOWN + + for index, block in enumerate(blocks): + text = block.text.strip() + if not text: + continue + line_section = self._rules.section_for( + text, + current_section, + ) + matches = list(PRICE_RE.finditer(text)) + is_range = any( + term in text.lower() + for term in OFFER_TERMS + ) + + for match in matches: + amount = int( + re.sub(r"\D", "", match.group("amount")) + ) + prior = ( + blocks[index - 1].text + if index > 0 + else None + ) + following = ( + blocks[index + 1].text + if index + 1 < len(blocks) + else None + ) + section = ( + PriceSection.OFFER_RANGE + if is_range + else line_section + ) + reject_reason = self._rules.initial_rejection( + match, + amount, + section, + is_range, + ) + title, title_text, title_distance = ( + layout_extractor.find_title( + document, + blocks, + index, + block, + text[:match.start()].strip(), + ) + ) + + confidence = 0.22 + if title is None and reject_reason is None: + reject_reason = ( + "價格附近找不到 Marketplace 商品標題" + ) + else: + confidence += ( + self._rules.title_confidence_bonus( + detection.layout, + title_distance, + ) + ) + + position_bonus, position_reject = ( + self._rules.position_score( + detection.layout, + document, + title, + block, + ) + ) + confidence += position_bonus + if ( + position_reject + and reject_reason is None + ): + reject_reason = position_reject + confidence += ( + 0.10 if document.has_coordinates else -0.03 + ) + confidence = max(0.0, min(confidence, 1.0)) + + candidate = PriceCandidate( + amount=amount, + currency="TWD", + source_text=( + f"NT${amount:,}" + if NT_MAIN_PRICE_RE.fullmatch( + match.group("currency") + ) + else match.group(0) + ), + block_index=index, + x=block.x, + y=block.y, + context_before=prior, + context_after=following, + section=( + PriceSection.MAIN_PRICE + if reject_reason is None + else section + ), + confidence=confidence, + reject_reason=reject_reason, + ) + if title_text: + collection.titles[id(candidate)] = title_text + collection.positions[id(candidate)] = ( + candidate_horizontal_position( + block, + text, + match, + ), + block.y, + block.height, + ) + target = ( + collection.rejected + if reject_reason + else collection.candidates + ) + target.append(candidate) + + # 出價範圍只套用於命中關鍵字的當行,避免英文「from」讓 + # 後續主價格持續被歸類為出價區段。 + if line_section != "offer_range": + current_section = line_section + + return collection + + def _extract_mobile_page( + self, + document: OCRDocument, + detection: DetectionResult, + blocks: list[OCRTextBlock], + lines: list[str], + ) -> MainPriceExtractionResult: + """抽取 mobile Marketplace 頁面商品資訊。""" + collection = self._collect_candidates( + document, + detection, + blocks, + self._mobile_layout, + ) + return self._build_result( + document, + detection, + lines, + collection, + ) + + def _extract_desktop_page( + self, + document: OCRDocument, + detection: DetectionResult, + blocks: list[OCRTextBlock], + lines: list[str], + ) -> MainPriceExtractionResult: + """抽取 desktop Marketplace 頁面商品資訊。""" + collection = self._collect_candidates( + document, + detection, + blocks, + self._desktop_layout, + ) + return self._build_result( + document, + detection, + lines, + collection, + ) + + def _build_result( + self, + document: OCRDocument, + detection: DetectionResult, + lines: list[str], + collection: CandidateCollection, + ) -> MainPriceExtractionResult: + """由候選資料組裝成功結果或主價格抽取錯誤。""" + seller_name, _ = self._rules.extract_seller_name(lines) + selected, has_multiple_prices_on_row = ( + self._candidate_selector.select( + document, + collection, + minimum_confidence=self.minimum_confidence, + ) + ) + + if selected is not None: + return self._build_success_result( + detection, + lines, + collection, + selected, + has_multiple_prices_on_row, + seller_name, + ) + self._raise_failure_result( + detection, + lines, + collection, + seller_name, + ) + + def _build_success_result( + self, + detection: DetectionResult, + lines: list[str], + collection: CandidateCollection, + selected: PriceCandidate, + has_multiple_prices_on_row: bool, + seller_name: str | None, + ) -> MainPriceExtractionResult: + """組裝成功的 Marketplace 商品資訊。""" + product_name = collection.titles.get(id(selected)) + ( + condition, + condition_detail, + condition_source_text, + condition_confidence, + warnings, + ) = ( + self._rules.extract_condition( + lines, + product_name, + ) + ) + self._rules.append_missing_required_field_warnings( + warnings, + product_name=product_name, + price=selected.amount, + ) + title_confidence = 0.95 if product_name else 0.0 + overall_confidence = min( + 1.0, + ( + selected.confidence * 0.60 + + title_confidence * 0.20 + + condition_confidence * 0.20 + ), + ) + if has_multiple_prices_on_row: + reason = "同列出現多個 NT$ 價格,採用最左側價格" + else: + reason = ( + "商品標題正下方的第一個 NT$ 單一價格" + if detection.layout == MarketplaceLayout.MOBILE + else "桌面版右側商品標題下方 NT$ 價格列" + ) + return MainPriceExtractionResult( + price=selected.amount, + currency=selected.currency, + confidence=overall_confidence, + source_text=selected.source_text, + layout=detection.layout, + candidates=collection.candidates, + rejected_candidates=collection.rejected, + reason=reason, + product_name=product_name, + seller_name=seller_name, + condition=condition, + condition_detail=condition_detail, + condition_source_text=condition_source_text, + condition_extraction_confidence=condition_confidence, + warnings=warnings, + ) + + def _raise_failure_result( + self, + detection: DetectionResult, + lines: list[str], + collection: CandidateCollection, + seller_name: str | None, + ) -> NoReturn: + """組裝失敗結果並中止後續商品分析。""" + ( + condition, + condition_detail, + condition_source_text, + condition_confidence, + warnings, + ) = ( + self._rules.extract_condition(lines) + ) + self._rules.append_missing_required_field_warnings( + warnings, + product_name=None, + price=None, + ) + low_confidence = bool(collection.candidates) + result = MainPriceExtractionResult( + price=None, + currency=None, + confidence=max( + ( + item.confidence + for item in collection.candidates + ), + default=0.0, + ), + source_text=None, + layout=detection.layout, + candidates=collection.candidates, + rejected_candidates=collection.rejected, + error_code=( + "LOW_CONFIDENCE_PRICE_EXTRACTION" + if low_confidence + else "MAIN_PRICE_NOT_FOUND" + ), + message=( + "商品主價格候選信心不足,未進行價格驗證" + if low_confidence + else "找不到 FB Marketplace 商品主價格" + ), + seller_name=seller_name, + condition=condition, + condition_detail=condition_detail, + condition_source_text=condition_source_text, + condition_extraction_confidence=condition_confidence, + warnings=warnings, + ) + raise MainPriceExtractionError(result) + + def extract( + self, + document: OCRDocument, + detection: DetectionResult, + ) -> MainPriceExtractionResult: + """依偵測版型分派 mobile 或 desktop 商品資訊抽取。""" + blocks = expand_blocks(document) + lines = normalized_lines(document) + if detection.layout == MarketplaceLayout.DESKTOP: + return self._extract_desktop_page( + document, + detection, + blocks, + lines, + ) + return self._extract_mobile_page( + document, + detection, + blocks, + lines, + ) + + +fb_marketplace_price_extractor = FBMarketplacePriceExtractor() diff --git a/backend/services/image_price_service/page_analysis/fb_marketplace/fb_marketplace_rules.py b/backend/services/image_price_service/page_analysis/fb_marketplace/fb_marketplace_rules.py new file mode 100644 index 0000000000000000000000000000000000000000..91a8942bf2b2c865ea3a9ee1650e221afafd0867 --- /dev/null +++ b/backend/services/image_price_service/page_analysis/fb_marketplace/fb_marketplace_rules.py @@ -0,0 +1,414 @@ +"""Facebook Marketplace 特有的文字、欄位與價格判斷規則。""" + +import re + +from backend.services.image_price_service.domain.models import ( + MarketplaceCondition, + MarketplaceLayout, + OCRDocument, + OCRTextBlock, + PriceSection, +) +from backend.services.image_price_service.domain.policy import ( + DEFAULT_PRICE_RISK_POLICY, +) +from backend.services.image_price_service.ocr.ocr_text_utils import ( + normalize_display_text, +) + +PRICE_RE = re.compile( + r"(?PNT\s*\$|NTD|TWD|台幣|\$)\s*" + r"(?P[1-9]\d{0,6}(?:\s*[,,]\s*\d{3})*)", + flags=re.IGNORECASE, +) +NT_MAIN_PRICE_RE = re.compile(r"^NT\s*\$$", flags=re.IGNORECASE) +OFFER_TERMS = ( + "offers from", + "offer from", + "from", + " to ", + "傳送出價", + "出價", + "議價", +) +DETAIL_TERMS = ("詳細內容", "詳細資料", "狀況", "描述", "說明") +SELLER_TERMS = ("賣家資訊", "賣家詳細資料", "賣家") +MESSAGE_TERMS = ( + "發送訊息給賣家", + "傳送訊息給賣家", + "傳訊息給賣家", + "留言", + "訊息輸入", +) +RECOMMENDATION_TERMS = ( + "推薦商品", + "相關商品", + "你可能也喜歡", + "更多商品", +) +LOCATION_TERMS = ("地點", "地圖", "大致位置") +UI_TERMS = ( + *OFFER_TERMS, + *DETAIL_TERMS, + *SELLER_TERMS, + *MESSAGE_TERMS, + *RECOMMENDATION_TERMS, + *LOCATION_TERMS, + "分享", + "儲存", + "公開面交", + "有存貨", +) +USED_TERMS = ( + "二手", + "2手", + "中古", + "近全新", + "使用過", + "已使用", + "良好使用", + "狀況良好", + "狀態良好", + "尚可", + "使用痕跡", + "無修無拆", +) +NEW_TERMS = ("全新", "未使用", "未拆封", "全新品") +USED_GRADE_RE = re.compile( + r"(?:[1-9](?:\.\d)?|10|[一二三四五六七八九十])成新", + flags=re.IGNORECASE, +) +MISSING_TITLE_WARNING = "找不到主價格上方的 Marketplace 商品標題" +MISSING_PRICE_WARNING = "找不到 Marketplace 商品價格" + + +class FBMarketplaceRules: + """集中管理 FB Marketplace 特有的欄位與候選規則。""" + + @staticmethod + def section_for( + text: str, + current: PriceSection, + ) -> PriceSection: + """依區段關鍵字更新目前價格候選所屬區段。""" + lowered = text.lower() + if any(term in lowered for term in OFFER_TERMS): + return PriceSection.OFFER_RANGE + if any(term in lowered for term in MESSAGE_TERMS): + return PriceSection.MESSAGE_BOX + if any(term in lowered for term in SELLER_TERMS): + return PriceSection.SELLER_INFO + if any(term in lowered for term in DETAIL_TERMS): + return PriceSection.DETAIL + if any(term in lowered for term in RECOMMENDATION_TERMS): + return PriceSection.UNKNOWN + return current + + @staticmethod + def looks_like_title(text: str) -> bool: + """排除價格與介面用語,判斷文字是否可能為商品標題。""" + lowered = text.lower().strip() + return ( + len(lowered) >= 4 + and not PRICE_RE.search(lowered) + and not any(term in lowered for term in UI_TERMS) + ) + + @staticmethod + def append_missing_required_field_warnings( + warnings: list[str], + *, + product_name: str | None, + price: int | None, + ) -> None: + """將缺少的必要商品欄位警告依序且不重複加入。""" + missing_warnings = ( + (MISSING_TITLE_WARNING, not product_name), + (MISSING_PRICE_WARNING, price is None), + ) + for warning, is_missing in missing_warnings: + if is_missing and warning not in warnings: + warnings.append(warning) + + @staticmethod + def position_score( + layout: MarketplaceLayout, + document: OCRDocument, + title: OCRTextBlock | None, + price: OCRTextBlock, + ) -> tuple[float, str | None]: + """依標題與價格位置計算候選分數。""" + if ( + not title + or title.x is None + or title.y is None + or price.x is None + or price.y is None + ): + return 0.0, None + if title is price: + return 0.10, None + title_bottom = title.y + (title.height or 0.0) + vertical_gap = price.y - title_bottom + same_column = abs(price.x - title.x) <= max( + title.width or 0.0, + price.width or 0.0, + 120.0, + ) + if vertical_gap < -5 or not same_column: + return -0.30, "價格不在商品標題下方同一資訊欄" + if ( + document.height + and vertical_gap > document.height * 0.12 + ): + return -0.20, "價格與商品標題距離過遠" + if ( + layout == MarketplaceLayout.DESKTOP + and document.width + and price.x < document.width * 0.55 + ): + return -0.35, "桌面版價格不在右側商品資訊欄" + return 0.16, None + + @staticmethod + def is_section_header(text: str, header: str) -> bool: + """忽略空白與裝飾字元後,比對完整區段標題。""" + normalized = re.sub(r"\s+", "", text).strip(">›·:") + return normalized == header + + @classmethod + def condition_from_text( + cls, + text: str, + ) -> MarketplaceCondition: + """依新舊狀況關鍵字判定商品狀況。""" + compact = re.sub(r"\s+", "", text).lower() + if ( + any(term in compact for term in USED_TERMS) + or USED_GRADE_RE.search(compact) + ): + return MarketplaceCondition.USED + if any(term in compact for term in NEW_TERMS): + return MarketplaceCondition.NEW + return MarketplaceCondition.UNKNOWN + + @classmethod + def extract_condition( + cls, + lines: list[str], + product_title: str | None = None, + ) -> tuple[ + MarketplaceCondition, + str, + str, + float, + list[str], + ]: + """依詳細資料、標題、說明的優先序擷取有限狀態證據。""" + detail_start = next( + ( + index + for index, line in enumerate(lines) + if cls.is_section_header(line, "詳細內容") + or cls.is_section_header(line, "詳細資料") + ), + None, + ) + if detail_start is not None: + detail_lines = lines[detail_start + 1:detail_start + 12] + for index, line in enumerate(detail_lines): + compact_line = re.sub(r"\s+", "", line) + if "狀況" not in compact_line and "狀態" not in compact_line: + continue + value = line + if ( + cls.condition_from_text(value) + is MarketplaceCondition.UNKNOWN + and index + 1 < len(detail_lines) + ): + value = f"{value} {detail_lines[index + 1]}" + condition = cls.condition_from_text(value) + if condition is not MarketplaceCondition.UNKNOWN: + source_text = normalize_display_text(value) + return ( + condition, + cls._condition_detail(source_text, condition), + source_text, + 0.97, + [], + ) + + if product_title: + title_condition = cls.condition_from_text(product_title) + if title_condition is not MarketplaceCondition.UNKNOWN: + source_text = normalize_display_text(product_title) + return ( + title_condition, + cls._condition_detail(source_text, title_condition), + source_text, + 0.99, + [], + ) + + description_start = next( + ( + index + for index, line in enumerate(lines) + if cls.is_section_header(line, "說明") + ), + None, + ) + if description_start is not None: + description_lines: list[str] = [] + for line in lines[description_start + 1:]: + if ( + cls.is_section_header(line, "賣家") + or cls.is_section_header(line, "賣家資訊") + ): + break + description_lines.append(line) + for line in description_lines: + condition = cls.condition_from_text(line) + if condition is MarketplaceCondition.UNKNOWN: + continue + source_text = normalize_display_text(line) + return ( + condition, + cls._condition_detail(source_text, condition), + source_text, + 0.82, + [], + ) + + return ( + MarketplaceCondition.UNKNOWN, + "", + "", + 0.0, + ["未找到明確商品狀況"], + ) + + @staticmethod + def _condition_detail( + source_text: str, + condition: MarketplaceCondition, + ) -> str: + """從有限狀態原文保留可供查價使用的詳細描述。""" + detail = re.sub( + r"^(?:商品)?(?:狀況|狀態)\s*[::・·-]?\s*", + "", + normalize_display_text(source_text), + flags=re.IGNORECASE, + ).strip() + if condition is MarketplaceCondition.USED: + without_generic_label = re.sub( + r"^(?:二\s*手|2\s*手|中古)\s*[::・·-]?\s*", + "", + detail, + flags=re.IGNORECASE, + ).strip() + return without_generic_label or detail + return detail + + @classmethod + def extract_seller_name( + cls, + lines: list[str], + ) -> tuple[str | None, float]: + """從賣家區段擷取不含數字的中英文顯示名稱。""" + seller_start = next( + ( + index + for index, line in enumerate(lines) + if cls.is_section_header(line, "賣家") + or cls.is_section_header(line, "賣家資訊") + ), + None, + ) + if seller_start is None: + return None, 0.0 + + for line in lines[seller_start + 1:seller_start + 7]: + candidate = normalize_display_text(line) + lowered = candidate.lower() + if ( + not candidate + or any( + term in lowered + for term in ( + "facebook", + "追蹤", + "加入", + "詳細", + "發訊息", + "傳訊息", + ) + ) + or any(char.isdigit() for char in candidate) + ): + continue + english_name = re.fullmatch( + r"[A-Za-z][A-Za-z'’-]*" + r"(?:-[A-Za-z][A-Za-z'’-]*)?" + r"(?:\s+[A-Za-z][A-Za-z'’-]*" + r"(?:-[A-Za-z][A-Za-z'’-]*)?){1,4}", + candidate, + ) + chinese_name = re.fullmatch( + r"[\u4e00-\u9fff]{2,5}", + candidate.replace(" ", ""), + ) + if chinese_name: + return candidate.replace(" ", ""), 0.92 + if english_name: + return candidate, 0.92 + return None, 0.0 + + @staticmethod + def initial_rejection( + match: re.Match[str], + amount: int, + section: PriceSection, + is_range: bool, + ) -> str | None: + """套用 FB Marketplace 的價格候選排除規則。""" + if not NT_MAIN_PRICE_RE.fullmatch(match.group("currency")): + return "商品主價格必須以 NT$ 開頭" + if ( + amount < 1 + or amount + > DEFAULT_PRICE_RISK_POLICY.maximum_supported_price + ): + return "價格超出支援範圍" + if is_range: + return "出價或多價格範圍不可作為商品主價格" + if section != "unknown": + return f"價格位於非主價格區塊:{section}" + return None + + @staticmethod + def title_confidence_bonus( + layout: MarketplaceLayout, + title_distance: int | None, + ) -> float: + """依版型與標題距離回傳候選信心加分。""" + if title_distance is None: + return 0.0 + if title_distance == 0: + return 0.50 + if ( + layout == MarketplaceLayout.DESKTOP + and title_distance <= 2 + ): + return 0.48 + if ( + layout == MarketplaceLayout.MOBILE + and title_distance == 1 + ): + return 0.48 + if ( + layout == MarketplaceLayout.MOBILE + and title_distance <= 2 + ): + return 0.34 + return 0.15 diff --git a/backend/services/image_price_service/page_analysis/fb_marketplace/layout_extractors.py b/backend/services/image_price_service/page_analysis/fb_marketplace/layout_extractors.py new file mode 100644 index 0000000000000000000000000000000000000000..f36d89af9cab27eb59300dd5ca25b8fa031427db --- /dev/null +++ b/backend/services/image_price_service/page_analysis/fb_marketplace/layout_extractors.py @@ -0,0 +1,244 @@ +"""FB Marketplace mobile 與 desktop 標題版型策略。""" + +from typing import Protocol + +from backend.services.image_price_service.domain.models import ( + OCRDocument, + OCRTextBlock, +) +from backend.services.image_price_service.ocr.ocr_text_utils import ( + are_adjacent_title_lines, + normalize_display_text, +) +from backend.services.image_price_service.page_analysis.fb_marketplace.fb_marketplace_rules import ( + FBMarketplaceRules, +) + +_MAX_TITLE_LINES = 4 +_DESKTOP_INFO_PANEL_LEFT_RATIO = 0.48 +_DESKTOP_TITLE_LOOKBACK_RATIO = 0.20 + +TitleMatch = tuple[OCRTextBlock | None, str | None, int | None] + + +class LayoutTitleExtractor(Protocol): + """定義版型標題抽取策略介面。""" + + def find_title( + self, + document: OCRDocument, + blocks: list[OCRTextBlock], + price_index: int, + price: OCRTextBlock, + prefix: str, + ) -> TitleMatch: + """尋找價格對應的商品標題。""" + ... + + +def _adjacent_title_above_price( + rules: FBMarketplaceRules, + document: OCRDocument, + blocks: list[OCRTextBlock], + price_index: int, + price: OCRTextBlock, + prefix: str, +) -> TitleMatch: + """從價格同區塊前綴及相鄰 OCR 行組合商品標題。""" + title: OCRTextBlock | None = None + title_distance: int | None = None + title_fragments: list[str] = [] + if rules.looks_like_title(prefix): + title = price + title_distance = 0 + title_fragments.append(normalize_display_text(prefix)) + + found_title = title is not None + adjacent_block = title or price + for prior_index in range( + price_index - 1, + max(-1, price_index - _MAX_TITLE_LINES - 1), + -1, + ): + prior_block = blocks[prior_index] + if not rules.looks_like_title(prior_block.text): + if found_title: + break + continue + if found_title and not are_adjacent_title_lines( + document, + prior_block, + adjacent_block, + ): + break + + title_fragments.append( + normalize_display_text(prior_block.text) + ) + if title is None: + title = prior_block + title_distance = price_index - prior_index + found_title = True + adjacent_block = prior_block + + title_text = ( + normalize_display_text( + " ".join(reversed(title_fragments)) + ) + if title_fragments + else None + ) + return title, title_text, title_distance + + +class MobileLayoutExtractor: + """依 mobile 相鄰 OCR 行規則擷取商品標題。""" + + def __init__(self, rules: FBMarketplaceRules) -> None: + self._rules = rules + + def find_title( + self, + document: OCRDocument, + blocks: list[OCRTextBlock], + price_index: int, + price: OCRTextBlock, + prefix: str, + ) -> TitleMatch: + return _adjacent_title_above_price( + self._rules, + document, + blocks, + price_index, + price, + prefix, + ) + + +class DesktopLayoutExtractor: + """依 desktop 右側資訊欄規則擷取商品標題。""" + + def __init__(self, rules: FBMarketplaceRules) -> None: + self._rules = rules + + def _title_above_price( + self, + document: OCRDocument, + blocks: list[OCRTextBlock], + price: OCRTextBlock, + ) -> TitleMatch: + """在右側資訊欄的加大範圍內尋找主價格上方標題。""" + if ( + not document.width + or not document.height + or price.x is None + or price.y is None + ): + return None, None, None + + panel_left = ( + document.width * _DESKTOP_INFO_PANEL_LEFT_RATIO + ) + lookback = max( + document.height * _DESKTOP_TITLE_LOOKBACK_RATIO, + (price.height or 0.0) * 6, + 80.0, + ) + same_row_tolerance = max( + document.height * 0.006, + (price.height or 0.0) * 0.25, + 6.0, + ) + horizontal_tolerance = document.width * 0.12 + spatial_matches: list[tuple[int, OCRTextBlock]] = [] + + for block_index, candidate in enumerate(blocks): + if ( + candidate is price + or candidate.x is None + or candidate.y is None + or candidate.x < panel_left + or not self._rules.looks_like_title(candidate.text) + ): + continue + if not ( + price.y - lookback + <= candidate.y + <= price.y + same_row_tolerance + ): + continue + + candidate_right = ( + candidate.x + (candidate.width or 0.0) + ) + price_right = price.x + (price.width or 0.0) + if ( + candidate_right + < price.x - horizontal_tolerance + or candidate.x + > price_right + horizontal_tolerance + ): + continue + spatial_matches.append((block_index, candidate)) + + if not spatial_matches: + return None, None, None + + spatial_matches.sort( + key=lambda item: ( + item[1].y or 0.0, + item[1].x or 0.0, + item[0], + ) + ) + selected_matches = spatial_matches[-_MAX_TITLE_LINES:] + title_text = normalize_display_text( + " ".join( + block.text + for _, block in selected_matches + ) + ) + _, nearest_block = max( + selected_matches, + key=lambda item: ( + item[1].y or 0.0, + item[0], + ), + ) + # 桌面 OCR 閱讀順序可能被左側圖片與導覽列打散; + # 通過空間驗證後仍視為緊鄰主價格。 + return nearest_block, title_text, 1 + + def find_title( + self, + document: OCRDocument, + blocks: list[OCRTextBlock], + price_index: int, + price: OCRTextBlock, + prefix: str, + ) -> TitleMatch: + if self._rules.looks_like_title(prefix): + return _adjacent_title_above_price( + self._rules, + document, + blocks, + price_index, + price, + prefix, + ) + + spatial_title = self._title_above_price( + document, + blocks, + price, + ) + if spatial_title[1] is not None: + return spatial_title + return _adjacent_title_above_price( + self._rules, + document, + blocks, + price_index, + price, + prefix, + ) diff --git a/backend/services/image_price_service/pricing/__init__.py b/backend/services/image_price_service/pricing/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/backend/services/image_price_service/pricing/__init__.py @@ -0,0 +1 @@ + diff --git a/backend/services/image_price_service/pricing/market_price_resolver.py b/backend/services/image_price_service/pricing/market_price_resolver.py new file mode 100644 index 0000000000000000000000000000000000000000..797c312aea0e11fab18a7a9796f357ab35b74dbe --- /dev/null +++ b/backend/services/image_price_service/pricing/market_price_resolver.py @@ -0,0 +1,58 @@ +"""解析結構化市場價格。""" + +from typing import Any + +from backend.core.config import settings +from backend.services.dto.price_analysis import MarketPriceEstimate +from backend.services.image_price_service.domain.models import \ + MarketplaceCondition + + +def resolve_market_price( + online_price_service: Any, + product_name: str, + brand_model: str, + fallback_price: int, + search_query: str = "", + condition: MarketplaceCondition = MarketplaceCondition.NEW, + condition_text: str = "", +) -> tuple[MarketPriceEstimate, ...]: + """取得市場估計。""" + if settings.ONLINE_PRICE_ENABLED: + query = ( + search_query + or ( + brand_model + if brand_model != "未知型號" + else product_name + ) + ) + return online_price_service.estimate_prices( + query, + max_results=settings.ONLINE_PRICE_MAX_RESULTS, + condition=condition, + condition_text=condition_text, + ) + + supported_fallback = max( + 0, + min( + int(fallback_price), + online_price_service.policy.maximum_supported_price, + ), + ) + return ( + MarketPriceEstimate( + status="success" if supported_fallback > 0 else "not_found", + condition=condition, + reference_mode="median_low_sample", + median_price=supported_fallback, + low_price=supported_fallback, + high_price=supported_fallback, + sample_count=1 if supported_fallback > 0 else 0, + site_count=1 if supported_fallback > 0 else 0, + source="fallback_local", + confidence=0.0, + candidates=(), + ), + ) diff --git a/backend/services/image_price_service/pricing/online_marketprice_service.py b/backend/services/image_price_service/pricing/online_marketprice_service.py new file mode 100644 index 0000000000000000000000000000000000000000..3364939dd4e38d4d2b51880da6f499cf04919798 --- /dev/null +++ b/backend/services/image_price_service/pricing/online_marketprice_service.py @@ -0,0 +1,524 @@ +"""定義價格查詢、候選驗證與台灣市場價格統計。""" + +import logging +import re +import time +from concurrent.futures import ThreadPoolExecutor +from typing import Any, Literal, cast + +from backend.services.dto.price_analysis import (MarketPriceCandidateEvidence, + MarketPriceEstimate, + SearchTool) +from backend.services.image_price_service.domain.models import \ + MarketplaceCondition +from backend.services.image_price_service.domain.policy import ( + DEFAULT_PRICE_RISK_POLICY, PriceRiskPolicy) +from backend.services.image_price_service.pricing.search_result_price_extractor import ( + SearchResultPriceExtractor, default_search_result_price_extractor, + extract_prices_from_search_results) +from backend.services.image_price_service.pricing.search_tools import ( + SEARCH_FUNCTIONS, SEARCH_TOOL_STAGES) + +logger = logging.getLogger(__name__) + +_PRICE_RE = re.compile( + r"(?:NT\$|TWD\s*|\$\s*|(?:售價|特價|價格|優惠價)\s*[::]?\s*)" + r"(?P\d[\d,]{1,8})" + r"|(?P\d[\d,]{1,8})\s*元", + flags=re.IGNORECASE, +) +_PRICE_RANGE_RE = re.compile( + r"(?:NT\$|TWD\s*|\$\s*)?\d[\d,]*\s*" + r"(?:-|~|~|至|到)\s*" + r"(?:NT\$|TWD\s*|\$\s*)?\d[\d,]*", + flags=re.IGNORECASE, +) +_NON_SALE_TERMS = ("月付", "分期", "折價", "折扣", "運費", "定金") +_NEW_TERMS = ("全新", "新品", "未拆", "未使用") +_USED_TERMS = ("二手", "中古", "拆封", "拆擺", "展示品", "使用過") + + +class OnlineMarketPriceService: + """搜尋、驗證並彙整同商品且同品況的台灣市場價格。""" + + _SEARCH_TOOL_LABELS = { + "serpapi": "serp_api", + "tavily": "tavily", + "ddgs": "ddgs", + } + + def __init__( + self, + *, + policy: PriceRiskPolicy = DEFAULT_PRICE_RISK_POLICY, + search_functions: dict[str, Any] | None = None, + search_stages: tuple[tuple[str, ...], ...] = SEARCH_TOOL_STAGES, + price_extractor: SearchResultPriceExtractor | None = None, + ) -> None: + """建立線上市價服務。""" + self.policy = policy + self._search_functions = search_functions or SEARCH_FUNCTIONS + self._search_stages = search_stages + self._price_extractor = ( + price_extractor or default_search_result_price_extractor + ) + + def estimate_prices( + self, + product_query: str, + max_results: int = 10, + *, + condition: MarketplaceCondition = MarketplaceCondition.NEW, + condition_text: str = "", + ) -> tuple[MarketPriceEstimate, ...]: + """一次搜尋後建立市場價格區間。""" + if condition is MarketplaceCondition.UNKNOWN: + candidates, search_tools = self._collect_candidates( + product_query, + max_results, + condition=condition, + ) + return ( + self._aggregate_candidates( + candidates, + condition, + search_tools=search_tools, + ), + ) + return ( + self.estimate_price( + product_query, + max_results, + condition=condition, + condition_text=condition_text, + ), + ) + + def estimate_price( + self, + product_query: str, + max_results: int = 10, + *, + condition: MarketplaceCondition = MarketplaceCondition.NEW, + condition_text: str = "", + ) -> MarketPriceEstimate: + """回傳單一已知品況的結構化市場價格估計。""" + if condition is MarketplaceCondition.UNKNOWN: + raise ValueError("UNKNOWN 必須使用 estimate_prices() 執行合併估價") + + candidates, search_tools = self._collect_candidates( + product_query, + max_results, + condition=condition, + ) + return self._aggregate_candidates( + candidates, + condition, + search_tools=search_tools, + ) + + def _collect_candidates( + self, + product_query: str, + max_results: int, + *, + condition: MarketplaceCondition, + ) -> tuple[list[MarketPriceCandidateEvidence], list[SearchTool]]: + """分階段並行搜尋並累積結果,最後以一次 LLM 呼叫整理候選價格。""" + query = product_query.strip() + if not query or max_results <= 0: + return [], [] + + condition_term = self._condition_search_term(condition) + conditioned_query = self._append_condition_term(query, condition_term) + search_results: list[dict[str, Any]] = [] + search_tools: list[SearchTool] = [] + preflight_target = ( + self.policy.minimum_market_samples + * self.policy.preflight_multiplier + ) + + for stage in self._search_stages: + stage_tools = [ + tool_name + for tool_name in stage + if self._search_functions.get(tool_name) is not None + ] + for tool_name in set(stage) - set(stage_tools): + logger.warning("找不到搜尋工具:%s", tool_name) + if not stage_tools: + continue + + # 同一階段的工具彼此獨立,並行呼叫;map 保持工具順序以維持結果排序穩定 + with ThreadPoolExecutor(max_workers=len(stage_tools)) as executor: + stage_results = list( + executor.map( + lambda tool_name: self._fetch_results( + conditioned_query, + max_results, + tool_name, + ), + stage_tools, + ) + ) + for tool_name, tool_results in zip(stage_tools, stage_results): + search_tools.append( + cast(SearchTool, self._SEARCH_TOOL_LABELS.get(tool_name, tool_name)) + ) + search_results.extend(tool_results) + + preflight_count = sum( + self._is_price_result(result, condition) + for result in search_results + ) + logger.info( + "價格搜尋預檢 工具=%s 合格數=%d 門檻=%d", + ",".join(search_tools), + preflight_count, + preflight_target, + ) + if preflight_count >= preflight_target: + break + + if not search_results: + return [], search_tools + + prioritized_results = [ + result + for result in search_results + if self._is_price_result(result, condition) + ] + prioritized_results.extend( + result + for result in search_results + if not self._is_price_result(result, condition) + ) + extraction_started = time.perf_counter() + try: + candidates = extract_prices_from_search_results( + prioritized_results, + condition, + product_query=query, + policy=self.policy, + extractor=self._price_extractor, + ) + except Exception as error: + logger.error( + "搜尋結果價格擷取失敗 搜尋工具=%s 錯誤=%s", + ",".join(search_tools), + str(error), + ) + return [], search_tools + + logger.info( + "搜尋結果價格擷取完成 搜尋結果數=%d 候選數=%d 耗時=%.2f 秒", + len(search_results), + len(candidates), + time.perf_counter() - extraction_started, + ) + return candidates, search_tools + + def _fetch_results( + self, + query: str, + max_results: int, + tool_name: str, + ) -> list[dict[str, Any]]: + """執行單一搜尋工具並回傳可供後續預檢的結果。""" + tool_label = self._SEARCH_TOOL_LABELS.get(tool_name, tool_name) + logger.info("價格搜尋開始 工具=%s", tool_label) + + search_fn = self._search_functions.get(tool_name) + if search_fn is None: + logger.warning("找不到搜尋工具:%s", tool_name) + return [] + + search_started = time.perf_counter() + try: + search_results = search_fn(query, max_results) + except Exception as error: + logger.error( + "搜尋工具執行失敗 工具=%s 錯誤=%s 耗時=%.2f 秒", + tool_label, + str(error), + time.perf_counter() - search_started, + ) + return [] + + if not isinstance(search_results, list): + return [] + + results = [ + result + for result in search_results + if isinstance(result, dict) + ] + logger.info( + "價格搜尋完成 工具=%s 結果數=%d 耗時=%.2f 秒", + tool_label, + len(results), + time.perf_counter() - search_started, + ) + return results + + def _is_price_result( + self, + result: dict[str, Any], + condition: MarketplaceCondition, + ) -> bool: + """判斷搜尋結果是否可能含有目標品況的一次性商品售價。""" + text = " ".join( + str(result.get(field, "")).strip() + for field in ("title", "snippet") + ) + if not text.strip(): + return False + + has_used_term = any(term in text for term in _USED_TERMS) + if condition is MarketplaceCondition.NEW: + if has_used_term or not any(term in text for term in _NEW_TERMS): + return False + elif ( + condition is MarketplaceCondition.USED + and not has_used_term + ): + return False + + for match in _PRICE_RE.finditer(text): + start = max(0, match.start() - 16) + end = min(len(text), match.end() + 16) + context = text[start:end] + if any(term in context for term in _NON_SALE_TERMS): + continue + if _PRICE_RANGE_RE.search(context): + continue + + raw_price = match.group("prefix") or match.group("suffix") + price = int(raw_price.replace(",", "")) + if 0 < price <= self.policy.maximum_supported_price: + return True + return False + + def _aggregate_candidates( + self, + candidates: list[MarketPriceCandidateEvidence], + condition: MarketplaceCondition, + *, + search_tools: list[SearchTool] | None = None, + ) -> MarketPriceEstimate: + """依 policy 門檻選擇低樣本中位數模式或 IQR 模式。""" + if not candidates: + return self._empty_estimate( + condition, + search_tools=search_tools, + ) + + sample_count = len(candidates) + site_count = self._site_count(candidates) + confidence = self._market_confidence(sample_count, site_count) + required_samples = self.policy.minimum_market_samples + if condition is MarketplaceCondition.UNKNOWN: + required_samples += 1 + if ( + sample_count < required_samples + or site_count < self.policy.minimum_market_sites + or confidence < self.policy.minimum_market_confidence + ): + return self._small_sample_estimate( + candidates, + condition, + status="insufficient", + search_tools=search_tools, + ) + + if sample_count < self.policy.minimum_iqr_samples: + return self._small_sample_estimate( + candidates, + condition, + status="success", + search_tools=search_tools, + ) + + prices = sorted(candidate.price for candidate in candidates) + initial_p25 = self._percentile(prices, 0.25) + initial_p75 = self._percentile(prices, 0.75) + iqr = initial_p75 - initial_p25 + lower_fence = initial_p25 - 1.5 * iqr + upper_fence = initial_p75 + 1.5 * iqr + retained = [ + candidate + for candidate in candidates + if lower_fence <= candidate.price <= upper_fence + ] + retained_sample_count = len(retained) + retained_site_count = self._site_count(retained) + retained_confidence = self._market_confidence( + retained_sample_count, + retained_site_count, + ) + if ( + retained_sample_count < required_samples + or retained_site_count < self.policy.minimum_market_sites + or retained_confidence < self.policy.minimum_market_confidence + ): + return self._small_sample_estimate( + retained, + condition, + status="insufficient", + search_tools=search_tools, + ) + if retained_sample_count < self.policy.minimum_iqr_samples: + return self._small_sample_estimate( + retained, + condition, + status="success", + search_tools=search_tools, + ) + + retained_prices = sorted(candidate.price for candidate in retained) + return MarketPriceEstimate( + status="success", + condition=condition, + reference_mode="iqr", + median_price=round(self._percentile(retained_prices, 0.50)), + low_price=round(self._percentile(retained_prices, 0.25)), + high_price=round(self._percentile(retained_prices, 0.75)), + sample_count=retained_sample_count, + site_count=retained_site_count, + source="online", + confidence=retained_confidence, + search_tools=list(search_tools or []), + candidates=tuple(retained), + ) + + def _small_sample_estimate( + self, + candidates: list[MarketPriceCandidateEvidence], + condition: MarketplaceCondition, + *, + status: Literal["success", "insufficient"], + search_tools: list[SearchTool] | None = None, + ) -> MarketPriceEstimate: + """以中位數及 policy 的固定相對容許範圍建立估計。""" + if not candidates: + return self._empty_estimate( + condition, + status="insufficient", + search_tools=search_tools, + ) + + prices = sorted(candidate.price for candidate in candidates) + median_price = round(self._percentile(prices, 0.50)) + tolerance = self.policy.small_sample_relative_tolerance + low_price = round(median_price * (1 - tolerance)) + high_price = round(median_price * (1 + tolerance)) + resolved_status = status + if ( + low_price <= 0 + or high_price > self.policy.maximum_supported_price + ): + resolved_status = "insufficient" + return MarketPriceEstimate( + status=resolved_status, + condition=condition, + reference_mode="median_low_sample", + median_price=median_price, + low_price=low_price, + high_price=high_price, + sample_count=len(candidates), + site_count=self._site_count(candidates), + source="online", + confidence=self._market_confidence( + len(candidates), + self._site_count(candidates), + ), + search_tools=list(search_tools or []), + candidates=tuple(candidates), + ) + + def _empty_estimate( + self, + condition: MarketplaceCondition, + *, + status: Literal["not_found", "insufficient"] = "not_found", + search_tools: list[SearchTool] | None = None, + ) -> MarketPriceEstimate: + """建立沒有可用市場候選的估計結果。""" + return MarketPriceEstimate( + status=status, + condition=condition, + reference_mode="median_low_sample", + median_price=0, + low_price=0, + high_price=0, + sample_count=0, + site_count=0, + source="online", + confidence=0.0, + search_tools=list(search_tools or []), + candidates=(), + ) + + def _market_confidence(self, sample_count: int, site_count: int) -> float: + """依 IQR 樣本完整度與獨立來源完整度計算資料可信度。""" + sample_confidence = min( + 1.0, + sample_count / self.policy.minimum_iqr_samples, + ) + site_confidence = min( + 1.0, + site_count / self.policy.minimum_market_sites, + ) + return round(min(sample_confidence, site_confidence), 4) + + @staticmethod + def _site_count( + candidates: list[MarketPriceCandidateEvidence], + ) -> int: + return len( + { + candidate.candidate_id.rsplit("#", 3)[0] + for candidate in candidates + if candidate.candidate_id + } + ) + + @staticmethod + def _percentile(values: list[int], quantile: float) -> float: + """使用固定線性插值計算百分位數。""" + if not values: + return 0.0 + ordered = sorted(values) + position = (len(ordered) - 1) * quantile + lower_index = int(position) + upper_index = min(lower_index + 1, len(ordered) - 1) + fraction = position - lower_index + return ( + ordered[lower_index] + + (ordered[upper_index] - ordered[lower_index]) * fraction + ) + + @staticmethod + def _condition_search_term( + condition: MarketplaceCondition, + ) -> str: + """只加入必要的全新/二手搜尋關鍵字。""" + if condition is MarketplaceCondition.NEW: + return "全新" + if condition is MarketplaceCondition.USED: + return "二手" + return "全新 二手" + + @staticmethod + def _append_condition_term(query: str, condition_term: str) -> str: + missing_terms = [ + term + for term in condition_term.split() + if term.casefold() not in query.casefold() + ] + if not missing_terms: + return query + return f"{query} {' '.join(missing_terms)}" + + +online_marketprice_service = OnlineMarketPriceService() diff --git a/backend/services/image_price_service/pricing/search_result_price_extractor.py b/backend/services/image_price_service/pricing/search_result_price_extractor.py new file mode 100644 index 0000000000000000000000000000000000000000..3b50bd0b0682e34de6080f184af4596d2f94c855 --- /dev/null +++ b/backend/services/image_price_service/pricing/search_result_price_extractor.py @@ -0,0 +1,342 @@ +"""使用一次結構化 LLM 呼叫整理搜尋結果中的同商品價格。""" + +import hashlib +import json +import logging +import re +import unicodedata +from typing import Any + +from langchain_core.output_parsers import PydanticOutputParser +from langchain_core.utils.function_calling import convert_to_json_schema +from pydantic import BaseModel, ConfigDict, Field + +from backend.core.config import settings +from backend.providers import groq_provider +from backend.services.dto.price_analysis import MarketPriceCandidateEvidence +from backend.services.image_price_service.domain.models import \ + MarketplaceCondition +from backend.services.image_price_service.domain.policy import ( + DEFAULT_PRICE_RISK_POLICY, PriceRiskPolicy) + +logger = logging.getLogger(__name__) + +_ZERO_WIDTH_CHARACTERS = frozenset( + { + "\N{ZERO WIDTH SPACE}", + "\N{ZERO WIDTH NON-JOINER}", + "\N{ZERO WIDTH JOINER}", + "\N{ZERO WIDTH NO-BREAK SPACE}", + } +) +_ALLOWED_SYMBOLS = frozenset({"$", "+", "×", "=", "~"}) +_MAX_RESULTS_FOR_LLM = 10 +_MAX_LLM_TEXT_CHARS = 4_000 +_MAX_TITLE_CHARS = 160 +_MAX_SNIPPET_CHARS = 800 +_MAX_COMPLETION_TOKENS = 4_096 + +_SYSTEM_PROMPT = """ +你是台灣購物搜尋結果的商品價格整理器。請一次分析所有輸入結果,擷取其中明確的 +商品售價並判斷品況,最後只輸出符合下列格式的有效 json 物件。 + +規則: +1. 只負責從每筆 title 與 snippet 擷取能明確配對到商品的售價。 +2. title 或 snippet 可能以分號列出多項商品,價格也可能出現在商品描述之前或之後。 + 必須依語意將每個價格配對到正確商品,不得假設價格位於固定位置。 +3. 只接受明確的一次性商品售價;排除運費、折扣金額、定金、月付、分期、面交地點、 + 年份、尺寸、比例、商品編號、價格範圍,以及無法確定是售價的數字。 +4. condition 只能是 new、used 或 unknown。全新未拆為 new;二手、拆封、拆擺、 + 中古、展示品為 used;沒有足夠文字判斷時為 unknown。 +5. result_index 必須引用輸入中的索引。evidence 必須逐字取自該筆 title 或 snippet, + 並包含足以核對該價格的文字。不得捏造商品、價格、品況或來源。 +6. 同一筆結果若有多個可明確配對的獨立商品售價,可輸出多筆;若是合售, + 只輸出合售總價,不可自行拆算單價。沒有可靠候選時回傳空陣列。 + +輸出格式: +{ + "candidates": [ + { + "result_index": 0, + "price": 840, + "condition": "used", + "evidence": "二手拆擺七支合售. $840" + } + ] +} + +欄位規則: +- candidates 是價格候選陣列。 +- result_index 必須是對應輸入 results 的非負整數索引。 +- price 必須是大於 0 的新台幣整數,不得包含貨幣符號或千分位逗號。 +- condition 只能是 "new"、"used" 或 "unknown"。 +- evidence 必須逐字取自對應結果的 title 或 snippet。 +- 沒有可靠候選時輸出 {"candidates": []}。 +- 不得輸出 candidates 以外的欄位或額外說明。 +""".strip() + + +class ExtractedSearchPrice(BaseModel): + """LLM 從單筆搜尋結果辨識出的價格候選。""" + + model_config = ConfigDict(extra="forbid") + + result_index: int = Field(ge=0) + price: int = Field(gt=0) + condition: MarketplaceCondition + evidence: str = Field(min_length=1) + + +class SearchPriceExtraction(BaseModel): + """一次搜尋結果整理的結構化輸出。""" + + model_config = ConfigDict(extra="forbid") + + candidates: list[ExtractedSearchPrice] = Field(default_factory=list) + + +def _strict_response_format() -> dict[str, Any]: + """strict mode JSON Schema response format。""" + schema = convert_to_json_schema(SearchPriceExtraction, strict=True) + return { + "type": "json_schema", + "json_schema": { + "name": schema.pop("title", "SearchPriceExtraction"), + "strict": True, + "schema": schema, + }, + } + + +class SearchResultPriceExtractor: + """延遲建立 Groq client,並以一次 structured-output 呼叫整理價格。""" + + def __init__( + self, + *, + api_key: str | None = None, + model_name: str, + structured_llm: Any | None = None, + ) -> None: + self._api_key = api_key.strip() if api_key else None + self._model_name = model_name.strip() + self._structured_llm = structured_llm + + def _get_structured_llm(self) -> Any: + if self._structured_llm is not None: + return self._structured_llm + if not groq_provider.is_configured(self._api_key): + raise RuntimeError("尚未設定 GROQ_API_KEY,無法整理搜尋結果價格") + if not self._model_name: + raise RuntimeError("尚未設定價格整理模型") + + llm = groq_provider.create( + model=self._model_name, + temperature=0, + api_key=self._api_key, + reasoning_effort=settings.PRICE_REASONING_EFFORT, + timeout=settings.PRICE_LLM_TIMEOUT_SECONDS, + max_retries=settings.PRICE_LLM_MAX_RETRIES, + ) + self._structured_llm = ( + llm.bind( + response_format=_strict_response_format(), + max_completion_tokens=_MAX_COMPLETION_TOKENS, + ) + | PydanticOutputParser(pydantic_object=SearchPriceExtraction) + ) + return self._structured_llm + + def extract( + self, + search_results: list[dict[str, Any]], + condition: MarketplaceCondition, + *, + product_query: str, + policy: PriceRiskPolicy = DEFAULT_PRICE_RISK_POLICY, + ) -> list[MarketPriceCandidateEvidence]: + """將一批搜尋結果交給 LLM,並驗證其結構化候選。""" + normalized_results = _normalize_search_results(search_results) + if not normalized_results or not product_query.strip(): + return [] + + payload = { + "results": _prepare_llm_results(normalized_results), + } + response = self._get_structured_llm().invoke( + [ + ("system", _SYSTEM_PROMPT), + ("human", f"輸入:{json.dumps(payload, ensure_ascii=False)}"), + ] + ) + extraction = ( + response + if isinstance(response, SearchPriceExtraction) + else SearchPriceExtraction.model_validate(response) + ) + return _validate_candidates( + extraction, + normalized_results, + condition, + policy, + ) + + +def _normalize_search_results( + search_results: list[dict[str, Any]], +) -> list[dict[str, str]]: + """只保留 LLM 與來源回填所需的可信欄位。""" + normalized: list[dict[str, str]] = [] + for result in search_results: + if not isinstance(result, dict): + continue + title = str(result.get("title", "")).strip() + snippet = str(result.get("snippet", "")).strip() + title = _sanitize_llm_text(title) + snippet = _sanitize_llm_text(snippet) + if not (title or snippet): + continue + normalized.append( + { + "title": title, + "snippet": snippet, + } + ) + return normalized + + +def _prepare_llm_results( + search_results: list[dict[str, str]], +) -> list[dict[str, str]]: + """限制送入的結果數量與文字量""" + selected = search_results[:_MAX_RESULTS_FOR_LLM] + if not selected: + return [] + + per_result_budget = _MAX_LLM_TEXT_CHARS // len(selected) + prepared: list[dict[str, str]] = [] + for result in selected: + title = result["title"][: min(_MAX_TITLE_CHARS, per_result_budget)] + snippet_budget = min( + _MAX_SNIPPET_CHARS, + max(0, per_result_budget - len(title)), + ) + snippet = result["snippet"][:snippet_budget] + if not title and not snippet: + continue + prepared.append( + { + "title": title, + "snippet": snippet, + } + ) + return prepared + + +def _sanitize_llm_text(text: str) -> str: + """清除 output 的隱形與裝飾字元。""" + cleaned: list[str] = [] + for character in unicodedata.normalize("NFKC", text): + if character in _ZERO_WIDTH_CHARACTERS: + continue + if character.isspace(): + cleaned.append(" ") + continue + + category = unicodedata.category(character) + family = category[0] + if ( + family in {"L", "N", "P"} + or category == "Sc" + or character in _ALLOWED_SYMBOLS + ): + cleaned.append(character) + continue + if ( + family == "M" + and cleaned + and unicodedata.category(cleaned[-1]).startswith("L") + ): + cleaned.append(character) + continue + + # 不在白名單的 emoji、圖形與控制字元以空白隔開, + # 避免移除後將原本分離的兩個詞黏在一起。 + cleaned.append(" ") + + return re.sub(r"\s+", " ", "".join(cleaned)).strip() + + +def _validate_candidates( + extraction: SearchPriceExtraction, + search_results: list[dict[str, str]], + condition: MarketplaceCondition, + policy: PriceRiskPolicy, +) -> list[MarketPriceCandidateEvidence]: + """以原始索引回填來源,並執行非語意性的安全驗證。""" + candidates: list[MarketPriceCandidateEvidence] = [] + for candidate_index, extracted in enumerate(extraction.candidates): + if extracted.result_index >= len(search_results): + continue + if (condition is not MarketplaceCondition.UNKNOWN and extracted.condition is not condition): + continue + if extracted.price > policy.maximum_supported_price: + continue + + source = search_results[extracted.result_index] + source_text = " ".join( + part + for part in (source["title"], source["snippet"]) + if part + ) + evidence = extracted.evidence.strip() + if evidence not in source_text: + continue + + source_id = hashlib.sha256( + f"{source['title']}\0{source['snippet']}".encode("utf-8") + ).hexdigest()[:16] + candidates.append( + MarketPriceCandidateEvidence( + candidate_id=( + f"{source_id}#{extracted.price}" + f"#{extracted.condition.value}#{candidate_index}" + ), + title=source["title"], + price=extracted.price, + condition=extracted.condition, + evidence=evidence, + ) + ) + return candidates + + +default_search_result_price_extractor = SearchResultPriceExtractor( + model_name=settings.PRICE_MODEL, +) + + +def extract_prices_from_search_results( + search_results: list[dict[str, Any]], + condition: MarketplaceCondition, + *, + product_query: str, + policy: PriceRiskPolicy = DEFAULT_PRICE_RISK_POLICY, + extractor: SearchResultPriceExtractor | None = None, +) -> list[MarketPriceCandidateEvidence]: + """相容函式:以一次 LLM 呼叫整理整批搜尋結果。""" + active_extractor = extractor or default_search_result_price_extractor + return active_extractor.extract( + search_results, + condition, + product_query=product_query, + policy=policy, + ) + + +__all__ = [ + "ExtractedSearchPrice", + "SearchResultPriceExtractor", + "SearchPriceExtraction", + "extract_prices_from_search_results", +] diff --git a/backend/services/image_price_service/pricing/search_tools.py b/backend/services/image_price_service/pricing/search_tools.py new file mode 100644 index 0000000000000000000000000000000000000000..f85f50ce81ddc34b8c81b28f8b5687c79c5b4796 --- /dev/null +++ b/backend/services/image_price_service/pricing/search_tools.py @@ -0,0 +1,176 @@ +"""獨立的搜尋工具函式:SerpApi、DuckDuckGo 與 Tavily。""" + +import json +import logging +import os +from concurrent.futures import ThreadPoolExecutor +from typing import Any + +import serpapi +from tavily import TavilyClient + +from backend.core.config import settings + +logger = logging.getLogger(__name__) + + +def search_serpapi( + query: str, + max_results: int = 10, +) -> list[dict[str, str]]: + """透過 SerpApi 搜尋並回傳標準化結果;多頁時同時發出請求。""" + api_key = ( + settings.SERP_API_KEY.get_secret_value() + or os.getenv("SERP_API_KEY", "") + ) + if not api_key: + raise RuntimeError("尚未設定 SERP_API_KEY") + + logger.info("SerpApi 搜尋 query=%r max_results=%d", query, max_results) + page_starts = list(range(0, max_results, 10)) + with ThreadPoolExecutor(max_workers=max(1, len(page_starts))) as executor: + pages = list( + executor.map( + lambda start: _fetch_serpapi_page(api_key, query, start), + page_starts, + ) + ) + + organic_results: list[dict[str, Any]] = [] + for page_results in pages: + if not page_results: + break + organic_results.extend(page_results) + + results = [ + { + "title": str(item.get("title", "")).strip(), + "snippet": str(item.get("snippet", "")).strip(), + } + for item in organic_results[:max_results] + if ( + str(item.get("title", "")).strip() + or str(item.get("snippet", "")).strip() + ) + ] + return results + + +def _fetch_serpapi_page( + api_key: str, + query: str, + start: int, +) -> list[dict[str, Any]]: + """取得單一 SerpApi 分頁;每個執行緒使用獨立 client 以免共用 HTTP session。""" + client = serpapi.Client(api_key=api_key, timeout=settings.SEARCH_TIMEOUT_SECONDS) + search_params: dict[str, Any] = { + "engine": "google_light", + "q": query, + "google_domain": "google.com.tw", + "hl": "zh-tw", + "gl": "tw", + } + if start: + search_params["start"] = start + response = client.search(search_params) + if isinstance(response, str): + response_data = json.loads(response) + elif isinstance(response, dict): + response_data = response + elif hasattr(response, "as_dict"): + response_data = response.as_dict() + else: + response_data = dict(response) + if not isinstance(response_data, dict): + raise TypeError("SerpApi 回傳格式不是 JSON 物件") + if response_data.get("error"): + raise RuntimeError(str(response_data["error"])) + + page_results = response_data.get("organic_results", []) + if not isinstance(page_results, list): + return [] + return [item for item in page_results if isinstance(item, dict)] + + +def search_ddgs( + query: str, + max_results: int = 10, +) -> list[dict[str, str]]: + """透過 DuckDuckGo 搜尋並回傳標準化結果。""" + from ddgs import DDGS + + with DDGS() as ddgs: + logger.info("DuckDuckGo 搜尋 query=%r max_results=%d", query, max_results) + raw_results = list( + ddgs.text( + query, + region="tw-tzh", + safesearch="off", + backend="duckduckgo, brave, google, bing, yahoo", + max_results=max_results, + ) + ) + + results = [ + { + "title": str(item.get("title", "")).strip(), + "snippet": str(item.get("body", "")).strip(), + } + for item in raw_results + if ( + str(item.get("title", "")).strip() + or str(item.get("body", "")).strip() + ) + ] + return results + + +def search_tavily( + query: str, + max_results: int = 10 +) -> list[dict[str, str]]: + """透過 Tavily 搜尋並回傳標準化結果。""" + api_key = settings.TAVILY_SEARCH_API_KEY.get_secret_value() or os.getenv( + "TAVILY_SEARCH_API_KEY", + "", + ) + if not api_key: + raise RuntimeError("尚未設定 TAVILY_SEARCH_API_KEY") + + logger.info("Tavily 搜尋 query=%r max_results=%d", query, max_results) + response = TavilyClient(api_key=api_key).search( + query=query, + max_results=max_results, + country=settings.SEARCH_COUNTRY, + include_domains=settings.SEARCH_DOMAIN, + exclude_domains=settings.EXCLUDE_DOMAIN, + timeout=settings.SEARCH_TIMEOUT_SECONDS, + ) + + results = [ + { + "title": str(item.get("title", "")).strip(), + "snippet": str(item.get("content", "")).strip(), + } + for item in response.get("results", []) + if isinstance(item, dict) + if ( + str(item.get("title", "")).strip() + or str(item.get("content", "")).strip() + ) + ] + return results + + +# 搜尋工具名稱到函式的映射 +SEARCH_FUNCTIONS: dict[str, Any] = { + "serpapi": search_serpapi, + "tavily": search_tavily, + "ddgs": search_ddgs, +} + +# 搜尋工具分階段執行:同一階段內並行呼叫,合格結果不足時才進入下一階段 +SEARCH_TOOL_STAGES: tuple[tuple[str, ...], ...] = ( + ("serpapi", "tavily"), + ("ddgs",), +) diff --git a/backend/services/image_price_service/product/__init__.py b/backend/services/image_price_service/product/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/backend/services/image_price_service/product/__init__.py @@ -0,0 +1 @@ + diff --git a/backend/services/image_price_service/product/patterm_identifier.py b/backend/services/image_price_service/product/patterm_identifier.py new file mode 100644 index 0000000000000000000000000000000000000000..9930d016278f0c22cdcfe91abbe403dca903103f --- /dev/null +++ b/backend/services/image_price_service/product/patterm_identifier.py @@ -0,0 +1,205 @@ +"""使用品牌對照表、型號正則表達式與標題規則辨識商品。""" + +import logging +import re + +from backend.services.dto.price_analysis import ProductIdentification + +logger = logging.getLogger(__name__) + +_GENERIC_MODEL_RE = re.compile( + ( + r"(? str | None: + """依已知品牌關鍵字回傳標準化品牌名稱。""" + lowered = text.lower() + for token, canonical in _KNOWN_BRANDS.items(): + if token in lowered or token in text: + return canonical + + text = re.sub(r"\s+", "", text) + model_match = re.search(model, text, flags=re.IGNORECASE) if model else None + logger.info(f"Model match found: {model_match}, {model}, {text}") + text_before_model = text[:model_match.start()] if model_match else text + + return text_before_model + + def _extract_generic_model(self, text: str) -> str | None: + """擷取並排序可能的英數型號,排除價格與一般數字。""" + model_text = text + for brand_token in _KNOWN_BRANDS: + token_pattern = ( + rf"(? str: + """從 OCR 文字選出可能的商品標題,必要時補上品牌名稱。""" + lines = [ + re.sub(r"[^\S\r\n]+", " ", line).strip() + for line in text.splitlines() + if line.strip() + ] + if not lines: + return f"{brand} 商品" if brand else "一般商品" + + for line in lines[:6]: + if any(stop in line for stop in _GENERIC_STOPWORDS): + continue + + # 截斷價格與促銷欄位,只保留前方較接近商品標題的文字。 + line = re.split(r"\$|nt\$|twd|售價|特價|優惠價", line, flags=re.IGNORECASE)[0].strip() + if len(line) >= 6: + if brand and brand.lower() not in line.lower(): + return f"{brand} {line[:40]}" + return line[:40] + + return f"{brand} 商品" if brand else "一般商品" + + def identify_product(self, text: str) -> ProductIdentification: + """將 OCR 文字轉成基本商品資訊。""" + model = self._extract_generic_model(text) + brand = self._extract_brand(text, model) + product_name = self._extract_generic_product_name(text, brand) + + if brand and model: + brand_model = f"{brand} {model}" + elif brand: + brand_model = f"{brand}" + elif model: + brand_model = model + else: + brand_model = "未知型號" + + return ProductIdentification(product_name=product_name, brand_model=brand_model, market_price=0) +pattern_identifier = PatternIdentifier() diff --git a/backend/services/image_price_service/product/product_identifier.py b/backend/services/image_price_service/product/product_identifier.py new file mode 100644 index 0000000000000000000000000000000000000000..9e3bb6aec8f2189a06f3541f64cb8ce38d43d900 --- /dev/null +++ b/backend/services/image_price_service/product/product_identifier.py @@ -0,0 +1,212 @@ +"""協調本地資料、規則辨識與 AI 正規化,並建立台灣市場查價搜尋詞。""" + +import json +import logging +from typing import Any + +from groq import APIError + +from backend.repository.market_price_repository import ( + MarketPriceRepository, market_price_repository) +from backend.services.dto.price_analysis import ProductIdentification +from backend.services.image_price_service.product.patterm_identifier import \ + pattern_identifier +from backend.services.image_price_service.product.product_info_extractor import ( + ProductInfoExtractor, create_product_info_extractor) + +logging = logging.getLogger(__name__) + +PRODUCT_NORMALIZATION_SYSTEM_PROMPT = """ +你是商品資訊正規化助手。請根據購物平台 OCR 文字,辨識並整理商品資訊。 + +規則: +1. 修正明顯 OCR 錯字,但不可捏造無法確認的型號或規格。 +2. 刊登價格、運費、折扣與分期金額都不是商品規格,不得放入 known_specs。 +3. product_name 使用一般消費者可理解的標準商品名稱。 +4. brand_model 必須使用「品牌 + 型號」;無法確認時填「未知型號」。 +5. known_specs 放入容量、尺寸、解析度、顏色、版本、技術規格等已確認資訊 +6. 不要建立搜尋詞,也不要查詢價格。 + +只回傳以下 JSON,不要加入 Markdown 或額外說明: +{ + "product_name": "標準商品名稱", + "brand_model": "品牌 型號", + "known_specs": ["已確認的規格"] +} +""".strip() + + +class ProductIdentifier: + """辨識商品資訊並建立查價搜尋詞。""" + + def __init__( + self, + market_repo: MarketPriceRepository | None = None, + info_extractor: ProductInfoExtractor | None = None, + ) -> None: + """建立商品辨識器。""" + self._market_repo = market_repo or market_price_repository + self._info_extractor = info_extractor + + def identify(self, text: str) -> ProductIdentification: + """辨識商品並回傳正規化資訊。""" + product_name, brand_model, market_price = self._market_repo.find_by_text( + text + ) + if market_price > 0: + return self._build_identification( + product_name=product_name, + brand_model=brand_model, + market_price=market_price, + ) + + pattern_product = pattern_identifier.identify_product(text) + user_prompt = json.dumps( + { + "ocr_text": text, + "rule_hint": { + "product_name": pattern_product.product_name, + "brand_model": pattern_product.brand_model, + }, + }, + ensure_ascii=False, + ) + + try: + normalized = self._get_info_extractor().complete_json( + system_prompt=PRODUCT_NORMALIZATION_SYSTEM_PROMPT, + user_prompt=user_prompt, + ) + normalized_name = str( + normalized.get("product_name", pattern_product.product_name) + ).strip() + normalized_model = str( + normalized.get("brand_model", pattern_product.brand_model) + ).strip() + known_specs = self._normalize_specs( + normalized.get("known_specs", []) + ) + return self._build_identification( + product_name=normalized_name or pattern_product.product_name, + brand_model=normalized_model or pattern_product.brand_model, + known_specs=known_specs, + ) + except (APIError, ValueError, TypeError) as e: + logging.warning( + "商品正規化失敗,使用 Pattern 規則辨識結果。錯誤: %s", e + ) + return self._build_identification( + product_name=pattern_product.product_name, + brand_model=pattern_product.brand_model, + ) + + def _get_info_extractor(self) -> ProductInfoExtractor: + """取得或建立商品資訊擷取器。""" + if self._info_extractor is None: + self._info_extractor = create_product_info_extractor() + return self._info_extractor + + def _build_identification( + self, + *, + product_name: str, + brand_model: str, + market_price: int = 0, + known_specs: list[str] | None = None, + ) -> ProductIdentification: + """建立商品辨識結果與搜尋詞。""" + specs = known_specs or [] + return ProductIdentification( + product_name=product_name, + brand_model=brand_model, + known_specs=specs, + search_query=self._build_price_search_query( + product_name, + brand_model, + specs, + ), + market_price=market_price, + ) + + @staticmethod + def _normalize_specs(value: Any) -> list[str]: + """整理規格並移除重複值。""" + if not isinstance(value, list): + return [] + + specs: list[str] = [] + seen: set[str] = set() + for item in value: + spec = str(item).strip() + normalized = spec.casefold() + if not spec or normalized in seen: + continue + seen.add(normalized) + specs.append(spec) + return specs + + @staticmethod + def _build_price_search_query( + product_name: str, + brand_model: str, + known_specs: list[str], + ) -> str: + """使用完整品名與少量關鍵規格建立搜尋詞。""" + unknown_values = { + "", + "一般商品", + "未知商品", + "未知型號", + "未知品牌型號", + } + unknown_model_markers = ("未知型號", "未知品牌型號") + normalized_name = " ".join(str(product_name).split()) + normalized_model = " ".join(str(brand_model).split()) + + name_is_known = normalized_name not in unknown_values + model_is_known = ( + normalized_model not in unknown_values + and not any( + marker in normalized_model + for marker in unknown_model_markers + ) + ) + + if name_is_known and model_is_known: + if normalized_model.casefold() in normalized_name.casefold(): + search_name = normalized_name + else: + model_tokens = { + token.casefold() + for token in normalized_model.split() + } + description_tokens = [ + token + for token in normalized_name.split() + if token.casefold() not in model_tokens + ] + search_name = " ".join( + [normalized_model, *description_tokens] + ) + elif name_is_known: + search_name = normalized_name + elif model_is_known: + search_name = normalized_model + else: + return "" + + additional_specs: list[str] = [] + normalized_search_name = search_name.casefold() + for value in known_specs: + spec = " ".join(str(value).split()) + if not spec or spec.casefold() in normalized_search_name: + continue + additional_specs.append(spec) + + if additional_specs: + search_name = f"{search_name} {' '.join(additional_specs[:2])}" + + return f"{search_name} 價格" + + +product_identifier = ProductIdentifier() diff --git a/backend/services/image_price_service/product/product_info_extractor.py b/backend/services/image_price_service/product/product_info_extractor.py new file mode 100644 index 0000000000000000000000000000000000000000..37475f00f327db54c072b4128fa67e45bf70b778 --- /dev/null +++ b/backend/services/image_price_service/product/product_info_extractor.py @@ -0,0 +1,105 @@ +"""使用 LLM 擷取並正規化商品資訊。""" + +import json +from typing import Any + +from langchain_core.language_models.chat_models import BaseChatModel +from langchain_core.messages import HumanMessage, SystemMessage + +from backend.core.config import settings +from backend.providers import groq_provider + + +class ProductInfoExtractor: + """協調 LLM 結構化輸出,用於商品資訊擷取。""" + + def __init__( + self, + llm: BaseChatModel, + ) -> None: + """建立商品資訊擷取器。""" + self._llm = llm + + def complete_json( + self, + *, + system_prompt: str, + user_prompt: str, + ) -> dict[str, Any]: + """呼叫模型並解析 JSON 回覆。""" + response = self._llm.invoke( + [ + SystemMessage(content=system_prompt), + HumanMessage(content=user_prompt), + ] + ) + return self._parse_json_object(self._message_text(response)) + + @staticmethod + def _message_text(message: Any) -> str: + """從模型訊息取出文字內容。""" + content = getattr(message, "content", "") + if isinstance(content, str): + if content.strip(): + return content + elif isinstance(content, list): + text_parts: list[str] = [] + for block in content: + if isinstance(block, str): + text_parts.append(block) + elif isinstance(block, dict): + block_text = block.get("text") or block.get("content") + if isinstance(block_text, str): + text_parts.append(block_text) + if text_parts: + return "\n".join(text_parts) + + additional_kwargs = getattr(message, "additional_kwargs", {}) + if isinstance(additional_kwargs, dict): + reasoning_content = additional_kwargs.get("reasoning_content") + if isinstance(reasoning_content, str) and reasoning_content.strip(): + return reasoning_content + + return str(content) + + @staticmethod + def _parse_json_object(text: str) -> dict[str, Any]: + """從模型文字中解析第一個 JSON 物件。""" + cleaned = text.strip() + if not cleaned: + raise ValueError("Groq 未回傳有效 JSON 物件(回覆為空)") + + decoder = json.JSONDecoder() + last_error: json.JSONDecodeError | None = None + for start, character in enumerate(cleaned): + if character != "{": + continue + try: + parsed, _ = decoder.raw_decode(cleaned[start:]) + except json.JSONDecodeError as error: + last_error = error + continue + if isinstance(parsed, dict): + return parsed + + if last_error is not None: + raise ValueError( + f"Groq 回傳 JSON 解析失敗:{last_error.msg}" + ) from last_error + else: + raise ValueError("Groq 未回傳有效 JSON 物件") + + +def create_product_info_extractor() -> ProductInfoExtractor: + """建立使用 Groq 的商品資訊擷取器。""" + if not groq_provider.is_configured(): + raise RuntimeError("尚未設定 GROQ_API_KEY") + + llm = groq_provider.create( + model=settings.NORMALIZER_MODEL, + temperature=0.1, + reasoning_effort=settings.NORMALIZER_REASONING_EFFORT, + timeout=settings.PRICE_LLM_TIMEOUT_SECONDS, + max_retries=settings.PRICE_LLM_MAX_RETRIES, + ) + return ProductInfoExtractor(llm=llm) diff --git a/backend/services/image_price_service/risk/__init__.py b/backend/services/image_price_service/risk/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/backend/services/image_price_service/risk/__init__.py @@ -0,0 +1 @@ + diff --git a/backend/services/image_price_service/risk/condition_reviewer.py b/backend/services/image_price_service/risk/condition_reviewer.py new file mode 100644 index 0000000000000000000000000000000000000000..a9a32dfbc125e5d5361498cd2fc247e6139d5540 --- /dev/null +++ b/backend/services/image_price_service/risk/condition_reviewer.py @@ -0,0 +1,81 @@ +"""以受限狀態證據執行結構化 LLM 商品狀態複核。""" + +import json +import threading +from typing import Any + +from backend.core.config import settings +from backend.providers import groq_provider +from backend.services.dto.price_analysis import DeepAnalysisReview + +_REVIEW_CONTEXT_FIELDS = ( + "product_name", + "text", + "condition", + "condition_detail", + "condition_source_text", + "condition_extraction_confidence", +) + +_SYSTEM_PROMPT = """ +你是商品狀態複核器。請同時查看輸入中的完整刊登文字 text、condition_detail 與 +condition_source_text,確認或修正商品狀態,並將狀態正規化為 new、used 或 unknown。 +condition_evidence 必須逐字取自這三個欄位其中之一;證據不足時回傳 unknown。 +不得推測價格、風險、商品匹配或市場候選,也不得使用輸入以外的資訊。 +""".strip() + + +class ConditionReviewer: + """商品狀態複核器""" + + def __init__( + self, + *, + model_name: str, + api_key: str | None = None, + ) -> None: + self._api_key = api_key.strip() if api_key else None + self._model_name = model_name.strip() + # 重用同一個 client 以沿用 HTTP 連線,避免每次複核都重新 TLS 握手 + self._structured_llm: Any | None = None + self._llm_lock = threading.Lock() + + def _get_structured_llm(self) -> Any: + if self._structured_llm is not None: + return self._structured_llm + with self._llm_lock: + if self._structured_llm is None: + llm = groq_provider.create( + model=self._model_name, + temperature=0, + reasoning_effort="none", + timeout=settings.PRICE_LLM_TIMEOUT_SECONDS, + max_retries=settings.PRICE_LLM_MAX_RETRIES, + api_key=self._api_key, + ) + self._structured_llm = llm.with_structured_output(DeepAnalysisReview) + return self._structured_llm + + def __call__( + self, + context: dict[str, object], + ) -> DeepAnalysisReview | dict[str, object] | None: + if ( + not self._model_name + or not groq_provider.is_configured(self._api_key) + ): + return None + + limited_context = { + field: context.get(field) + for field in _REVIEW_CONTEXT_FIELDS + } + return self._get_structured_llm().invoke( + ( + f"{_SYSTEM_PROMPT}\n\n" + f"輸入:{json.dumps(limited_context, ensure_ascii=False)}" + ) + ) + + +__all__ = ["ConditionReviewer"] diff --git a/backend/services/image_price_service/risk/fusion_decision_engine.py b/backend/services/image_price_service/risk/fusion_decision_engine.py new file mode 100644 index 0000000000000000000000000000000000000000..54760a48d9316c5b2b4e035fa3f3ef8784bc2ff9 --- /dev/null +++ b/backend/services/image_price_service/risk/fusion_decision_engine.py @@ -0,0 +1,611 @@ +"""依市場價格區間、商品狀態及選用的狀態複核產生風險決策。""" + +import re +from collections.abc import Callable +from dataclasses import dataclass, replace +from typing import Any, Literal + +from pydantic import ValidationError + +from backend.services.dto.price_analysis import ( + DeepAnalysisReview, + MarketPriceEstimate, + MarketPriceSource, + RiskLabel, +) +from backend.services.image_price_service.domain.models import MarketplaceCondition +from backend.services.image_price_service.domain.policy import ( + DEFAULT_PRICE_RISK_POLICY, + PriceRiskPolicy, +) + +ConditionReviewer = Callable[ + [dict[str, object]], + DeepAnalysisReview | dict[str, object] | None, +] +PriceDirection = Literal["within", "under", "over"] +ConditionReviewStatus = Literal["skipped", "confirmed", "corrected", "unavailable"] + + +@dataclass(frozen=True) +class ConditionReviewOutcome: + """查價前的狀態複核結果;condition/condition_detail 為查價與決策採用的最終品況。 + + status:skipped=不需複核、confirmed=LLM 確認原品況、corrected=採用 LLM 修正、 + unavailable=需要複核但 LLM 失敗、證據不可追溯或不得覆寫高信心品況。 + """ + + condition: MarketplaceCondition + condition_detail: str + status: ConditionReviewStatus = "skipped" + review: DeepAnalysisReview | None = None + + +class FusionDecisionEngine: + """以同一份 PriceRiskPolicy 執行價格規則與狀態複核。""" + + def __init__( + self, + policy: PriceRiskPolicy = DEFAULT_PRICE_RISK_POLICY, + *, + condition_reviewer: ConditionReviewer | None = None, + ) -> None: + """注入 domain policy 與選用的狀態複核器。""" + self.policy = policy + self._condition_reviewer = condition_reviewer + + @staticmethod + def _score_from_bands( + value: float | int, + bands: tuple[tuple[float | int, int], ...], + ) -> int: + """依含等號的上界級距取得分數。""" + for threshold, score in bands: + if value <= threshold: + return score + return bands[-1][1] + + def _price_components( + self, + selling_price: int, + market_estimate: MarketPriceEstimate, + ) -> dict[str, float | int | str]: + """以實際被違反的市場邊界計算相對差距與絕對差額。""" + if selling_price <= 0: + raise ValueError("刊登價格必須大於零") + if not self._is_valid_market_estimate(market_estimate): + raise ValueError("市場價格區間無效或證據不足") + + if selling_price < market_estimate.low_price: + direction: PriceDirection = "under" + boundary = market_estimate.low_price + absolute_gap = boundary - selling_price + relative_gap = absolute_gap / boundary + relative_score = self._score_from_bands( + relative_gap, + self.policy.underprice_relative_bands, + ) + score_cap = self.policy.maximum_score + elif selling_price > market_estimate.high_price: + direction = "over" + boundary = market_estimate.high_price + absolute_gap = selling_price - boundary + relative_gap = absolute_gap / boundary + relative_score = self._score_from_bands( + relative_gap, + self.policy.overprice_relative_bands, + ) + score_cap = self.policy.overprice_score_cap + else: + return { + "direction": "within", + "boundary": 0, + "absolute_gap": 0, + "relative_gap": 0.0, + "relative_score": 0, + "absolute_bonus": 0, + "score": 0.0, + } + + absolute_bonus = self._score_from_bands( + absolute_gap, + self.policy.absolute_gap_bands, + ) + if market_estimate.reference_mode == "median_low_sample": + score_cap = min( + score_cap, + self.policy.small_sample_score_cap, + ) + score = min( + relative_score + absolute_bonus, + score_cap, + self.policy.maximum_score, + ) + return { + "direction": direction, + "boundary": boundary, + "absolute_gap": absolute_gap, + "relative_gap": relative_gap, + "relative_score": relative_score, + "absolute_bonus": absolute_bonus, + "score": float(score), + } + + def _calculate_price_score( + self, + selling_price: int, + market_estimate: MarketPriceEstimate, + ) -> float: + """依市場區間與 policy 計算純價格風險分數。""" + return float( + self._price_components( + selling_price, + market_estimate, + )["score"] + ) + + def _risk_label_from_score(self, score: float) -> RiskLabel: + """使用 policy 的 LOW/MEDIUM 邊界轉換風險標籤。""" + if score > self.policy.medium_score_max: + return "HIGH" + if score > self.policy.low_score_max: + return "MEDIUM" + return "LOW" + + def _has_price_risk( + self, + selling_price: int, + market_estimate: MarketPriceEstimate, + ) -> bool: + """判斷結構化價格結果是否已達 MEDIUM/HIGH。""" + try: + score = self._calculate_price_score( + selling_price, + market_estimate, + ) + except ValueError: + return False + return score > self.policy.low_score_max + + def _enforce_price_risk( + self, + decision: dict[str, Any], + selling_price: int, + market_estimate: MarketPriceEstimate, + ) -> dict[str, Any]: + """只對完整 IQR、高可信且已達 HIGH 的最終規則套用硬性下限。""" + price_score = self._calculate_price_score( + selling_price, + market_estimate, + ) + if ( + market_estimate.reference_mode != "iqr" + or market_estimate.confidence + < self.policy.minimum_market_confidence + or price_score <= self.policy.medium_score_max + ): + return decision + + decision_score = decision.get("risk_score") + enforced_score = ( + max(float(decision_score), price_score) + if isinstance(decision_score, (int, float)) + else price_score + ) + enforced_score = min( + enforced_score, + float(self.policy.maximum_score), + ) + evidence = [str(item) for item in decision.get("evidence") or []] + price_evidence = "完整 IQR 市場資料的 HIGH 價格規則下限" + if price_evidence not in evidence: + evidence.append(price_evidence) + return { + **decision, + "risk_label": self._risk_label_from_score(enforced_score), + "risk_score": enforced_score, + "evidence": evidence, + } + + def _requires_condition_review( + self, + condition: MarketplaceCondition, + condition_extraction_confidence: float, + text: str, + condition_detail: str, + condition_source_text: str, + condition_has_conflict: bool, + ) -> bool: + """只在品況不確定且有可複核的狀態證據時呼叫 LLM。""" + has_reviewable_evidence = any( + value.strip() + for value in (text, condition_detail, condition_source_text) + ) + condition_needs_review = ( + condition == MarketplaceCondition.UNKNOWN + or condition_extraction_confidence + <= self.policy.condition_llm_correction_max_confidence + or condition_has_conflict + ) + return has_reviewable_evidence and condition_needs_review + + def _alt_deep_result( + self, + base_result: dict[str, Any], + ) -> dict[str, Any]: + """LLM 狀態複核無效時原樣保留價格規則結果。""" + score = base_result.get("risk_score") + if not isinstance(score, (int, float)): + raise ValueError("_alt_deep_result() 需要數字規則分數") + label = self._risk_label_from_score(float(score)) + + evidence = [str(item) for item in base_result.get("evidence") or []] + fallback_evidence = "LLM 狀態複核不可用,保留原始價格規則結果" + if fallback_evidence not in evidence: + evidence.append(fallback_evidence) + reason = str(base_result.get("reason") or "價格規則評估完成") + return { + **base_result, + "risk_label": label, + "risk_score": float(score), + "reason": f"{reason};未套用任何狀態修正", + "evidence": evidence, + "confidence": float(base_result.get("confidence") or 0.0), + "decision_layer": "llm_simulated", + } + + def _call_llm_deep_analysis( + self, + product_context: dict[str, object], + ) -> DeepAnalysisReview | None: + """呼叫注入的狀態複核器並以 DeepAnalysisReview 驗證輸出。""" + if self._condition_reviewer is None: + return None + try: + payload = self._condition_reviewer(product_context) + if payload is None: + return None + if isinstance(payload, DeepAnalysisReview): + return payload + return DeepAnalysisReview.model_validate(payload) + except (ValidationError, TypeError, ValueError): + return None + except Exception: + return None + + def _is_valid_market_estimate( + self, + estimate: MarketPriceEstimate, + ) -> bool: + """驗證決策使用的市場區間、資料量及可信度。""" + maximum = self.policy.maximum_supported_price + return ( + estimate.status == "success" + and estimate.source != "not_evaluated" + and estimate.sample_count >= self.policy.minimum_market_samples + and estimate.site_count >= self.policy.minimum_market_sites + and estimate.confidence >= self.policy.minimum_market_confidence + and 0 < estimate.low_price <= maximum + and 0 < estimate.median_price <= maximum + and 0 < estimate.high_price <= maximum + and estimate.low_price <= estimate.median_price <= estimate.high_price + ) + + def _rule_result_for_estimate( + self, + selling_price: int, + estimate: MarketPriceEstimate, + ) -> dict[str, Any]: + components = self._price_components(selling_price, estimate) + score = float(components["score"]) + direction = str(components["direction"]) + if direction == "within": + reason = ( + f"刊登價格 {selling_price} 位於" + f" {estimate.low_price}~{estimate.high_price} 市場區間," + "相對差距 0.00%、絕對差額 0" + ) + price_evidence = ( + "刊登價格位於有效市場區間:相對分數 0、" + "絕對差額加分 0" + ) + else: + direction_text = "低於" if direction == "under" else "高於" + reason = ( + f"刊登價格 {selling_price} {direction_text}市場邊界" + f" {components['boundary']},相對差距" + f" {float(components['relative_gap']):.2%}、絕對差額" + f" {components['absolute_gap']}" + ) + price_evidence = ( + f"{direction_text}行情:相對分數" + f" {components['relative_score']}、絕對差額加分" + f" {components['absolute_bonus']}" + ) + + return { + "risk_label": self._risk_label_from_score(score), + "risk_score": score, + "reason": reason, + "evidence": [ + ( + f"{estimate.condition.value} 市場區間" + f" {estimate.low_price}~{estimate.high_price}" + ), + price_evidence, + ], + "confidence": estimate.confidence, + "market_price_source": estimate.source, + "reference_mode": estimate.reference_mode, + "condition": estimate.condition, + } + + def _evaluate_market_paths( + self, + *, + selling_price: int, + condition: MarketplaceCondition, + market_estimates: tuple[MarketPriceEstimate, ...], + ) -> dict[str, Any]: + if len(market_estimates) != 1: + return self._decision_error( + "MARKET_PRICE_INSUFFICIENT", + "商品狀態必須且只能取得一個相同狀態的有效市場結果", + condition=condition, + ) + estimate = market_estimates[0] + if ( + estimate.condition != condition + or not self._is_valid_market_estimate(estimate) + or ( + condition is MarketplaceCondition.UNKNOWN + and estimate.sample_count <= self.policy.minimum_market_samples + ) + ): + return self._market_estimate_error( + market_estimates, + condition, + ) + return self._rule_result_for_estimate(selling_price, estimate) + + def _market_estimate_error( + self, + estimates: tuple[MarketPriceEstimate, ...], + condition: MarketplaceCondition, + ) -> dict[str, Any]: + has_candidate_data = any( + estimate.status == "insufficient" + or estimate.sample_count > 0 + for estimate in estimates + ) + return self._decision_error( + ( + "MARKET_PRICE_INSUFFICIENT" + if has_candidate_data + else "MARKET_PRICE_NOT_FOUND" + ), + "市場價格資料不足或區間無效", + condition=condition, + ) + + @staticmethod + def _decision_error( + error_code: str, + reason: str, + *, + condition: MarketplaceCondition = MarketplaceCondition.UNKNOWN, + ) -> dict[str, Any]: + return { + "risk_label": "UNKNOWN", + "risk_score": "未知", + "reason": reason, + "evidence": [], + "confidence": 0.0, + "decision_layer": "decision_error", + "market_price_source": "not_evaluated", + "condition": condition, + "error_code": error_code, + } + + @staticmethod + def _normalize_review_text(value: str) -> str: + return re.sub(r"[\s・·::_\-]+", "", value).casefold() + + def _review_is_acceptable( + self, + review: DeepAnalysisReview, + text: str, + condition_detail: str, + condition_source_text: str, + ) -> bool: + evidence = self._normalize_review_text(review.condition_evidence) + sources = tuple( + self._normalize_review_text(value) + for value in (text, condition_detail, condition_source_text) + if value.strip() + ) + return ( + review.review_confidence + >= self.policy.llm_review_min_confidence + and bool(evidence) + and any(evidence in source for source in sources) + ) + + def _apply_review( + self, + base_result: dict[str, Any], + condition_detail: str, + condition_review: ConditionReviewOutcome | None, + ) -> dict[str, Any]: + """依查價前的狀態複核結果標註決策層、理由與證據,不改變價格分數。""" + if condition_review is None or condition_review.status == "skipped": + return { + **base_result, + "decision_layer": "fast", + "condition_detail": condition_detail, + } + + review = condition_review.review + if condition_review.status == "unavailable" or review is None: + return { + **self._alt_deep_result(base_result), + "condition_detail": condition_detail, + } + + review_reason = ( + "LLM 已確認原始商品狀態" + if condition_review.status == "confirmed" + else f"LLM 狀態複核:{review.reason}" + ) + return { + **base_result, + "reason": f"{base_result['reason']};{review_reason}", + "evidence": [ + *[str(item) for item in base_result.get("evidence") or []], + review.condition_evidence, + ], + "confidence": min( + float(base_result.get("confidence") or 0.0), + review.review_confidence, + ), + "decision_layer": "llm", + "condition_detail": condition_detail, + } + + def _enforce_final_decision( + self, + decision: dict[str, Any], + *, + selling_price: int, + condition: MarketplaceCondition, + market_estimates: tuple[MarketPriceEstimate, ...], + ) -> dict[str, Any]: + """在狀態複核完成後,才對最終單一路徑套用價格硬性下限。""" + if ( + condition == MarketplaceCondition.UNKNOWN + or len(market_estimates) != 1 + or not self._is_valid_market_estimate(market_estimates[0]) + ): + return decision + return self._enforce_price_risk( + decision, + selling_price, + market_estimates[0], + ) + + def review_condition( + self, + *, + product_name: str, + text: str, + condition: MarketplaceCondition, + condition_detail: str, + condition_source_text: str, + condition_extraction_confidence: float, + condition_has_conflict: bool = False, + ) -> ConditionReviewOutcome: + """在查價前複核品況,讓後續只需以最終品況查價一次。""" + original = ConditionReviewOutcome( + condition=condition, + condition_detail=condition_detail, + ) + if self._condition_reviewer is None or not self._requires_condition_review( + condition, + condition_extraction_confidence, + text, + condition_detail, + condition_source_text, + condition_has_conflict, + ): + return original + + review = self._call_llm_deep_analysis( + { + "product_name": " ".join(product_name.split()), + "text": " ".join(text.split()), + "condition": condition.value, + "condition_detail": condition_detail, + "condition_source_text": condition_source_text, + "condition_extraction_confidence": ( + condition_extraction_confidence + ), + } + ) + if review is None or not self._review_is_acceptable( + review, + text, + condition_detail, + condition_source_text, + ): + return replace(original, status="unavailable") + + same_detail = ( + self._normalize_review_text(review.condition_detail) + == self._normalize_review_text(condition_detail) + ) + if review.reviewed_condition == condition and same_detail: + return replace(original, status="confirmed", review=review) + if ( + condition_extraction_confidence + > self.policy.condition_llm_correction_max_confidence + ): + return replace(original, status="unavailable") + return ConditionReviewOutcome( + condition=review.reviewed_condition, + condition_detail=review.condition_detail, + status="corrected", + review=review, + ) + + def evaluate( + self, + *, + product_name: str, + selling_price: int, + market_estimates: tuple[MarketPriceEstimate, ...] = (), + condition: MarketplaceCondition = MarketplaceCondition.UNKNOWN, + condition_detail: str = "", + condition_review: ConditionReviewOutcome | None = None, + brand_model: str = "", + market_price: int | None = None, + market_price_source: MarketPriceSource = "not_evaluated", + ) -> dict[str, Any]: + """以最終品況的市場區間執行價格規則,並標註查價前的狀態複核結果。""" + del brand_model, market_price, market_price_source + + if not product_name.strip(): + return self._decision_error( + "PRODUCT_NAME_MISSING", + "缺少商品標題,無法執行價格判斷", + condition=condition, + ) + if selling_price <= 0: + return self._decision_error( + "LISTED_PRICE_INVALID", + "刊登價格必須大於零", + condition=condition, + ) + if selling_price > self.policy.maximum_supported_price: + return self._decision_error( + "PRICE_OUT_OF_SUPPORTED_RANGE", + "刊登價格超出服務支援範圍", + condition=condition, + ) + + base_result = self._evaluate_market_paths( + selling_price=selling_price, + condition=condition, + market_estimates=tuple(market_estimates), + ) + if base_result.get("decision_layer") == "decision_error": + return base_result + + return self._enforce_final_decision( + self._apply_review(base_result, condition_detail, condition_review), + selling_price=selling_price, + condition=condition, + market_estimates=market_estimates, + ) + + +fusion_decision_engine = FusionDecisionEngine() diff --git a/backend/services/phone_service/__init__.py b/backend/services/phone_service/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..19c587b65481e7f0b835676e6d620c7088f0a9f2 --- /dev/null +++ b/backend/services/phone_service/__init__.py @@ -0,0 +1,3 @@ +from backend.services.phone_service.phone_service import phone_service + +__all__ = ["phone_service"] diff --git a/backend/services/phone_service/phone_family.py b/backend/services/phone_service/phone_family.py new file mode 100644 index 0000000000000000000000000000000000000000..c0a421917aab4788b16b4a5b0f8b441866053807 --- /dev/null +++ b/backend/services/phone_service/phone_family.py @@ -0,0 +1,388 @@ +"""以圖論分析通報資料,找出電話號碼之間的詐騙家族關聯。""" + +import logging +import math +from collections import Counter, defaultdict +from datetime import datetime +from typing import Any + +import networkx as nx + +from backend.persistence.mysql_connection import engine as db_engine +from backend.repository.family_repository import family_repository +from backend.utils.phone_normalizer import _is_nan, normalize + +logger = logging.getLogger(__name__) + +__all__ = ["analyze_family", "build_phone_genealogy"] + +MIN_LINK_SCORE = 40 +SAME_REPORTER_WINDOW_SECONDS = 2 * 3600 +TRANSFER_ASSET_LABELS = {2: "LINE", 3: "URL", 4: "TG"} +DATETIME_FORMATS = ( + "%Y-%m-%d %H:%M:%S", "%Y-%m-%dT%H:%M:%S", "%Y-%m-%d", "%Y/%m/%d %H:%M", "%Y/%m/%d", +) + +# {(號碼A, 號碼B): [(通報者, 該時窗內兩號碼的通報次數), ...]} +ReportPairs = dict[tuple[str, str], list[tuple[str, int]]] + + +# --- 1. 基礎格式與工具 --- + +def _safe_int(value: Any, default: int = 0) -> int: + """轉成 int,NaN 或無法轉換時回傳預設值。""" + if value is None or _is_nan(value): + return default + try: + return int(value) + except (ValueError, TypeError): + return default + + +def _levenshtein(a: str, b: str) -> int: + """計算字串編輯距離(用於尾數比對)。""" + if a == b: + return 0 + if not a or not b: + return len(a) or len(b) + prev = list(range(len(b) + 1)) + for i, ca in enumerate(a, start=1): + cur = [i] + [0] * len(b) + for j, cb in enumerate(b, start=1): + cur[j] = min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + (ca != cb)) + prev = cur + return prev[-1] + + +def _safe_datetime(value: Any) -> datetime | None: + if isinstance(value, datetime): + return value + if not value or _is_nan(value) or str(value).lower() in ("none", "nan", ""): + return None + for fmt in DATETIME_FORMATS: + try: + return datetime.strptime(str(value), fmt) + except ValueError: + continue + return None + + +def _normalize_record(record: dict[str, Any]) -> dict[str, Any] | None: + """把資料庫列(中文欄位)轉成評分用的標準格式;號碼無效時回傳 None。""" + phone = normalize(record.get("電話號碼")) + if not phone: + return None + + transfer_type = _safe_int(record.get("轉介類型"), 0) + raw_content = record.get("轉介內容") + content = "" if raw_content is None or _is_nan(raw_content) else str(raw_content).strip() + if content.lower() in ("nan", "none"): + content = "" + if transfer_type == 1 and content: + content = normalize(content) + + return { + "phone_number": phone, + "reporter_id": normalize(record.get("通報者識別碼")), + "total_reports": _safe_int(record.get("通報次數"), 1), + "first_reported_at": _safe_datetime(record.get("首次通報時間")), + "last_reported_at": _safe_datetime(record.get("最後通報時間")), + "phone_type": str(record.get("通報標籤") or "其他"), + "transfer_type": transfer_type, + "transfer_content": content, + } + + +# --- 2. 同通報者短時間內通報多支號碼 --- + +def _list_report_events(phones: set[str]) -> dict[str, list[tuple[str, datetime]]]: + """讀取指定號碼的通報事件,依通報者分組並按時間排序;查詢失敗時視為沒有事件。""" + try: + raw_events = family_repository.list_fraud_report_events(phones, db_engine) + except Exception: + logger.warning("讀取通報事件失敗,略過同通報者關聯", exc_info=True) + return {} + + events: dict[str, list[tuple[str, datetime]]] = {} + for reporter, entries in raw_events.items(): + parsed = [ + (phone, time) + for phone, raw_time in entries + if phone and (time := _safe_datetime(raw_time)) + ] + if parsed: + events[reporter] = sorted(parsed, key=lambda item: item[1]) + return events + + +def _build_report_pairs(events: dict[str, list[tuple[str, datetime]]]) -> ReportPairs: + """找出同一通報者在 2 小時內通報的不同號碼配對,並記錄該時窗內的通報次數。""" + report_pairs: ReportPairs = defaultdict(list) + for reporter, reporter_events in events.items(): + for i, (left_phone, left_time) in enumerate(reporter_events): + for right_phone, right_time in reporter_events[i + 1:]: + if left_phone == right_phone: + continue + if abs((right_time - left_time).total_seconds()) > SAME_REPORTER_WINDOW_SECONDS: + continue + pair = (min(left_phone, right_phone), max(left_phone, right_phone)) + pair_report_count = sum( + 1 + for phone, time in reporter_events + if phone in pair + and abs((time - left_time).total_seconds()) <= SAME_REPORTER_WINDOW_SECONDS + ) + report_pairs[pair].append((reporter, pair_report_count)) + return report_pairs + + +# --- 3. 核心權重計算引擎 --- + +def _score_phone_pair( + left: dict[str, Any], right: dict[str, Any], report_pairs: ReportPairs +) -> tuple[int, list[str], tuple[str, str] | None]: + """依各項家族特徵計算兩支號碼的關聯分數。""" + if left.get("phone_type") == "商業騷擾" or right.get("phone_type") == "商業騷擾": + return 0, ["商業騷擾:禁止建立詐騙家族連結"], None + + score = 0 + reasons: list[str] = [] + directed_edge = None + l_phone = left["phone_number"] + r_phone = right["phone_number"] + + if left.get("transfer_type") == 1 and left.get("transfer_content") == r_phone: + score += 40 + reasons.append("直接電話轉介:A -> B") + directed_edge = (l_phone, r_phone) + elif right.get("transfer_type") == 1 and right.get("transfer_content") == l_phone: + score += 40 + reasons.append("直接電話轉介:B -> A") + directed_edge = (r_phone, l_phone) + + if ( + left.get("transfer_type") == right.get("transfer_type") + and left.get("transfer_type") not in (0, None) + and left.get("transfer_content") + and left.get("transfer_content") == right.get("transfer_content") + ): + score += 40 + reasons.append(f"外部導流端點共用 ({left.get('transfer_content')})") + + # 手機為 10 碼、家用市話(無區碼)為 8 碼,兩者都適用同號段比對, + # 但長度不同時代表號碼類型不同,不應互相比對。 + same_segment = ( + l_phone[:6] == r_phone[:6] and len(l_phone) == len(r_phone) and len(l_phone) in (8, 10) + ) + tail_l, tail_r = l_phone[-4:], r_phone[-4:] + if ( + same_segment + and tail_l.isdigit() + and tail_r.isdigit() + and (abs(int(tail_l) - int(tail_r)) <= 3 or _levenshtein(tail_l, tail_r) == 1) + ): + score += 30 + reasons.append("同號段且尾碼物理接近") + + pair_key = (min(l_phone, r_phone), max(l_phone, r_phone)) + pair_stats = report_pairs.get(pair_key, []) if l_phone and r_phone else [] + if pair_stats: + supernode_strength = max((count for _, count in pair_stats), default=1) + decay = 1.0 / (1.0 + math.log1p(supernode_strength)) + score += int(25 * decay) + reasons.append("同通報者 2 小時內通報不同電話(已衰減)" if decay < 1 else "同通報者 2 小時內通報不同電話") + + l_first, r_first = left.get("first_reported_at"), right.get("first_reported_at") + l_last, r_last = left.get("last_reported_at"), right.get("last_reported_at") + if ( + l_first and r_first and l_last and r_last + and abs((l_first - r_first).total_seconds()) <= 24 * 3600 + ): + overlap = (min(l_last, r_last) - max(l_first, r_first)).total_seconds() + union = (max(l_last, r_last) - min(l_first, r_first)).total_seconds() + if union > 0 and overlap / union > 0.5: + score += 15 + reasons.append("生命週期同步") + + ltag, rtag = left.get("phone_type"), right.get("phone_type") + if ltag == rtag and ltag not in ("其他", "商業騷擾"): + score += 15 + reasons.append(f"標籤一致({ltag})") + elif {ltag, rtag} == {"約會交友", "假投資"}: + score += 10 + reasons.append("跨標籤上下游產業鏈 (約會+投資)") + + return score, reasons, directed_edge + + +# --- 4. 分析流程與分桶機制 --- + +def _list_candidate_pairs(processed: list[dict[str, Any]]) -> set[tuple[int, int]]: + """以號碼、號段、導流端點、通報者與通報週分桶,只比對同桶內的紀錄。""" + buckets: dict[str, list[int]] = defaultdict(list) + for idx, record in enumerate(processed): + phone = record["phone_number"] + buckets[f"num_{phone}"].append(idx) + buckets[f"p_{phone[:6]}"].append(idx) + if record["transfer_content"]: + # 直接轉介以目標號碼分桶,讓 A -> B 仍能配對 + prefix = "num" if record["transfer_type"] == 1 else "t" + buckets[f"{prefix}_{record['transfer_content']}"].append(idx) + if record["reporter_id"]: + buckets[f"r_{record['reporter_id']}"].append(idx) + if record["first_reported_at"]: + buckets[f"w_{record['first_reported_at'].strftime('%Y-W%U')}"].append(idx) + + candidate_pairs: set[tuple[int, int]] = set() + for members in buckets.values(): + for i, left in enumerate(members): + for right in members[i + 1:]: + candidate_pairs.add((min(left, right), max(left, right))) + return candidate_pairs + + +def _summarize_family( + family_id: str, graph: Any, nodes: set[str], processed: list[dict[str, Any]] +) -> dict[str, Any]: + """彙整單一家族的號段、標籤、轉介鏈、共用導流資產與時間範圍。""" + member_data = [r for r in processed if r["phone_number"] in nodes] + paths = [ + f"{d['directed_edge'][0]} -> {d['directed_edge'][1]}" + for _, _, d in graph.subgraph(nodes).edges(data=True) + if d.get("directed_edge") + ] + assets: dict[str, set[str]] = defaultdict(set) + for r in member_data: + if r["transfer_type"] in TRANSFER_ASSET_LABELS: + assets[TRANSFER_ASSET_LABELS[r["transfer_type"]]].add(r["transfer_content"]) + + t_starts = [r["first_reported_at"] for r in member_data if r["first_reported_at"]] + t_ends = [r["last_reported_at"] for r in member_data if r["last_reported_at"]] + return { + "family_id": family_id, + "family_size": len(nodes), + "core_prefixes": dict(Counter(n[:6] for n in nodes).most_common(3)), + "primary_labels": dict(Counter(r["phone_type"] for r in member_data).most_common(3)), + "transfer_chains": paths, + "shared_assets": {k: list(v) for k, v in assets.items()}, + "timeline": { + "start": min(t_starts).strftime("%Y-%m-%d %H:%M:%S") if t_starts else None, + "end": max(t_ends).strftime("%Y-%m-%d %H:%M:%S") if t_ends else None, + }, + } + + +def build_phone_genealogy(records: list[dict[str, Any]]) -> dict[str, Any]: + """建立詐騙家族候選配對,並把相關號碼分組成家族。""" + processed = [r for r in map(_normalize_record, records) if r is not None] + skipped = len(records) - len(processed) + + # 只有 processed 內號碼的配對會被查詢,因此只需載入這些號碼的通報事件 + report_pairs = _build_report_pairs(_list_report_events({r["phone_number"] for r in processed})) + candidate_pairs = _list_candidate_pairs(processed) + + edges = [] + scored_pairs = [] + graph = nx.DiGraph() + for idx_a, idx_b in candidate_pairs: + left = processed[idx_a] + right = processed[idx_b] + score, reasons, directed_edge = _score_phone_pair(left, right, report_pairs) + passed = score >= MIN_LINK_SCORE + + pair_debug = { + "left_phone": left["phone_number"], + "right_phone": right["phone_number"], + "left_type": left.get("phone_type"), + "right_type": right.get("phone_type"), + "left_reporter": left.get("reporter_id"), + "right_reporter": right.get("reporter_id"), + "left_transfer_type": left.get("transfer_type"), + "right_transfer_type": right.get("transfer_type"), + "left_transfer_content": left.get("transfer_content"), + "right_transfer_content": right.get("transfer_content"), + "score": score, + "reasons": reasons, + "passed": passed, + "status": "accepted" if passed else "rejected", + } + scored_pairs.append(pair_debug) + if not passed: + continue + + edge_info = { + "source": left["phone_number"], + "target": right["phone_number"], + "weight": score, + "reasons": reasons, + "directed_edge": directed_edge, + "debug": pair_debug, + } + edges.append(edge_info) + graph.add_edge(edge_info["source"], edge_info["target"], **edge_info) + + families = [ + _summarize_family(f"FAMILY_{i:03d}", graph, nodes, processed) + for i, nodes in enumerate(nx.weakly_connected_components(graph), 1) + ] + + return { + "summary": { + "input_records": len(records), + "skipped_invalid_phone": skipped, + "evaluated_records": len(processed), + "evaluated_pairs": len(candidate_pairs), + "total_edges": len(edges), + "family_count": len(families), + }, + "candidate_pairs": len(candidate_pairs), + "edges": edges, + "scored_pairs": scored_pairs, + "families": families, + } + + +# --- 5. 查詢號碼的直接家族關聯 --- + +def analyze_family(phone_number: str, limit: int = 30) -> dict[str, Any]: + """回傳與查詢號碼直接相連、且分數達門檻的家族關聯。""" + normalized_phone = normalize(phone_number) + result: dict[str, Any] = { + "static": [], + "cooccurrence": {"shared_reporters": [], "call_density": [], "content_similarity": []}, + "families": [], + "phone_number": normalized_phone, + "limit": limit, + } + + records = family_repository.list_family_records(phone_number, engine=db_engine) + if not records: + return result + + genealogy = build_phone_genealogy(records) + static = [] + seen = set() + for pair in genealogy["scored_pairs"]: + score = int(pair.get("score") or 0) + if not pair.get("passed") or score < MIN_LINK_SCORE: + continue + + left = str(pair.get("left_phone") or "") + right = str(pair.get("right_phone") or "") + if left == normalized_phone and right: + related_phone = right + elif right == normalized_phone and left: + related_phone = left + else: + continue + + reason = "; ".join(pair.get("reasons", [])[:2]) or "同一家族成員" + dedupe_key = (related_phone, score, reason) + if dedupe_key in seen: + continue + seen.add(dedupe_key) + static.append({"related_phone": related_phone, "link_reason": reason, "weight": score}) + + result["static"] = static[:limit] + result["families"] = genealogy["families"] + return result diff --git a/backend/services/phone_service/phone_service.py b/backend/services/phone_service/phone_service.py new file mode 100644 index 0000000000000000000000000000000000000000..e4ae058e86f2aece685be5916feeeff73310ea91 --- /dev/null +++ b/backend/services/phone_service/phone_service.py @@ -0,0 +1,135 @@ +import logging + +from backend.api.schemas.phone import PhoneReportError +from backend.repository.phone_repository import phone_repository +from backend.services.phone_service.phone_family import MIN_LINK_SCORE, analyze_family +from backend.utils.phone_normalizer import normalize + +logger = logging.getLogger(__name__) + + +class PhoneService: + @staticmethod + def _display_phone(value: str | None) -> str | None: + if value is None: + return None + return normalize(value) or None + + @staticmethod + def _db_status_to_api_status(value: str | None) -> str: + normalized = (value or "").strip().lower() + if normalized in ("black", "white"): + return normalized + return "unknown" + + @staticmethod + def _format_datetime(value) -> str | None: + if value is None or isinstance(value, str): + return value + return value.strftime("%Y-%m-%d %H:%M:%S") + + def _list_family_links(self, target_phone: str, target_phone_type: str) -> list[dict]: + """列出黑名單號碼的直接家族關聯;分析失敗時不影響查詢結果,回傳空清單。""" + try: + family = analyze_family(target_phone) + except Exception: + logger.warning("詐騙家族關聯分析失敗 phone=%s", target_phone, exc_info=True) + return [] + + links: list[dict] = [] + seen: set[tuple] = set() + for item in family.get("static", []): + weight = item.get("weight") or 0 + related_phone = self._display_phone(item.get("related_phone")) + if not related_phone or weight < MIN_LINK_SCORE: + continue + reason = item.get("link_reason") or "同一家族成員" + dedupe_key = (related_phone, weight, reason) + if dedupe_key in seen: + continue + seen.add(dedupe_key) + links.append({ + "related_phone": related_phone, + "weight": weight, + "reason": reason, + "target_phone_type": target_phone_type, + }) + return links + + def query_phone(self, phone_number: str) -> dict: + result = phone_repository.get_phone_record(phone_number) + if not result: + return { + "phone_number": phone_number, + "status": "unknown", + "phone_type": None, + "total_reports": None, + "first_reported_at": None, + "last_reported_at": None, + "owner_name": None, + "can_report": True, + "family_static": [], + } + + status = self._db_status_to_api_status(result.get("tags")) + phone_type = result.get("phone_type") or result.get("dominant_tag") + record_phone = result.get("phone_number") or phone_number + + family_static = [] + if status == "black": + target_phone = normalize(record_phone) or normalize(phone_number) + family_static = self._list_family_links(target_phone, phone_type or "unknown") + + return { + "phone_number": self._display_phone(record_phone) or phone_number, + "status": status, + "phone_type": phone_type, + "total_reports": int(result.get("total_reports") or 0) if result.get("total_reports") else None, + "first_reported_at": self._format_datetime(result.get("first_reported_at")), + "last_reported_at": self._format_datetime(result.get("last_reported_at")), + "owner_name": result.get("owner_name") or result.get("org_name"), + "can_report": status != "white", + "family_static": family_static, + } + + def report_suspicious( + self, + phone_number: str, + phone_type: str, + other_type: str | None = None, + connection_reason: str | None = None, + reporter_phone: str | None = None, + transfer_type: int | None = 0, + transfer_content: str | None = None, + ) -> dict: + if phone_type == "其他": + phone_type = other_type or phone_type + + reason_text = (connection_reason or "").strip() + try: + # 號碼正規化由 repository 負責 + result = phone_repository.report_suspicious( + phone_number=phone_number, + phone_type=phone_type, + reporter_phone=reporter_phone, + transfer_type=transfer_type, + transfer_content=transfer_content, + ) + except ValueError as exc: + raise PhoneReportError(str(exc)) from exc + + message = "可疑電話號碼已回報,已新增或更新黑名單記錄。" + if reason_text: + message = f"{message} 關聯原因:{reason_text}" + + return { + "phone_number": self._display_phone(result.get("phone_number")) or phone_number, + "status": "black", + "total_reports": int(result.get("total_reports") or 1), + "report_time": result.get("report_time"), + "message": message, + "connection_reason": reason_text or None, + } + + +phone_service = PhoneService() diff --git a/backend/services/text_service/__init__.py b/backend/services/text_service/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/backend/services/text_service/__init__.py @@ -0,0 +1 @@ + diff --git a/backend/services/text_service/base_classifier.py b/backend/services/text_service/base_classifier.py new file mode 100644 index 0000000000000000000000000000000000000000..1ae114eaaf8fb66a469b0f89351eabd000b5e82e --- /dev/null +++ b/backend/services/text_service/base_classifier.py @@ -0,0 +1,144 @@ +from typing import Any, cast + +from optimum.onnxruntime import ORTModelForSequenceClassification +from transformers import (AutoTokenizer, Pipeline, PreTrainedTokenizerBase, + pipeline) + +from backend.core.config import settings + + +class BaseClassifier: + """Transformer 模型加載與推理""" + + def __init__(self) -> None: + self.classifier: Pipeline | None = None + self.tokenizer: PreTrainedTokenizerBase | None = None + self.tochinese: dict[str, str] = { + "LOW": "低風險", + "MEDIUM": "中等風險", + "HIGH": "高風險", + "UNKNOWN": "未知風險", + } + self.rank_map: dict[str, int] = { + "高風險": 3, + "中等風險": 2, + "低風險": 1, + "未知風險": 0, + } + + def load_model(self) -> None: + """加載量化文字分類模型""" + print("載入量化文字推理模型...") + model_id = settings.HF_TEXT_REPO_ID + device = settings.DEVICE + token = settings.HF_TOKEN or None + tokenizer = AutoTokenizer.from_pretrained(model_id, token=token) + model = ORTModelForSequenceClassification.from_pretrained( + model_id, + file_name="model_quantized.onnx", + token=token, + provider="CPUExecutionProvider", + ) + self.classifier = pipeline( + "text-classification", + model=cast(Any, model), + tokenizer=tokenizer, + truncation=True, + device=device, + ) + self.tokenizer = tokenizer + print("文字模型載入完成") + + def _evaluate_distribution(self, result: list[dict[str, Any]]) -> tuple[str, float | None, float]: + """評估模型輸出的分類分佈""" + dist: dict[str, float] = {str(item["label"]): float(item["score"]) for item in result} + sorted_dist = sorted(dist.items(), key=lambda x: x[1], reverse=True) + + top_label, top_score = sorted_dist[0] + second_score = sorted_dist[1][1] if len(sorted_dist) > 1 else 0.0 + margin = top_score - second_score + + if dist.get("UNKNOWN", 0.0) >= settings.UNKNOWN_THRESHOLD: + return "未知風險", dist.get("UNKNOWN"), margin + if top_score >= 0.7: + return self.tochinese.get(top_label, "未知風險"), top_score, margin + if dist.get("HIGH", 0.0) >= settings.HIGH_THRESHOLD: + return "高風險", dist.get("HIGH"), margin + if margin >= 0.1: + return self.tochinese.get(top_label, "未知風險"), top_score, margin + return "中等風險", dist.get("MEDIUM"), margin + + def predict_text(self, text: str, max_length: int = 192, stride: int = 64) -> dict[str, Any]: + """ + 對文本進行分類預測 + + Args: + text: 輸入文本 + max_length: 最大序列長度 + stride: 滑動窗口步長 + + Returns: + 預測結果,包含 label, confidence, margin, chunk_consistent + """ + if self.tokenizer is None: + raise RuntimeError("分詞器未載入") + if self.classifier is None: + raise RuntimeError("文字模型未載入") + + # 不截斷:長文交給下方滑動窗口切段,否則超過模型上限(512)的內容會被直接丟掉。 + # verbose=False 關閉「序列超過模型長度」警告,整段不會直接送進模型 + tokens = self.tokenizer.encode(text, add_special_tokens=False, verbose=False) + + if len(tokens) <= max_length: + result = cast(list[dict[str, Any]], self.classifier(text, top_k=None)) + label, confidence, margin = self._evaluate_distribution(result) + return { + "label": label, + "confidence": confidence, + "margin": margin, + "chunk_consistent": True, + } + + chunks: list[str] = [] + for i in range(0, len(tokens), max_length - stride): + chunk_tokens = tokens[i : i + max_length] + chunk_text = self.tokenizer.decode(chunk_tokens) + chunks.append(chunk_text) + + # 每段內容已是 max_length 個 token,模型輸入還要加上 [CLS]/[SEP],否則段尾會被再截掉 + model_max_length = max_length + self.tokenizer.num_special_tokens_to_add() + results = cast( + list[list[dict[str, Any]]], + self.classifier( + chunks, truncation=True, max_length=model_max_length, top_k=None, batch_size=8 + ), + ) + + final_label = "未知風險" + final_confidence = 0.0 + final_margin = 0.0 + chunk_labels: list[str] = [] + + for chunk_result in results: + label, confidence, margin = self._evaluate_distribution(chunk_result) + chunk_labels.append(label) + current_rank = self.rank_map.get(label, 0) + final_rank = self.rank_map.get(final_label, 0) + + if current_rank > final_rank: + final_label = label + final_confidence = float(confidence or 0.0) + final_margin = margin + elif current_rank == final_rank: + final_confidence = max(final_confidence, float(confidence or 0.0)) + final_margin = max(final_margin, margin) + + return { + "label": final_label, + "confidence": final_confidence, + "margin": final_margin, + "chunk_consistent": len(set(chunk_labels)) <= 2, + } + + +base_classifier = BaseClassifier() diff --git a/backend/services/text_service/confidence_router.py b/backend/services/text_service/confidence_router.py new file mode 100644 index 0000000000000000000000000000000000000000..c80782dc997a6dd1a124db93020cc41a9aaa565e --- /dev/null +++ b/backend/services/text_service/confidence_router.py @@ -0,0 +1,36 @@ +from dataclasses import dataclass +from typing import Any + +from backend.services.dto.text_analysis import BaseEvidence + + +@dataclass(frozen=True) +class RoutingDecision: + use_llm: bool + route_reason: str + + +class ConfidenceRouter: + def decide(self, evidence: BaseEvidence, base_result: dict[str, Any]) -> RoutingDecision: + model_label = evidence.model_label + model_confidence = float(evidence.model_confidence or 0.0) + model_margin = float(evidence.model_margin or 0.0) + rule_score = evidence.rule_score + chunk_consistent = evidence.chunk_consistent + + if not chunk_consistent: + return RoutingDecision(True, "chunk_predictions_conflict") + if model_margin <= 0.12: + return RoutingDecision(True, "low_margin") + if model_label == "低風險" and rule_score >= 50: + return RoutingDecision(True, "rules_high_but_model_low") + if model_label == "高風險" and rule_score <= 20: + return RoutingDecision(True, "model_high_but_rules_low") + if base_result.get("label") == "中等風險" and model_confidence < 0.75: + return RoutingDecision(True, "base_result_medium") + if model_confidence < 0.8: + return RoutingDecision(True, "low_model_confidence") + return RoutingDecision(False, "base_confident") + + +confidence_router = ConfidenceRouter() diff --git a/backend/services/text_service/fusion.py b/backend/services/text_service/fusion.py new file mode 100644 index 0000000000000000000000000000000000000000..8cef2bafc4831693986a3f1509d9e8a4de1dc5dd --- /dev/null +++ b/backend/services/text_service/fusion.py @@ -0,0 +1,26 @@ +from typing import Any + +from backend.rag.dto.rag_analysis import RagEvidence + + +class Fusion: + def merge(self, base_result: dict[str, Any], + rag_result: RagEvidence, route_reason: str) -> dict[str, Any]: + + merged = dict(base_result) + merged["decision_source"] = "rag" + + rag_label = rag_result.label + rag_score = rag_result.score + rag_reason = rag_result.reason + + merged["llm_used"] = True + merged["route_reason"] = route_reason + + merged["label"] = rag_label + merged["score"] = f"{rag_score:.2f}" + merged["reason"] = rag_reason + return merged + + +fusion = Fusion() diff --git a/backend/services/text_service/text_analyzer.py b/backend/services/text_service/text_analyzer.py new file mode 100644 index 0000000000000000000000000000000000000000..9705870981b2a47eae50e45aa163ab6d53a3d441 --- /dev/null +++ b/backend/services/text_service/text_analyzer.py @@ -0,0 +1,131 @@ +from typing import Any, cast + +from starlette.concurrency import run_in_threadpool + +from backend.core.config import settings +from backend.rag.rag_reasoner import analyze_with_rag +from backend.services.dto.text_analysis import BaseEvidence +from backend.services.text_service.base_classifier import base_classifier +from backend.services.text_service.confidence_router import confidence_router +from backend.services.text_service.fusion import fusion +from backend.utils.pattern.text import compare_rules +from backend.utils.text_cleaner import normalize_text + + +class TextAnalyzer: + """文字分析協調層 - 負責功能編排與決策路由""" + + def _build_base_evidence(self, text: str) -> BaseEvidence: + """構建基礎證據:規則特徵 + 模型預測""" + cleaned = normalize_text(text) + rule_score, rule_reason, rule_hits = compare_rules(cleaned) + model_result = base_classifier.predict_text(cleaned) + return BaseEvidence( + text=cleaned, + rule_score=rule_score, + rule_reason=rule_reason, + rule_hits=list(set(rule_hits)), + model_label=cast(str, model_result["label"]), + model_confidence=cast(float | None, model_result["confidence"]), + model_margin=cast(float | None, model_result["margin"]), + chunk_consistent=cast(bool, model_result["chunk_consistent"]), + ) + + def _base_decision(self, evidence: BaseEvidence) -> dict[str, Any]: + """基礎決策:融合規則和模型預測""" + w_model = settings.MODEL_WEIGHT + w_rule = settings.REGEX_WEIGHT + + rule_score = evidence.rule_score + rule_reason = evidence.rule_reason + rule_hits = set(evidence.rule_hits) + model_label = evidence.model_label + model_confidence = float(evidence.model_confidence or 0.0) + extra_reason = rule_reason if "反詐" in rule_reason else "" + + response: dict[str, Any] = { + "label": "", + "score": "", + "cls_model_confidence": model_confidence, + "reason": "", + } + + if model_label == "未知風險" and not rule_hits: + response.update({"label": "未知風險", "score": 0.0, "reason": "語句缺乏明確資訊,無法進行有效判斷,評估風險為未知"}) + return response + if model_label == "低風險" and model_confidence >= 0.8 and len(rule_hits) <= 2: + response.update({"label": "低風險", "score": 10.0, "reason": f"此訊息所含詐騙特徵較少{extra_reason},評估風險為低。"}) + return response + if model_label == "高風險" and (rule_score >= 45 or len(rule_hits) >= 2): + response.update({"label": "高風險", "score": 85.0, "reason": f"此訊息{rule_reason},評估風險為高"}) + return response + if rule_score >= 85 and len(rule_hits) >= 3: + response.update({"label": "高風險", "score": 95.0, "reason": f"此訊息{rule_reason},評估風險為高"}) + return response + if rule_score <= 20 and len(rule_hits) <= 2: + response.update({"label": "低風險", "score": 20.0, "reason": "此訊息所含詐騙特徵較少,評估風險為低。"}) + return response + + if model_label == "高風險": + model_eval = 100 + elif model_label == "中等風險": + model_eval = 50 + elif model_label == "低風險": + model_eval = 20 + else: + model_eval = -1 + + final_score = (20 + rule_score) * w_rule + model_eval * w_model + + if model_label == "低風險" and rule_score <= 28: + final_score = min(final_score, 30.0) + + if final_score >= 80: + response.update({"label": "高風險", "score": final_score, "reason": f"此訊息{rule_reason},評估風險為高"}) + return response + if final_score >= 40 and rule_reason: + response.update({"label": "中等風險", "score": final_score, "reason": f"此訊息{rule_reason},評估風險為中等"}) + return response + if final_score >= 40: + response.update({"label": "中等風險", "score": final_score, "reason": "此訊息疑似有詐騙風險,但特徵較模糊,評估風險為中等"}) + return response + if model_eval < 0: + response.update({"label": "未知風險", "score": 0.0, "reason": "此訊息缺乏明確資訊,無法進行有效判斷,評估風險為未知"}) + return response + response.update({"label": "低風險", "score": final_score, "reason": f"此訊息所含詐騙特徵較少{extra_reason},評估風險為低。"}) + return response + + async def hybrid_detector(self, text: str) -> dict[str, Any]: + """混合檢測:規則 + 模型 + 可選的 LLM/RAG""" + # 規則比對與 ONNX 推論都是 CPU 密集的同步運算,移出 event loop 避免阻塞其他請求 + evidence = await run_in_threadpool(self._build_base_evidence, text) + base_result = self._base_decision(evidence) + decision = confidence_router.decide(evidence, base_result) + if not decision.use_llm: + base_result["decision_source"] = "base" + base_result["llm_used"] = False + base_result["route_reason"] = decision.route_reason + return base_result + + try: + rag_result = await analyze_with_rag(evidence.text) + except Exception as exc: + fallback = dict(base_result) + fallback["decision_source"] = "base" + fallback["llm_used"] = False + fallback["route_reason"] = f"rag_failed:{exc.__class__.__name__}" + return fallback + + return fusion.merge(base_result, rag_result, decision.route_reason) + + def model_detector(self, text: str) -> dict[str, Any]: + """純模型檢測""" + prediction = base_classifier.predict_text(text) + return { + "label": prediction["label"] or "", + "cls_model_confidence": float(prediction.get("confidence") or 0.0), + } + + +# 全局實例 +text_analyzer = TextAnalyzer() diff --git a/backend/services/url_service/__init__.py b/backend/services/url_service/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/backend/services/url_service/__init__.py @@ -0,0 +1 @@ + diff --git a/backend/services/url_service/redirect_resolver.py b/backend/services/url_service/redirect_resolver.py new file mode 100644 index 0000000000000000000000000000000000000000..f02a258e57d81291d85aad818721c6c61a2999fe --- /dev/null +++ b/backend/services/url_service/redirect_resolver.py @@ -0,0 +1,93 @@ +"""追蹤短網址的 HTTP 轉址,找出實際導向的網址。 + +這裡會依使用者輸入對外發出請求,因此每一跳都先檢查目標主機只解析到公網 IP, +並限制協定、埠號、轉址次數與總時間,避免被用來探測內網(SSRF)。 +已知限制:檢查 DNS 與實際連線之間仍有 DNS rebinding 的時間差。 +""" + +import ipaddress +import logging +import socket +import time +from urllib.parse import urljoin, urlsplit + +import httpx + +from backend.core.config import settings + +logger = logging.getLogger(__name__) + +_ALLOWED_PORTS = {"http": (None, 80), "https": (None, 443)} +_REDIRECT_STATUS = {301, 302, 303, 307, 308} +# 部分短網址服務會拒絕沒有 User-Agent 的請求;只用通用名稱,不帶任何使用者資訊 +_HEADERS = {"User-Agent": "Mozilla/5.0 (compatible; url-risk-checker/1.0)"} + + +def _is_public_host(host: str, port: int) -> bool: + """主機的所有解析結果都必須是公網 IP;解析失敗視為不安全。""" + try: + infos = socket.getaddrinfo(host, port, type=socket.SOCK_STREAM) + except (socket.gaierror, UnicodeError): + return False + for info in infos: + ip = ipaddress.ip_address(info[4][0]) + if isinstance(ip, ipaddress.IPv6Address) and ip.ipv4_mapped: + ip = ip.ipv4_mapped + if not ip.is_global: + return False + return bool(infos) + + +def _is_allowed_target(url: str) -> bool: + parts = urlsplit(url) + try: + port = parts.port + except ValueError: + return False + if parts.scheme not in _ALLOWED_PORTS or port not in _ALLOWED_PORTS[parts.scheme]: + return False + if not parts.hostname: + return False + return _is_public_host(parts.hostname, port or (443 if parts.scheme == "https" else 80)) + + +def resolve_final_url( + url: str, + *, + timeout: float | None = None, + max_redirects: int | None = None, + transport: httpx.BaseTransport | None = None, +) -> str | None: + """回傳轉址鏈的最終網址;沒有轉址、超過次數、逾時或目標不安全時回傳 None。""" + timeout = settings.URL_EXPAND_TIMEOUT_SECONDS if timeout is None else timeout + max_redirects = settings.URL_EXPAND_MAX_REDIRECTS if max_redirects is None else max_redirects + deadline = time.monotonic() + timeout + current = url + + with httpx.Client(follow_redirects=False, headers=_HEADERS, transport=transport) as client: + for hop in range(max_redirects + 1): + remaining = deadline - time.monotonic() + if remaining <= 0: + logger.info("短網址展開逾時 url=%s", url) + return None + if not _is_allowed_target(current): + logger.warning("短網址導向不允許的目標 url=%s target=%s", url, current) + return None + + # 只需要回應標頭:用串流 GET 取得狀態碼與 Location 後立即關閉,不下載內容。 + # 不用 HEAD,因為部分短網址服務對 HEAD 回 405 或不回傳轉址 + try: + with client.stream("GET", current, timeout=remaining) as response: + location = response.headers.get("location") + status = response.status_code + except httpx.HTTPError as exc: + logger.info("短網址展開失敗 url=%s error=%s", url, exc.__class__.__name__) + return None + + if status not in _REDIRECT_STATUS or not location: + # hop 為 0 代表短網址本身沒有轉址(例如預覽頁),無法得知目標 + return current if hop > 0 else None + current = urljoin(current, location) + + logger.info("短網址轉址次數超過上限 url=%s", url) + return None diff --git a/backend/services/url_service/suffix_policy.py b/backend/services/url_service/suffix_policy.py new file mode 100644 index 0000000000000000000000000000000000000000..1e6780c9f7ca9ddbd96284b76a6c624eb490890d --- /dev/null +++ b/backend/services/url_service/suffix_policy.py @@ -0,0 +1,75 @@ +"""呼叫 URL 模型前的網域檢查:台灣受管制後綴、短網址與可疑 tw 字樣判斷。""" + +import re +from dataclasses import dataclass +from urllib.parse import urlparse + +import tldextract + +from backend.utils.features import COMMON_DOMAINS + +# 僅限政府機關、學校、軍方註冊,不必再交給模型判斷 +RESTRICTED_SUFFIXES = frozenset({"gov.tw", "edu.tw", "mil.tw"}) +# 需實名(公司、組織、個人)註冊,但仍可能遭入侵或被冒名申請,仍需經過模型 +REGISTERED_SUFFIXES = frozenset({"com.tw", "org.tw", "net.tw", "idv.tw"}) +REGULATED_SUFFIXES = RESTRICTED_SUFFIXES | REGISTERED_SUFFIXES + +# 只收可導向任意網站的公用短網址;youtu.be、amzn.to、lin.ee 這類只導回自家品牌的不列入 +SHORTENER_DOMAINS = frozenset({ + # 台灣常見大眾短網址 + "reurl.cc", "ppt.cc", "pse.is", "lihi.cc", "lihi1.com", "lihi1.cc", "lihi2.com", "lihi3.com", + # 國際 + "bit.ly", "j.mp", "tinyurl.com", "t.ly", "t.co", "goo.gl", "is.gd", "v.gd", "ow.ly", "buff.ly", + "cutt.ly", "rb.gy", "shorturl.at", "tiny.cc", "bit.do", "s.id", "rebrand.ly", "gg.gg", +}) + +HOST_TOKEN_SPLIT_RE = re.compile(r"[.\-_\d]+") + + +@dataclass(frozen=True) +class SuffixSignals: + suffix: str + is_regulated: bool + is_restricted: bool + is_shortened: bool + has_unregulated_tw_suffix: bool + has_tw_in_host: bool + + @property + def has_suspicious_tw(self) -> bool: + return self.has_unregulated_tw_suffix or self.has_tw_in_host + + +def get_suffix_signals(url: str) -> SuffixSignals: + """解析 URL 的公開後綴,回傳管制後綴、短網址與可疑 tw 旗標。 + + is_regulated:後綴屬於 REGULATED_SUFFIXES,例如 moe.gov.tw、example.com.tw。 + is_restricted:後綴屬於 RESTRICTED_SUFFIXES(gov.tw/edu.tw/mil.tw)。 + is_shortened:註冊網域屬於 SHORTENER_DOMAINS,例如 reurl.cc/abc、bit.ly/xyz。 + 兩個 tw 旗標都排除 COMMON_DOMAINS: + has_unregulated_tw_suffix:後綴是 tw 但不在管制清單,例如 ezpay.tw、abc.game.tw + has_tw_in_host:tw 出現在子網域或網域主體,例如 ezpaytw.com、gov.tw.login-check.xyz + """ + # 必須用 hostname 而非 netloc:https://x.gov.tw@evil.xyz 的 netloc 含帳密段,會被誤判成 gov.tw。 + host = (urlparse(url).hostname or "").rstrip(".") + ext = tldextract.extract(host) + suffix = ext.suffix + is_regulated = suffix in REGULATED_SUFFIXES + root_domain = f"{ext.domain}.{suffix}" + is_common_domain = root_domain in COMMON_DOMAINS + + is_unregulated_tw = not is_regulated and (suffix == "tw" or suffix.endswith(".tw")) + + # endswith 同時涵蓋獨立的 tw 與黏在結尾的 govtw;不比對 tw 開頭,否則 twitter、twitch 都會命中。 + host_part = f"{ext.subdomain}.{ext.domain}" if ext.subdomain else ext.domain + tokens = [t for t in HOST_TOKEN_SPLIT_RE.split(host_part) if t] + has_tw_outside_suffix = any(t.endswith("tw") for t in tokens) + + return SuffixSignals( + suffix=suffix, + is_regulated=is_regulated, + is_restricted=suffix in RESTRICTED_SUFFIXES, + is_shortened=root_domain in SHORTENER_DOMAINS, + has_unregulated_tw_suffix=is_unregulated_tw and not is_common_domain, + has_tw_in_host=has_tw_outside_suffix and not is_common_domain, + ) diff --git a/backend/services/url_service/url_analyzer.py b/backend/services/url_service/url_analyzer.py new file mode 100644 index 0000000000000000000000000000000000000000..82f2709a6fde36aceff6924db71f5c9f511f8073 --- /dev/null +++ b/backend/services/url_service/url_analyzer.py @@ -0,0 +1,110 @@ +from collections.abc import Callable + +import joblib +from huggingface_hub import hf_hub_download + +from backend.core.config import settings +from backend.services.url_service import suffix_policy +from backend.services.url_service.redirect_resolver import resolve_final_url +from backend.utils import features + +MODEL_REASONS = { + "HIGH": "此鏈結經過判別具有風險特徵,請立即查證網址拼寫是否正確", + "MEDIUM": "此鏈結可能具有風險,點擊前請先確認來源", + "LOW": "目前尚未發現鏈結風險,仍需留意陌生鏈結", +} +SHORTENED_UNRESOLVED_REASON = "此為短網址,無法得知實際導向的網站,點擊前請先確認來源" +MAX_DISPLAY_URL_LENGTH = 100 + + +def get_risk_label(score: float) -> str: + if score >= settings.URL_HIGH_THRESHOLD: + return "HIGH" + if score >= settings.URL_MEDIUM_THRESHOLD: + return "MEDIUM" + return "LOW" + + +def format_score(score: float) -> str: + """所有路徑的 score 一律輸出兩位小數字串,維持 API 回傳型別一致。""" + return f"{score:.2f}" + + +class UrlDetector: + def __init__(self, resolve_redirect: Callable[[str], str | None] = resolve_final_url): + self.classifier = None + self._resolve_redirect = resolve_redirect + + def load_model(self): + print("載入 url 推理模型...") + model_path = hf_hub_download( + repo_id=settings.HF_URL_REPO_ID, + filename=settings.URL_MODEL_NAME, + token=settings.HF_TOKEN or None, + ) + self.classifier = joblib.load(model_path) + print("url模型載入完畢") + + def model_detect(self, url: str, risk_bonus: float = 0.0, max_score: float = 1.0) -> dict: + if self.classifier is None: + raise RuntimeError('url模型未載入') + + feat = features.process_batch_urls([url]) + prob = float(self.classifier.predict_proba(feat)[0][1]) + score = min(prob + risk_bonus, max_score, 1.0) + label = get_risk_label(score) + return {"label": label, "score": format_score(score), "reason": MODEL_REASONS[label]} + + def analyze(self, url: str, *, expand_shortened: bool = True) -> dict: + """對外入口:依網域訊號決定是否進模型、加減分與補充理由。 + + - 短網址:展開後分析實際導向的網址,無法展開時回 UNKNOWN + - gov.tw/edu.tw/mil.tw:僅限特定單位註冊,不進模型直接判低風險 + - com.tw 等實名註冊後綴:仍進模型,但最高只到中等風險 + - 可疑 tw 字樣:模型分數加分,並依訊號類型補充理由 + """ + signals = suffix_policy.get_suffix_signals(url) + if signals.is_shortened: + return self._analyze_shortened(url, expand_shortened) + + if signals.is_restricted: + reason = f"此屬政府、學校或軍方專用後綴 .{signals.suffix},僅限該類單位註冊,仍請確認網址拼寫正確" + return {"label": "LOW", "score": format_score(0.0), "reason": reason} + + risk_bonus = settings.URL_TW_RISK_BONUS if signals.has_suspicious_tw else 0.0 + # 實名註冊不代表網站本身安全(可能遭入侵或冒名申請),因此仍跑模型,只是不判到高風險 + max_score = settings.URL_HIGH_THRESHOLD - 0.01 if signals.is_regulated else 1.0 + result = self.model_detect(url, risk_bonus=risk_bonus, max_score=max_score) + + notes: list[str] = [] + if signals.is_regulated and result["label"] == "LOW": + notes.append(f"此網域為 .{signals.suffix} 實名註冊後綴") + elif signals.is_regulated: + notes.append(f"雖為 .{signals.suffix} 實名註冊後綴,仍可能是遭入侵或冒名申請的網站") + if signals.has_unregulated_tw_suffix: + notes.append(f"此網域使用未受管制的 .{signals.suffix} 後綴,任何人皆可註冊,常見於假冒台灣企業的網址") + if signals.has_tw_in_host: + notes.append("網域名稱中夾帶 tw 字樣,此類網址常見於假冒台灣單位的網址") + + if notes: + result["reason"] = ",".join([result["reason"], *notes]) + return result + + def _analyze_shortened(self, url: str, expand: bool) -> dict: + final_url = None + if expand and settings.URL_EXPAND_SHORTENED: + final_url = self._resolve_redirect(url) + if not final_url: + score = format_score(0.0) + return {"label": "UNKNOWN", "score": score, "reason": SHORTENED_UNRESOLVED_REASON} + + # 只展開一層,避免短網址互相導向造成遞迴 + result = self.analyze(final_url, expand_shortened=False) + display_url = final_url + if len(display_url) > MAX_DISPLAY_URL_LENGTH: + display_url = f"{display_url[:MAX_DISPLAY_URL_LENGTH]}..." + result["reason"] = f"此短網址實際導向 {display_url},{result['reason']}" + return result + + +url_detector = UrlDetector() diff --git a/backend/utils/__init__.py b/backend/utils/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/backend/utils/features.py b/backend/utils/features.py new file mode 100644 index 0000000000000000000000000000000000000000..93c209aeae35f46d70a65d86c27665dfaf253118 --- /dev/null +++ b/backend/utils/features.py @@ -0,0 +1,299 @@ +"""URL 特徵抽取,供 service 端呼叫""" + +import math +import re +from functools import lru_cache +from urllib.parse import parse_qs, urlparse, urlunsplit + +import pandas as pd +import tldextract +from pandas import DataFrame + +FEATURE_NAMES = [ + "url_length", "num_dot", "num_dash", "num_slash", + "is_https", "domain_length", "subdomain_depth", "has_digit_in_domain", + "num_dash_in_domain", "num_phishing_keywords", "has_tw_keyword", + "path_length", "num_path_segments", "num_params", + "has_utm", "has_encoded_chars", "is_suspicious_tld", + "domain_entropy", "is_real_gov", "is_fake_gov", + "is_common_domain", "is_shortened", "is_common_tld", + "num_keywords_in_domain", "num_keywords_in_path", + "brand_in_domain_not_own", "domain_token_count", + "min_edit_distance_to_brand", "is_punycode", +] + +N_FEATURES = 29 + +SUSPICIOUS_TLD = ["xyz", "top", "cc", "tk", "ml", "ga", "club", "vip"] + +TW_KEYWORDS = [ + "gov", "nhia", "健保", "郵局", + "post", "etax", "tax", "電費", + "taipower", "7-11", "familymart", "物流", +] + +SHORTENER = ["bit.ly", "t.co", "tinyurl.com", "t.ly"] + +# 常見網域白名單 +COMMON_DOMAINS = { + "google.com", "youtube.com", "facebook.com", + "amazon.com", "wikipedia.org", "microsoft.com", + "x.com", "yahoo.com", "shopee.tw" +} + +COMMON_TLD = ["com", "org", "net", "tw", "com.tw"] + +PHISHING_KEYWORDS = [ + "login", "verify", "secure", "confirm", "account", + "authenticate", "payment", "receipt", "identity", + "update", "exchange", "security", "auth", "signin", +] + +GOV_TOKENS = ["gov", "govtw", "egov", "nhia", "nhi", "etax", "moi", "npa", "g0v", "g0vtw"] + +GOV_SUFFIX = {"gov.tw", "gov", "gov.uk", "go.jp"} + +# 只放網域主體那一段(不含後綴),判斷的是「這個詞出現在誰的註冊網域裡」。 +BRANDS = frozenset({ + "apple", "icloud", "paypal", "google", "gmail", "youtube", "facebook", + "instagram", "whatsapp", "microsoft", "outlook", "office365", "onedrive", + "amazon", "netflix", "linkedin", "twitter", "dropbox", "adobe", "docusign", + "steam", "roblox", "discord", "telegram", "signal", "zoom", "spotify", + "booking", "airbnb", "alibaba", "aliexpress", "wise", "revolut", + "fedex", "dhl", "ups", "usps", "hermes", "yamato", "sfexpress", + "binance", "coinbase", "metamask", "trustwallet", "ledger", "kraken", + "bitfinex", "okx", "bybit", + "shopee", "pchome", "momoshop", "ruten", "yahoo", "line", "gamer", + "esunbank", "cathaybk", "ctbcbank", "fubon", "megabank", "taishinbank", + "firstbank", "landbank", "hncb", "sinopac", "chb", "tcb", + "chunghwa", "cht", "taipower", "nhi", "post", "ubot", "jkos", "icash", +}) + +# 形近字元替換是常見的躲避字串比對手法,比對前先正規化。 +HOMOGLYPHS = str.maketrans({ + "0": "o", "1": "l", "3": "e", "4": "a", "5": "s", "7": "t", "8": "b", + "@": "a", "$": "s", +}) + +MAX_EDIT_DISTANCE = 5 # 超過就沒有 typosquat 的意義,截斷以壓縮值域 +MIN_TOKEN_LEN = 4 # 短詞之間的編輯距離沒有意義 +BRAND_PREFIX_SLACK = 4 # 品牌當 token 前綴時,後面最多還能接幾個字元 + +# 會被前綴規則誤判成品牌的一般英文字(revolut+ion、post+al、line+age…)。 +NOT_BRAND_TOKENS = frozenset({ + "revolution", "revolutions", "revolutionary", + "postal", "posted", "poster", "posters", "posting", "postre", + "lineage", "linear", "lineup", "liner", "liners", + "upstream", "upside", "upsell", "upstate", + "signals", "signage", "signature", + "steamer", "steamed", "wisely", "wisdom", + "zoomed", "zoomer", "adobers", + "amazonas", "amazonia", +}) + +_LONG_BRANDS = frozenset(b for b in BRANDS if len(b) >= MIN_TOKEN_LEN) + + +def _keyword_regex(keywords): + """詞界比對,避免 author 命中 auth、paypal 命中 pay;但允許複數形。""" + alt = "|".join(re.escape(k) for k in sorted(keywords, key=len, reverse=True)) + return re.compile(rf"(? int: + """命中的不同關鍵詞數量,同一個詞出現多次只算一次。""" + return len({m.group(1).lower() for m in pattern.finditer(text)}) + + +def calc_entropy(s): + if not s: + return 0 + prob = [float(s.count(c)) / len(s) for c in set(s)] + return -sum(p * math.log2(p) for p in prob) + + +def _typo_threshold(brand: str) -> int: + """短品牌不能用大門檻,否則 wise↔nibe 距離 2、line↔nine 距離 1 都會命中。""" + if len(brand) < 5: + return 0 + return 1 if len(brand) == 5 else 2 + + +def brand_hit(tokens: tuple[str, ...]) -> str | None: + """品牌詞是否出現在網域裡,回傳命中的品牌。 + + 只認 token 邊界,不做純子字串比對(否則 online 會命中 line、 + groovypost 會命中 post)。品牌本身是完整 token,或是某個 token 的前綴 + 且後面最多再接 BRAND_PREFIX_SLACK 個字元,才算命中。 + """ + token_set = set(tokens) + for b in BRANDS: + if b in token_set: + return b + for t in tokens: + if t in NOT_BRAND_TOKENS: + continue + if t.startswith(b) and 0 < len(t) - len(b) <= BRAND_PREFIX_SLACK: + return b + return None + + +@lru_cache(maxsize=200_000) +def edit_distance(a: str, b: str, cap: int = MAX_EDIT_DISTANCE) -> int: + """Levenshtein 距離,超過 cap 提早結束。""" + if a == b: + return 0 + if abs(len(a) - len(b)) > cap: + return cap + prev = list(range(len(b) + 1)) + for i, ca in enumerate(a, start=1): + cur = [i] + for j, cb in enumerate(b, start=1): + cur.append(min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + (ca != cb))) + if min(cur) > cap: + return cap + prev = cur + return min(prev[-1], cap) + + +def _domain_tokens(label: str) -> list[str]: + """把註冊網域主體切成詞:連字號、底線、數字邊界都算分隔。""" + return [t for t in re.split(r"[-_]+|(?<=[a-z])(?=\d)|(?<=\d)(?=[a-z])", label) if t] + + +@lru_cache(maxsize=100_000) +def brand_signals(host_label: str, subdomain: str) -> tuple[int, int]: + """回傳 (brand_in_domain_not_own, min_edit_distance_to_brand)。 + + brand_in_domain_not_own:品牌詞出現在 host 裡但註冊主體不是該品牌。 + apple.com -> 0,secure-login-apple-id.xyz -> 1 + min_edit_distance_to_brand:主體到最近品牌的距離,0 = 就是品牌本人, + 1-2 = typosquat,>=3 視為無關。 + """ + label = host_label.lower() + normalized = label.translate(HOMOGLYPHS) + + if label in BRANDS or normalized in BRANDS: + return 0, 0 + + host = f"{subdomain}.{label}".lower() if subdomain else label + host_norm = host.translate(HOMOGLYPHS) + tokens = tuple(t for part in host_norm.split(".") for t in _domain_tokens(part)) + in_domain = int(brand_hit(tokens) is not None) + + # 兩邊都要求至少 MIN_TOKEN_LEN 字元,否則 tw / id 這種短詞會產生無意義的小距離。 + candidates = [c for c in (normalized, *_domain_tokens(normalized)) + if len(c) >= MIN_TOKEN_LEN] + best = MAX_EDIT_DISTANCE + for c in candidates: + for b in _LONG_BRANDS: + d = edit_distance(c, b) + if d <= _typo_threshold(b): + best = min(best, d) + return in_domain, best + + +def strip_www(host: str) -> str: + return host[4:] if host.lower().startswith("www.") else host + + +def subdomain_depth_of(url: str) -> int: + """子網域層數,www. 不算。與 subdomain_depth 特徵同一套定義。""" + host = strip_www(urlparse(url).netloc) + subdomain = tldextract.extract(host).subdomain + return len(subdomain.split(".")) if subdomain else 0 + + +def extract_features(url: str) -> dict: + """抽出單一 URL 的 29 個特徵,回傳 dict。""" + parsed = urlparse(url) + domain = strip_www(parsed.netloc) + path = parsed.path + query = parsed.query + scheme = parsed.scheme + ext = tldextract.extract(domain) + root_domain = f"{ext.domain}.{ext.suffix}" + subdomain = ext.subdomain + tld = ext.suffix + + features = {} + + # 全部算在剝掉 www. 之後的 URL 上,否則 www. 會讓長度與點數憑空變動。 + url_norm = urlunsplit((scheme, domain, path, query, parsed.fragment)) + url_lower = url_norm.lower() + + features["url_length"] = len(url_norm) + features["num_dot"] = url_norm.count(".") + features["num_dash"] = url_norm.count("-") + features["num_slash"] = url_norm.count("/") + features["is_https"] = int(scheme == "https") + + features["domain_length"] = len(domain) + features["subdomain_depth"] = len(subdomain.split(".")) if subdomain else 0 + features["has_digit_in_domain"] = int(bool(re.search(r"\d", domain))) + features["num_dash_in_domain"] = domain.count("-") + + features["num_phishing_keywords"] = count_keywords(url_lower, PHISHING_KEYWORD_RE) + features["has_tw_keyword"] = int( + bool(TW_KEYWORD_RE.search(url_lower)) + or any(k in url_norm for k in TW_KEYWORD_CJK) + ) + + features["path_length"] = len(path) + features["num_path_segments"] = len([p for p in path.split("/") if p]) + + params = parse_qs(query) + features["num_params"] = len(params) + features["has_utm"] = int("utm_" in query) + features["has_encoded_chars"] = int("%" in url_norm) + + features["is_suspicious_tld"] = int(tld in SUSPICIOUS_TLD) + features["domain_entropy"] = calc_entropy(domain) + + # 必須比對「後綴」,不能用 endswith,否則 fake-gov.tw、evilgov.tw 會被當真政府。 + features["is_real_gov"] = int(tld in GOV_SUFFIX) + + host_part = f"{subdomain}.{ext.domain}" if subdomain else ext.domain + features["is_fake_gov"] = int( + bool(GOV_TOKEN_RE.search(host_part.lower())) and tld not in GOV_SUFFIX + ) + + features["is_common_domain"] = int(root_domain in COMMON_DOMAINS) + features["is_shortened"] = int(root_domain in SHORTENER) + features["is_common_tld"] = int(tld in COMMON_TLD) + + # 關鍵字在網域 vs 在 path 是兩種強度不同的訊號,分開算。 + features["num_keywords_in_domain"] = count_keywords( + host_part.lower(), PHISHING_KEYWORD_RE + ) + features["num_keywords_in_path"] = count_keywords( + f"{path}?{query}".lower(), PHISHING_KEYWORD_RE + ) + + in_domain, brand_dist = brand_signals(ext.domain, subdomain) + features["brand_in_domain_not_own"] = in_domain + features["min_edit_distance_to_brand"] = brand_dist + + features["domain_token_count"] = len(_domain_tokens(ext.domain.lower())) + features["is_punycode"] = int("xn--" in domain.lower()) + + return features + + +def process_batch_urls(urls: list) -> DataFrame: + """把一批 URL 轉成特徵矩陣,欄位順序固定為 FEATURE_NAMES。""" + result = [] + for url in urls: + feat = extract_features(url) + feat_list = [feat.get(name, 0) for name in FEATURE_NAMES] + result.append(feat_list) + return pd.DataFrame(result, columns=FEATURE_NAMES) diff --git a/backend/utils/pattern/text.py b/backend/utils/pattern/text.py new file mode 100644 index 0000000000000000000000000000000000000000..cd89db47e2f593c76ccbf01512aee7d862782b65 --- /dev/null +++ b/backend/utils/pattern/text.py @@ -0,0 +1,167 @@ +import re +from typing import List, Tuple + +from rapidfuzz import fuzz + +# 弱訊號詞: 單獨出現時常見於正常付款、通知或客服問答,不應直接視為高風險 +MONEY_RULE = re.compile( + r"\$|\bUSD\b|TWD|NT|錢|金額|台幣|美金|人民幣|日圓|匯款|匯入|佣金|手續費|未繳清|未繳費|" + r"交易連結|交易鏈結|入.{0,3}帳|出.{0,3}金|付.{0,3}費|付.{0,3}款|付.{0,3}現|" + r"繳.{0,3}費|刷.{0,3}卡|退.{0,3}款|退.{0,3}費|轉.{0,3}帳" +) +OTP_RULE = re.compile(r"OTP|驗證信|驗證碼|動態密碼|授權碼|一次性密碼") +ACCOUNT_RULE = re.compile(r"帳號|銀行|帳戶") + +# 高風險情境詞: 與詐騙語境強相關 +URGENCY_RULE = re.compile(r"立即|馬上|立刻|趕快|限時|最後|逾期|否則|將停用|凍結|封鎖|24小時|今日截止|搶購|盡速前往") +INVEST_RULE = re.compile(r"(投資|飆股|內線|代操|保證獲利|高報酬|穩賺|跟單|當沖|虛擬貨幣|以太幣|比特幣|BTC|USDT)") +IMPERSONATE_RULE = re.compile(r"(客服|官方|有誤|升級|操作不當|操作異常|帳戶異常|系統異常|登入異常|風控|安全驗證|司法|檢警|刑事|警察|法院|傳票|偵查|金管會)") +SENSITIVE_RULE = re.compile(r"(身分證|登入密碼|網銀密碼|交易密碼|信用卡|卡號|三碼|CVV|戶頭|提款卡|存摺|個資|證件)") +GIFT_RULE = re.compile(r"(免費|免費LINE貼圖|中獎|抽獎|贈品|領取|免費領取|點我領|兌換|禮物卡|點數|Steam卡)") +JOB_RULE = re.compile(r"(打工|兼職|在家工作|日領|週領|無經驗|免經驗|高薪|刷流水|刷單|打字員)") +TIME_RULE = re.compile(r"\d+秒|\d+分|^\d{2}:\d{2}(\d{2})?$") + +# 反詐騙及正規金融內容的白名單/降權規則 +DEBUNK_RULE = re.compile(r"反詐騙|165|手法|破解|多問|多查|手法分析|防範|防詐|提醒民眾|MyGoPen|cofacts|宣導") +APR_RULE = re.compile(r"年百分率(\s*\d{1,2}(\.\d{1,2})?%\s*\~\s*\d{1,2}(\.\d{1,2})?%)?|總費用年百分率(\s*\d{1,2}(\.\d{1,2})?%\s*\~\s*\d{1,2}(\.\d{1,2})?%)?|APR") +CACL_RULE = re.compile(r"試算|交易稅|機動計息|本息攤還|每月還款金額|每日還款金額|收入負債比") +DISCLAIMER_RULE = re.compile(r"免責聲明|主管機關|保有核貸與否權利|審慎評估還款能力|實際貸款額度及利率|參閱|產品公開說明書") +PERMISSION_RULE = re.compile(r"免(自行)?上傳(財力|證明)|應用金融科技|授權本行") +NORMAL_PAYMENT_RULE = re.compile( + r"付款成功|付款完成|完成付款|繳費完成|扣款成功|刷卡成功|交易成功|訂單|訂購|發票|" + r"收據|消費通知|入帳通知|轉帳成功|退款完成|付款通知|帳單|繳款通知" +) +OTP_SAFETY_RULE = re.compile(r"請勿|不要|切勿|勿將|勿提供|不要提供|勿告知|請勿告知|本人操作|非本人操作|驗證碼僅供本人") +QUESTION_RULE = re.compile(r"請問|想問|想請教|是否|怎麼|如何|可以|能否|嗎|呢|\?+$|?+$") +BANK_BRAND_RULE = re.compile(r"銀行|本行|信用卡|客服專線|客服中心") +BANK_TXN_NOTIFICATION_RULE = re.compile( + r"線上交易|交易驗證碼|本次交易|商店名稱|交易內容|完成付款|有效時間|" + r"交易頁面|客服專線|如非本人操作|請確認交易內容" +) + +WEAK_SIGNAL_RULES: List[Tuple[str, re.Pattern, int, str]] = [ + ("money", MONEY_RULE, 12, "金流交易相關字詞"), + ("otp", OTP_RULE, 12, "驗證碼相關字詞"), + ("account", ACCOUNT_RULE, 10, "帳戶金融相關字詞"), +] + +STRONG_SIGNAL_RULES: List[Tuple[str, re.Pattern, int, str]] = [ + ("urgency", URGENCY_RULE, 35, "催促或時效壓力用詞"), + ("invest", INVEST_RULE, 45, "投資獲利相關誘導字詞"), + ("impersonate", IMPERSONATE_RULE, 55, "假冒官方客服或異常通知用詞"), + ("sensitive", SENSITIVE_RULE, 45, "敏感金融個資相關用詞"), + ("gift", GIFT_RULE, 35, "贈品中獎相關誘導用詞"), + ("job", JOB_RULE, 30, "高薪兼職相關誘導用詞"), + ("time", TIME_RULE, 20, "時間壓力或操作時限用詞"), +] + +BANK_RULES: List[Tuple[re.Pattern, str]] = [ + (APR_RULE, "年百分率、總費用年百分率等相關用詞"), + (CACL_RULE, "試算款項等相關用詞"), + (DISCLAIMER_RULE, "免責聲明等相關用詞"), + (PERMISSION_RULE, "授權與免責等相關用詞"), +] + +# 僅保留辨識力較高的模糊詞,避免「驗證碼」這類正常場景常見詞直接加權 +FUZZY_KEYWORDS = [ + "保證獲利", + "高報酬", + "帳戶異常", + "虛擬貨幣", + "代操", + "會員指定任務", + "抽獎機會", + "數量有限", + "最後機會", + "認證失敗", + "點我領取", + "限時搶購", + "立即行動", + "官方客服", + "系統升級", + "司法調查", + "金管會公告", +] + + +def compare_rules(text: str) -> Tuple[int, str, List[str]]: + is_debunk = bool(len(re.findall(DEBUNK_RULE, text)) >= 2) + if is_debunk: + return 20, ",含反詐騙宣導用詞", [] + + reason_list: List[str] = [] + match = 0 + for pattern, reason in BANK_RULES: + counter = len(re.findall(pattern, text)) + if counter >= 2: + match += 1 + reason_list.append(reason) + if match == 2: + return 20, "含正規銀行所常用之" + "、".join(reason_list), [] + + bank_notification_markers = 0 + for pattern in (BANK_BRAND_RULE, BANK_TXN_NOTIFICATION_RULE, OTP_SAFETY_RULE, NORMAL_PAYMENT_RULE): + if re.search(pattern, text): + bank_notification_markers += 1 + + if bank_notification_markers >= 3 and re.search(MONEY_RULE, text) and re.search(OTP_RULE, text): + return 20, "含正規銀行交易驗證通知語境", [] + + score = 0 + signal_hits: List[str] = [] + reason_list = [] + + has_payment_context = bool(re.search(NORMAL_PAYMENT_RULE, text)) + has_otp_safety_context = bool(re.search(OTP_SAFETY_RULE, text)) + has_question_context = text.endswith(("?", "?")) or bool(re.search(QUESTION_RULE, text)) + + strong_signal_count = 0 + + for kw in FUZZY_KEYWORDS: + similarity = fuzz.partial_ratio(kw, text) + if similarity > 85: + score += 18 + signal_hits.append(f"fuzzy:{kw}") + reason_list.append(kw) + + for key, pattern, point, sentence in STRONG_SIGNAL_RULES: + if re.findall(pattern, text): + score += point + strong_signal_count += 1 + signal_hits.append(key) + reason_list.append(sentence) + + for key, pattern, point, sentence in WEAK_SIGNAL_RULES: + if not re.findall(pattern, text): + continue + + adjusted_point = point + if key == "money" and (has_payment_context or has_question_context): + adjusted_point = 5 + elif key == "otp" and (has_otp_safety_context or has_question_context): + adjusted_point = 4 + elif key == "account" and has_question_context: + adjusted_point = 4 + + score += adjusted_point + signal_hits.append(key) + reason_list.append(sentence) + + low_signal_combo = {"money", "otp", "account"}.intersection(signal_hits) + if strong_signal_count >= 1 and low_signal_combo: + score += 18 + reason_list.append("金流或驗證資訊與高風險情境交互出現") + + if strong_signal_count == 0 and low_signal_combo: + if has_payment_context or has_otp_safety_context: + score = min(score, 18) + elif has_question_context: + score = min(score, 20) + else: + score = min(score, 28) + + dedup_hits = list(dict.fromkeys(signal_hits)) + dedup_reasons = list(dict.fromkeys(reason_list)) + reason = "疑似包含" + "、".join(dedup_reasons) if dedup_reasons else "" + + return min(score, 100), reason, dedup_hits diff --git a/backend/utils/phone_normalizer.py b/backend/utils/phone_normalizer.py new file mode 100644 index 0000000000000000000000000000000000000000..2138bd55b2ac2cfb8feb8be9a3f9ceac433bb866 --- /dev/null +++ b/backend/utils/phone_normalizer.py @@ -0,0 +1,68 @@ +from __future__ import annotations + +from typing import Any + + +def _is_nan(value: Any) -> bool: + """安全判斷是否為 NaN(避免 float('nan') 這類 truthy 陷阱)""" + try: + return isinstance(value, float) and value != value + except Exception: + return False + + +def _pad_leading_zero(phone: str) -> str: + """補回資料庫格式(去前導0)電話號碼的前導0。 + + 手機:10 碼完整格式,DB 存 9 碼(開頭為 9)。 + 家用市話:9 碼完整格式,DB 存 8 碼。 + """ + if len(phone) == 9 and phone.startswith("9"): + return f"0{phone}" + if len(phone) == 8: + return f"0{phone}" + return phone + + +def _strip_taiwan_country_code(digits: str) -> str: + """去除台灣國碼前綴(00886/886),還原成去前導 0 的國內碼格式。 + + 本地完整號碼一律以 0 開頭,去除國碼後的號碼不會有前導 0, + 因此兩者不會互相誤判。僅在去除國碼後長度為 8 或 9 碼 + (對應台灣手機/市話不含前導 0 的國內碼長度)時才轉換, + 避免誤傷其他無法辨識的號碼。轉換後交給 `_pad_leading_zero` + 統一補回前導 0,這裡不重複處理。 + """ + if digits.startswith("00886"): + rest = digits[5:] + elif digits.startswith("886"): + rest = digits[3:] + else: + return digits + return rest if len(rest) in (8, 9) else digits + + +def normalize(phone: Any) -> str: + """Normalize a phone number to its complete display form (with leading zero).""" + if phone is None or _is_nan(phone): + return "" + if isinstance(phone, float): + if not phone.is_integer(): + # 不應該出現非整數電話號碼,視為髒資料 + return "" + phone = int(phone) + value = str(phone).strip() + if value.lower() in ("nan", "none", ""): + return "" + digits = "".join(ch for ch in value if ch.isdigit()) + digits = _strip_taiwan_country_code(digits) + digits = _pad_leading_zero(digits) + return digits[-10:] if len(digits) >= 10 else digits + + +def normalize_db_phone(phone: Any) -> str: + """轉成資料庫常見格式:手機 10 碼去前導 0 存 9 碼,家用市話 9 碼去前導 0 存 8 碼。""" + normalized = normalize(phone) + if normalized.startswith("0") and len(normalized) in (9, 10): + return normalized[1:] + return normalized diff --git a/backend/utils/text_cleaner.py b/backend/utils/text_cleaner.py new file mode 100644 index 0000000000000000000000000000000000000000..a590b4922c68a8640aca18b158663019faf4df83 --- /dev/null +++ b/backend/utils/text_cleaner.py @@ -0,0 +1,17 @@ +import re + + +# 標準化文字 +def normalize_text(text: str) -> str: + text = text.strip() + # 清掉多餘的空白,只留一個 + text = re.sub(r"\s+", " ", text) + # 清掉多餘的特殊字符 + text = re.sub(r"[-+\-*.]+", "", text) + return text + + + +def normalize_escape_sequences(text: str) -> str: + text = text.replace('\\"', '"').replace("\\r", " ").replace("\\n", " ").replace("\\t", " ") + return text.strip() \ No newline at end of file diff --git a/ca.pem b/ca.pem new file mode 100644 index 0000000000000000000000000000000000000000..17d0a5963c59c0a4f034da965a8fae357898e914 --- /dev/null +++ b/ca.pem @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIERDCCAqygAwIBAgIUNxvCwhUtg6FdHYrY7UaIJ7G4rM4wDQYJKoZIhvcNAQEM +BQAwOjE4MDYGA1UEAwwvOTkwN2Q3ZTctYTYwYy00YmNiLWE0MGQtODQyYzUwZjFi +NGEyIFByb2plY3QgQ0EwHhcNMjYwNzAyMTQzODE2WhcNMzYwNjI5MTQzODE2WjA6 +MTgwNgYDVQQDDC85OTA3ZDdlNy1hNjBjLTRiY2ItYTQwZC04NDJjNTBmMWI0YTIg +UHJvamVjdCBDQTCCAaIwDQYJKoZIhvcNAQEBBQADggGPADCCAYoCggGBAMvTMFm7 +CmRlEFmCllMjv9P6oC+ksAmfVgGQNQesaYwv65110K9QEBfFY7uADJMf5XKKr4fs +ewCd4TUL/7Y1DK1sahTMz47CeeMXaBlwCnmXz8WqGIzEE25KgjgFBb//pUx9ILSf +Po0JagJ10S79TBOypTAvKQI7taBrbZbk/Hogqe+w0re3/uJEJ5PliYY6ddetHStw +YgdHakynHYDcGRLFpIKCXwQOEkHZ1Qh6FEgf+PnEU+D0RHsKuP5rEAEvLu2IBYvB +M7MaOeP29IlAds8m401/FKVCwp72ANHwnR1SuIwzS29pdp1TC3VnmDvDNU5sA22G +m0NCtbpQY3Hy8c4QRzqMfbSWUpTuSKkCT/xXpJg70hcLawwQpE3PsfgNFHArxMMH +S8C58xn53DAbds7IVFyD6XHu4jvZHAx9oj36d4cv3rE/AX11C+tUZjLNBAm0InO5 +3ME3JDFgWAHEdSg2mawI7BC1ImLhbc0XcRoQsPTMLBj7US6Hea9MVeI9qQIDAQAB +o0IwQDAdBgNVHQ4EFgQUWIMuS7lGwmOfyIDDzWwgUfdm9X4wEgYDVR0TAQH/BAgw +BgEB/wIBADALBgNVHQ8EBAMCAQYwDQYJKoZIhvcNAQEMBQADggGBAHbL9teO50vD +zr6t/Rv78Ig/JGeOsAK9IzKCKkov12keBTwrNNea9CxAW2NEk4/jQFxd0ddXulU5 +DTYb2KJRqKs8mvCdCFe4syb2ofZ5FmrRoyGAC4i4R33C6tsqke7Q3w+tYq3u/ec9 +8pQMAp049YbZg7Ihq5Dk9DRI03Pm9L20ro0IjcdGeXmP7De7Gc+Zrq/l5q5DOjhU +zRGLEzCw3nPdgMLDTbEseeG/FxH3o3hxv84P8/uJ5KX9KHmMLNxzl57KUjWbJGfa +Alxav+lBPB1RHG++wOoDJt11OsY5a9qf7fJZrK/QZxT5vFZi7puYuqgehu3yfzUO +DnGfIe/qNVir11me5cmwjOuuGqy1JCv7+i6s5nayHOVvQBB9EVMmNZKkkawzfsZZ +O/ATX8xzrnbbn2aziRyCcrxmjmyIRss4JoVIOBNYuwvCsnJY07SVrY5FtB1uwEC1 +SS5pDqLJSVOK8aU7J8aNWDorlG6BsRq02HEu+gdTB45/nWapsoh7Og== +-----END CERTIFICATE----- diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000000000000000000000000000000000000..cc64f4fa27c27e784d67c8239b084222309545b3 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,44 @@ +[project] +name = "scam-detection-project" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.14" +dependencies = [ + "fastapi[standard]", + "ddgs", + "google-api-python-client", + "google-cloud-vision", + "huggingface-hub>=0.36.2", + "langchain-chroma>=0.2.6", + "langchain-community>=0.4.1", + "langchain-core>=0.3.79", + "langchain-groq>=1.1.2", + "langchain-huggingface>=1.2.2", + "langchain-text-splitters>=0.3.11", + "optimum[onnxruntime]>=1.16.0", + "pandas>=3.0.3", + "pydantic-settings>=2.13.1", + "rapidfuzz>=3.14.5", + "scikit-learn>=1.6.1", + "sentence-transformers>=5.5.0", + "tldextract", + "torch", + "transformers>=4.40.0", + "uvicorn", + "xgboost", + "sqlalchemy", + "pymysql", + "tavily-python>=0.7.26", + "serpapi>=1.0.2", + "networkx>=3.0", + "httpx>=0.28.1", +] + +[dependency-groups] +dev = [ + "pytest", +] + +[tool.fastapi] +entrypoint = "backend.main:app" diff --git a/readme.md b/readme.md new file mode 100644 index 0000000000000000000000000000000000000000..16d532811909c5cb80c806724ec43d9c33380442 --- /dev/null +++ b/readme.md @@ -0,0 +1,8 @@ +--- +title: Scam Detection +emoji: 🔍 +colorFrom: red +colorTo: blue +sdk: docker +sdk_version: "2.0" +--- diff --git a/scripts/check_groq_tavily_workflow.py b/scripts/check_groq_tavily_workflow.py new file mode 100644 index 0000000000000000000000000000000000000000..5bb1c8a4fb286f2976bb986fbebf25864a710bb3 --- /dev/null +++ b/scripts/check_groq_tavily_workflow.py @@ -0,0 +1,89 @@ +"""以真實 Tavily API 驗證確定性價格擷取流程。""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +if str(PROJECT_ROOT) not in sys.path: + sys.path.insert(0, str(PROJECT_ROOT)) + +from backend.core.config import settings +from backend.services.image_price_service.domain.models import \ + MarketplaceCondition +from backend.services.image_price_service.pricing.search_result_price_extractor import \ + extract_prices_from_search_results +from backend.services.image_price_service.pricing.search_tools import \ + search_tavily + + +def configure_console_encoding() -> None: + """避免 Windows 預設 CP950 無法輸出搜尋結果中的特殊符號。""" + for stream in (sys.stdout, sys.stderr): + reconfigure = getattr(stream, "reconfigure", None) + if callable(reconfigure): + reconfigure(encoding="utf-8", errors="replace") + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="實際執行 Tavily 搜尋,再以程式規則擷取價格。", + ) + parser.add_argument( + "--query", + default="Apple iPhone 15 128GB 台灣 全新 價格", + help="測試搜尋關鍵字", + ) + parser.add_argument( + "--max-results", + type=int, + default=5, + help="Tavily 最多回傳幾筆結果,預設為 5", + ) + return parser.parse_args() + + +def main() -> int: + configure_console_encoding() + args = parse_args() + if args.max_results <= 0: + print("失敗:--max-results 必須大於 0。", file=sys.stderr) + return 2 + + if not settings.TAVILY_SEARCH_API_KEY.get_secret_value().strip(): + print( + "失敗:尚未設定 TAVILY_SEARCH_API_KEY。", + file=sys.stderr, + ) + return 2 + + print("步驟 1/2:呼叫 Tavily 搜尋工具。") + print("步驟 2/2:以程式規則擷取價格。") + try: + results = search_tavily(args.query, args.max_results) + candidates = extract_prices_from_search_results( + results, + MarketplaceCondition.NEW, + product_query=args.query, + ) + except Exception as error: + print(f"失敗:workflow 發生錯誤:{error}", file=sys.stderr) + return 1 + + print( + "成功:Tavily 與價格擷取流程均已完成。" + f"Tavily 結果 {len(results)} 筆," + f"價格證據 {len(candidates)} 筆。" + ) + for index, candidate in enumerate(candidates, start=1): + print( + f"{index}. price={candidate.price} " + f"condition={candidate.condition.value}" + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/check_market_search_tools.py b/scripts/check_market_search_tools.py new file mode 100644 index 0000000000000000000000000000000000000000..7b0b46ba5fa2c73fe2311a5042bb596cd5d7ce43 --- /dev/null +++ b/scripts/check_market_search_tools.py @@ -0,0 +1,295 @@ +"""逐一呼叫市場搜尋工具,診斷 API、日誌與回傳內容大小。""" + +from __future__ import annotations + +import argparse +import json +import logging +import sys +import time +from collections.abc import Callable, Mapping, Sequence +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Protocol + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +if str(PROJECT_ROOT) not in sys.path: + sys.path.insert(0, str(PROJECT_ROOT)) + +logger = logging.getLogger("market_search_diagnostic") + +TOOL_LABELS = { + "serpapi": "SerpApi", + "tavily": "Tavily", + "ddgs": "DuckDuckGo", +} + + +class SearchTool(Protocol): + """診斷程式需要的最小搜尋工具介面。""" + + def invoke(self, input: dict[str, object]) -> object: + ... + + +@dataclass(frozen=True) +class SearchDiagnostic: + """單一搜尋工具的診斷摘要。""" + + tool_name: str + success: bool + elapsed_seconds: float + result_count: int + snippet_characters: int + largest_snippet_characters: int + serialized_characters: int + error: str | None = None + + +def configure_console() -> None: + """設定 UTF-8 console 與可看見 INFO 的 root logger。""" + for stream in (sys.stdout, sys.stderr): + reconfigure = getattr(stream, "reconfigure", None) + if callable(reconfigure): + reconfigure(encoding="utf-8", errors="replace") + + logging.basicConfig( + level=logging.INFO, + format="%(levelname)s:%(name)s:%(message)s", + force=True, + ) + root_logger = logging.getLogger() + logger.info( + "日誌已初始化 root_level=%s info_enabled=%s handlers=%d", + logging.getLevelName(root_logger.getEffectiveLevel()), + root_logger.isEnabledFor(logging.INFO), + len(root_logger.handlers), + ) + + +def load_project_tools() -> tuple[dict[str, SearchTool], dict[str, str]]: + """延遲載入正式工具,確保 logging 已先完成初始化。""" + from backend.core.config import settings + from backend.services.image_price_service.pricing.search_tools import ( + search_ddgs, search_serpapi, search_tavily) + + class _FnAdapter: + """將純函式包裝成 SearchTool 介面。""" + + def __init__(self, fn: Any) -> None: + self._fn = fn + + def invoke(self, input: dict[str, object]) -> object: + return self._fn( + str(input.get("query", "")), + int(input.get("max_results", 10)), + ) + + tools: dict[str, SearchTool] = { + "serpapi": _FnAdapter(search_serpapi), + "tavily": _FnAdapter(search_tavily), + "ddgs": _FnAdapter(search_ddgs), + } + credential_status = { + "serpapi": ( + "已設定" + if settings.SERP_API_KEY.get_secret_value().strip() + else "未設定" + ), + "tavily": ( + "已設定" + if settings.TAVILY_SEARCH_API_KEY.get_secret_value().strip() + else "未設定" + ), + "ddgs": "不需 API 金鑰", + } + return tools, credential_status + + +def _normalized_results(raw_results: object) -> list[dict[str, Any]]: + if not isinstance(raw_results, list): + raise TypeError( + f"工具回傳型別應為 list,實際為 {type(raw_results).__name__}" + ) + return [item for item in raw_results if isinstance(item, dict)] + + +def diagnose_tool( + tool_name: str, + tool: SearchTool, + *, + query: str, + max_results: int, + preview_characters: int, + clock: Callable[[], float] = time.perf_counter, +) -> SearchDiagnostic: + """呼叫一次工具,並記錄成功、耗時、結果數與內容大小。""" + label = TOOL_LABELS.get(tool_name, tool_name) + started_at = clock() + logger.info( + "搜尋工具呼叫開始 工具=%s query=%r max_results=%d", + label, + query, + max_results, + ) + try: + raw_results = tool.invoke( + { + "query": query, + "max_results": max_results, + } + ) + results = _normalized_results(raw_results) + except Exception as error: + elapsed = max(0.0, clock() - started_at) + logger.error( + "搜尋工具呼叫失敗 工具=%s 耗時=%.3f 秒 " + "錯誤類型=%s 錯誤=%s", + label, + elapsed, + type(error).__name__, + str(error), + ) + return SearchDiagnostic( + tool_name=tool_name, + success=False, + elapsed_seconds=elapsed, + result_count=0, + snippet_characters=0, + largest_snippet_characters=0, + serialized_characters=0, + error=f"{type(error).__name__}: {error}", + ) + + elapsed = max(0.0, clock() - started_at) + snippets = [ + str(result.get("snippet", "")).strip() + for result in results + ] + serialized_characters = len( + json.dumps(results, ensure_ascii=False) + ) + diagnostic = SearchDiagnostic( + tool_name=tool_name, + success=True, + elapsed_seconds=elapsed, + result_count=len(results), + snippet_characters=sum(len(snippet) for snippet in snippets), + largest_snippet_characters=max( + (len(snippet) for snippet in snippets), + default=0, + ), + serialized_characters=serialized_characters, + ) + logger.info( + "搜尋工具呼叫完成 工具=%s 耗時=%.3f 秒 結果數=%d " + "摘要總字元=%d 最大摘要字元=%d 序列化總字元=%d", + label, + diagnostic.elapsed_seconds, + diagnostic.result_count, + diagnostic.snippet_characters, + diagnostic.largest_snippet_characters, + diagnostic.serialized_characters, + ) + + for index, result in enumerate(results, start=1): + title = str(result.get("title", "")).strip() or "(無標題)" + snippet = str(result.get("snippet", "")).strip().replace("\n", " ") + preview = snippet + if len(preview) > preview_characters: + preview = f"{preview[:preview_characters]}..." + logger.info( + "搜尋結果 工具=%s 序號=%d 標題=%r " + "摘要字元=%d 摘要預覽=%r", + label, + index, + title, + len(snippet), + preview, + ) + + return diagnostic + + +def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace: + parser = argparse.ArgumentParser( + description=( + "逐一測試 SerpApi、Tavily、DDGS 搜尋工具;" + "本程式不會呼叫 Groq。" + ), + ) + parser.add_argument( + "--tool", + choices=("all", *TOOL_LABELS), + default="all", + help="要測試的工具,預設為 all。", + ) + parser.add_argument( + "--query", + default="Apple iPhone 15 128GB 台灣 二手 價格", + help="測試搜尋關鍵字。", + ) + parser.add_argument( + "--max-results", + type=int, + default=3, + help="每個工具最多回傳幾筆結果,預設為 3。", + ) + parser.add_argument( + "--preview-characters", + type=int, + default=160, + help="每筆摘要最多顯示幾個字元,預設為 160。", + ) + return parser.parse_args(argv) + + +def main(argv: Sequence[str] | None = None) -> int: + configure_console() + args = parse_args(argv) + if args.max_results <= 0: + logger.error("--max-results 必須大於 0") + return 2 + if args.preview_characters <= 0: + logger.error("--preview-characters 必須大於 0") + return 2 + if not args.query.strip(): + logger.error("--query 不可為空白") + return 2 + + tools, credential_status = load_project_tools() + selected_names = ( + tuple(TOOL_LABELS) + if args.tool == "all" + else (args.tool,) + ) + logger.info("本程式只測試搜尋 API,不會呼叫 Groq") + for name in selected_names: + logger.info( + "搜尋工具設定 工具=%s API金鑰=%s", + TOOL_LABELS[name], + credential_status[name], + ) + + diagnostics = [ + diagnose_tool( + name, + tools[name], + query=args.query.strip(), + max_results=args.max_results, + preview_characters=args.preview_characters, + ) + for name in selected_names + ] + succeeded = sum(item.success for item in diagnostics) + failed = len(diagnostics) - succeeded + logger.info( + "診斷完成 成功工具數=%d 失敗工具數=%d", + succeeded, + failed, + ) + return 1 if failed else 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/check_phone_blacklist_labels.py b/scripts/check_phone_blacklist_labels.py new file mode 100644 index 0000000000000000000000000000000000000000..803febffcd265b3b4f0cb8bb856b5697bd959a4f --- /dev/null +++ b/scripts/check_phone_blacklist_labels.py @@ -0,0 +1,21 @@ +from sqlalchemy import create_engine, text + +from backend.core.config import settings + +engine = create_engine( + ( + f"mysql+pymysql://{settings.DB_USERNAME}:{settings.DB_PASSWORD}@" + f"{settings.DB_HOST}:{settings.DB_PORT}/{settings.DB_NAME}?charset=utf8mb4" + ), + connect_args={"ssl": {"ca": settings.DB_SSL_CA}} if settings.DB_SSL_CA else {}, + pool_pre_ping=True, +) + +with engine.connect() as conn: + labels = conn.execute( + text("SELECT DISTINCT phone_type FROM phone WHERE status = 'black' ORDER BY phone_type") + ).fetchall() + values = [row[0] for row in labels] + print("BLACKLIST_LABELS:", values) + if not values: + print("NO_BLACKLIST_LABELS_FOUND") diff --git a/scripts/check_tavily_api.py b/scripts/check_tavily_api.py new file mode 100644 index 0000000000000000000000000000000000000000..fcb2eaac6e3938197432d6dd3c7103ae63f258ce --- /dev/null +++ b/scripts/check_tavily_api.py @@ -0,0 +1,87 @@ +"""實際呼叫專案的 Tavily 搜尋工具,確認 API 設定與連線是否正常。""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +if str(PROJECT_ROOT) not in sys.path: + sys.path.insert(0, str(PROJECT_ROOT)) + +from backend.core.config import settings +from backend.services.image_price_service.pricing.search_tools import \ + search_tavily + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="送出一次真實 Tavily 搜尋,並顯示標準化搜尋結果。", + ) + parser.add_argument( + "--query", + default="Apple iPhone 15 台灣 全新 價格", + help="測試搜尋關鍵字", + ) + parser.add_argument( + "--max-results", + type=int, + default=3, + help="最多回傳幾筆結果,預設為 3", + ) + return parser.parse_args() + + +def configure_console_encoding() -> None: + """避免 Windows 預設 CP950 因搜尋摘要含特殊符號而輸出失敗。""" + for stream in (sys.stdout, sys.stderr): + reconfigure = getattr(stream, "reconfigure", None) + if callable(reconfigure): + reconfigure(encoding="utf-8", errors="replace") + + +def main() -> int: + configure_console_encoding() + args = parse_args() + if args.max_results <= 0: + print("失敗:--max-results 必須大於 0。", file=sys.stderr) + return 2 + + api_key = settings.TAVILY_SEARCH_API_KEY.get_secret_value().strip() + if not api_key: + print( + "失敗:尚未設定 TAVILY_SEARCH_API_KEY。" + "請在專案根目錄或 backend/.env 中設定。", + file=sys.stderr, + ) + return 2 + + print(f"正在呼叫 Tavily:query={args.query!r}, max_results={args.max_results}") + try: + results = search_tavily( + args.query, + args.max_results, + ) + except Exception as error: + print( + f"失敗:Tavily API 呼叫發生錯誤:{error}", + file=sys.stderr, + ) + return 1 + + print(f"成功:Tavily API 已回應,共取得 {len(results)} 筆結果。") + for index, result in enumerate(results, start=1): + title = str(result.get("title", "")).strip() or "(無標題)" + snippet = str(result.get("snippet", "")).strip().replace("\n", " ") + if len(snippet) > 160: + snippet = f"{snippet[:157]}..." + print(f"{index}. {title}") + if snippet: + print(f" {snippet}") + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/print_db_settings.py b/scripts/print_db_settings.py new file mode 100644 index 0000000000000000000000000000000000000000..44f4f9e80677f7c73b1d05dba61a1d695d03f1d3 --- /dev/null +++ b/scripts/print_db_settings.py @@ -0,0 +1,18 @@ +import sys +import os + +# ensure project root is on sys.path so `backend` package can be imported +sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) + +from backend.core.config import settings + +def main(): + print("DB_HOST=", settings.DB_HOST) + print("DB_PORT=", settings.DB_PORT) + print("DB_NAME=", settings.DB_NAME) + print("DB_USERNAME=", settings.DB_USERNAME) + print("DB_SSL_CA=", settings.DB_SSL_CA) + + +if __name__ == "__main__": + main() diff --git a/scripts/test_db_connection.py b/scripts/test_db_connection.py new file mode 100644 index 0000000000000000000000000000000000000000..35798f1076f7cd803ed93af7d2ffe0066ac03e36 --- /dev/null +++ b/scripts/test_db_connection.py @@ -0,0 +1,49 @@ +import sys +import os +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +from backend.core.config import settings +from backend.persistence.mysql_connection import engine +from sqlalchemy import text + + +def main(): + print("Using DB settings:") + print(" host:", settings.DB_HOST) + print(" port:", settings.DB_PORT) + print(" name:", settings.DB_NAME) + print(" user:", settings.DB_USERNAME) + print(" ssl_ca:", settings.DB_SSL_CA) + + # check for ca file + possible_paths = [ + Path(settings.DB_SSL_CA), + Path(__file__).resolve().parent.parent / settings.DB_SSL_CA, + Path(__file__).resolve().parent / settings.DB_SSL_CA, + ] + found = None + for p in possible_paths: + if p.exists(): + found = p + break + + if found: + print("Found CA file at:", found) + else: + print("CA file not found in project paths; SSL may still work if DB does not require CA file") + + print("Attempting DB connection (this may take a few seconds)...") + try: + with engine.connect() as conn: + # simple test query + res = conn.execute(text("SELECT 1")).scalar() + print("Query result:", res) + print("Connection successful.") + except Exception as e: + print("Connection failed:", type(e).__name__, str(e)) + + +if __name__ == "__main__": + main() diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000000000000000000000000000000000000..7b33eace1803fdbac63a5ac2194e5e6423561e6d --- /dev/null +++ b/uv.lock @@ -0,0 +1,3468 @@ +version = 1 +revision = 3 +requires-python = ">=3.14" +resolution-markers = [ + "platform_machine != 's390x' and sys_platform == 'win32'", + "platform_machine != 's390x' and sys_platform == 'emscripten'", + "platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "platform_machine == 's390x' and sys_platform == 'win32'", + "platform_machine == 's390x' and sys_platform == 'emscripten'", + "platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] + +[[package]] +name = "aiohappyeyeballs" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, +] + +[[package]] +name = "aiohttp" +version = "3.13.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/77/9a/152096d4808df8e4268befa55fba462f440f14beab85e8ad9bf990516918/aiohttp-3.13.5.tar.gz", hash = "sha256:9d98cc980ecc96be6eb4c1994ce35d28d8b1f5e5208a23b421187d1209dbb7d1", size = 7858271, upload-time = "2026-03-31T22:01:03.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/ce/46572759afc859e867a5bc8ec3487315869013f59281ce61764f76d879de/aiohttp-3.13.5-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:eb4639f32fd4a9904ab8fb45bf3383ba71137f3d9d4ba25b3b3f3109977c5b8c", size = 745721, upload-time = "2026-03-31T21:58:50.229Z" }, + { url = "https://files.pythonhosted.org/packages/13/fe/8a2efd7626dbe6049b2ef8ace18ffda8a4dfcbe1bcff3ac30c0c7575c20b/aiohttp-3.13.5-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:7e5dc4311bd5ac493886c63cbf76ab579dbe4641268e7c74e48e774c74b6f2be", size = 497663, upload-time = "2026-03-31T21:58:52.232Z" }, + { url = "https://files.pythonhosted.org/packages/9b/91/cc8cc78a111826c54743d88651e1687008133c37e5ee615fee9b57990fac/aiohttp-3.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:756c3c304d394977519824449600adaf2be0ccee76d206ee339c5e76b70ded25", size = 499094, upload-time = "2026-03-31T21:58:54.566Z" }, + { url = "https://files.pythonhosted.org/packages/0a/33/a8362cb15cf16a3af7e86ed11962d5cd7d59b449202dc576cdc731310bde/aiohttp-3.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecc26751323224cf8186efcf7fbcbc30f4e1d8c7970659daf25ad995e4032a56", size = 1726701, upload-time = "2026-03-31T21:58:56.864Z" }, + { url = "https://files.pythonhosted.org/packages/45/0c/c091ac5c3a17114bd76cbf85d674650969ddf93387876cf67f754204bd77/aiohttp-3.13.5-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10a75acfcf794edf9d8db50e5a7ec5fc818b2a8d3f591ce93bc7b1210df016d2", size = 1683360, upload-time = "2026-03-31T21:58:59.072Z" }, + { url = "https://files.pythonhosted.org/packages/23/73/bcee1c2b79bc275e964d1446c55c54441a461938e70267c86afaae6fba27/aiohttp-3.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f7a18f258d124cd678c5fe072fe4432a4d5232b0657fca7c1847f599233c83a", size = 1773023, upload-time = "2026-03-31T21:59:01.776Z" }, + { url = "https://files.pythonhosted.org/packages/c7/ef/720e639df03004fee2d869f771799d8c23046dec47d5b81e396c7cda583a/aiohttp-3.13.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:df6104c009713d3a89621096f3e3e88cc323fd269dbd7c20afe18535094320be", size = 1853795, upload-time = "2026-03-31T21:59:04.568Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c9/989f4034fb46841208de7aeeac2c6d8300745ab4f28c42f629ba77c2d916/aiohttp-3.13.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:241a94f7de7c0c3b616627aaad530fe2cb620084a8b144d3be7b6ecfe95bae3b", size = 1730405, upload-time = "2026-03-31T21:59:07.221Z" }, + { url = "https://files.pythonhosted.org/packages/ce/75/ee1fd286ca7dc599d824b5651dad7b3be7ff8d9a7e7b3fe9820d9180f7db/aiohttp-3.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c974fb66180e58709b6fc402846f13791240d180b74de81d23913abe48e96d94", size = 1558082, upload-time = "2026-03-31T21:59:09.484Z" }, + { url = "https://files.pythonhosted.org/packages/c3/20/1e9e6650dfc436340116b7aa89ff8cb2bbdf0abc11dfaceaad8f74273a10/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6e27ea05d184afac78aabbac667450c75e54e35f62238d44463131bd3f96753d", size = 1692346, upload-time = "2026-03-31T21:59:12.068Z" }, + { url = "https://files.pythonhosted.org/packages/d8/40/8ebc6658d48ea630ac7903912fe0dd4e262f0e16825aa4c833c56c9f1f56/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a79a6d399cef33a11b6f004c67bb07741d91f2be01b8d712d52c75711b1e07c7", size = 1698891, upload-time = "2026-03-31T21:59:14.552Z" }, + { url = "https://files.pythonhosted.org/packages/d8/78/ea0ae5ec8ba7a5c10bdd6e318f1ba5e76fcde17db8275188772afc7917a4/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c632ce9c0b534fbe25b52c974515ed674937c5b99f549a92127c85f771a78772", size = 1742113, upload-time = "2026-03-31T21:59:17.068Z" }, + { url = "https://files.pythonhosted.org/packages/8a/66/9d308ed71e3f2491be1acb8769d96c6f0c47d92099f3bc9119cada27b357/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fceedde51fbd67ee2bcc8c0b33d0126cc8b51ef3bbde2f86662bd6d5a6f10ec5", size = 1553088, upload-time = "2026-03-31T21:59:19.541Z" }, + { url = "https://files.pythonhosted.org/packages/da/a6/6cc25ed8dfc6e00c90f5c6d126a98e2cf28957ad06fa1036bd34b6f24a2c/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f92995dfec9420bb69ae629abf422e516923ba79ba4403bc750d94fb4a6c68c1", size = 1757976, upload-time = "2026-03-31T21:59:22.311Z" }, + { url = "https://files.pythonhosted.org/packages/c1/2b/cce5b0ffe0de99c83e5e36d8f828e4161e415660a9f3e58339d07cce3006/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20ae0ff08b1f2c8788d6fb85afcb798654ae6ba0b747575f8562de738078457b", size = 1712444, upload-time = "2026-03-31T21:59:24.635Z" }, + { url = "https://files.pythonhosted.org/packages/6c/cf/9e1795b4160c58d29421eafd1a69c6ce351e2f7c8d3c6b7e4ca44aea1a5b/aiohttp-3.13.5-cp314-cp314-win32.whl", hash = "sha256:b20df693de16f42b2472a9c485e1c948ee55524786a0a34345511afdd22246f3", size = 438128, upload-time = "2026-03-31T21:59:27.291Z" }, + { url = "https://files.pythonhosted.org/packages/22/4d/eaedff67fc805aeba4ba746aec891b4b24cebb1a7d078084b6300f79d063/aiohttp-3.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:f85c6f327bf0b8c29da7d93b1cabb6363fb5e4e160a32fa241ed2dce21b73162", size = 464029, upload-time = "2026-03-31T21:59:29.429Z" }, + { url = "https://files.pythonhosted.org/packages/79/11/c27d9332ee20d68dd164dc12a6ecdef2e2e35ecc97ed6cf0d2442844624b/aiohttp-3.13.5-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:1efb06900858bb618ff5cee184ae2de5828896c448403d51fb633f09e109be0a", size = 778758, upload-time = "2026-03-31T21:59:31.547Z" }, + { url = "https://files.pythonhosted.org/packages/04/fb/377aead2e0a3ba5f09b7624f702a964bdf4f08b5b6728a9799830c80041e/aiohttp-3.13.5-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:fee86b7c4bd29bdaf0d53d14739b08a106fdda809ca5fe032a15f52fae5fe254", size = 512883, upload-time = "2026-03-31T21:59:34.098Z" }, + { url = "https://files.pythonhosted.org/packages/bb/a6/aa109a33671f7a5d3bd78b46da9d852797c5e665bfda7d6b373f56bff2ec/aiohttp-3.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:20058e23909b9e65f9da62b396b77dfa95965cbe840f8def6e572538b1d32e36", size = 516668, upload-time = "2026-03-31T21:59:36.497Z" }, + { url = "https://files.pythonhosted.org/packages/79/b3/ca078f9f2fa9563c36fb8ef89053ea2bb146d6f792c5104574d49d8acb63/aiohttp-3.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cf20a8d6868cb15a73cab329ffc07291ba8c22b1b88176026106ae39aa6df0f", size = 1883461, upload-time = "2026-03-31T21:59:38.723Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e3/a7ad633ca1ca497b852233a3cce6906a56c3225fb6d9217b5e5e60b7419d/aiohttp-3.13.5-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:330f5da04c987f1d5bdb8ae189137c77139f36bd1cb23779ca1a354a4b027800", size = 1747661, upload-time = "2026-03-31T21:59:41.187Z" }, + { url = "https://files.pythonhosted.org/packages/33/b9/cd6fe579bed34a906d3d783fe60f2fa297ef55b27bb4538438ee49d4dc41/aiohttp-3.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f1cbf0c7926d315c3c26c2da41fd2b5d2fe01ac0e157b78caefc51a782196cf", size = 1863800, upload-time = "2026-03-31T21:59:43.84Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3f/2c1e2f5144cefa889c8afd5cf431994c32f3b29da9961698ff4e3811b79a/aiohttp-3.13.5-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:53fc049ed6390d05423ba33103ded7281fe897cf97878f369a527070bd95795b", size = 1958382, upload-time = "2026-03-31T21:59:46.187Z" }, + { url = "https://files.pythonhosted.org/packages/66/1d/f31ec3f1013723b3babe3609e7f119c2c2fb6ef33da90061a705ef3e1bc8/aiohttp-3.13.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:898703aa2667e3c5ca4c54ca36cd73f58b7a38ef87a5606414799ebce4d3fd3a", size = 1803724, upload-time = "2026-03-31T21:59:48.656Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b4/57712dfc6f1542f067daa81eb61da282fab3e6f1966fca25db06c4fc62d5/aiohttp-3.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0494a01ca9584eea1e5fbd6d748e61ecff218c51b576ee1999c23db7066417d8", size = 1640027, upload-time = "2026-03-31T21:59:51.284Z" }, + { url = "https://files.pythonhosted.org/packages/25/3c/734c878fb43ec083d8e31bf029daae1beafeae582d1b35da234739e82ee7/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6cf81fe010b8c17b09495cbd15c1d35afbc8fb405c0c9cf4738e5ae3af1d65be", size = 1806644, upload-time = "2026-03-31T21:59:53.753Z" }, + { url = "https://files.pythonhosted.org/packages/20/a5/f671e5cbec1c21d044ff3078223f949748f3a7f86b14e34a365d74a5d21f/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:c564dd5f09ddc9d8f2c2d0a301cd30a79a2cc1b46dd1a73bef8f0038863d016b", size = 1791630, upload-time = "2026-03-31T21:59:56.239Z" }, + { url = "https://files.pythonhosted.org/packages/0b/63/fb8d0ad63a0b8a99be97deac8c04dacf0785721c158bdf23d679a87aa99e/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2994be9f6e51046c4f864598fd9abeb4fba6e88f0b2152422c9666dcd4aea9c6", size = 1809403, upload-time = "2026-03-31T21:59:59.103Z" }, + { url = "https://files.pythonhosted.org/packages/59/0c/bfed7f30662fcf12206481c2aac57dedee43fe1c49275e85b3a1e1742294/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:157826e2fa245d2ef46c83ea8a5faf77ca19355d278d425c29fda0beb3318037", size = 1634924, upload-time = "2026-03-31T22:00:02.116Z" }, + { url = "https://files.pythonhosted.org/packages/17/d6/fd518d668a09fd5a3319ae5e984d4d80b9a4b3df4e21c52f02251ef5a32e/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:a8aca50daa9493e9e13c0f566201a9006f080e7c50e5e90d0b06f53146a54500", size = 1836119, upload-time = "2026-03-31T22:00:04.756Z" }, + { url = "https://files.pythonhosted.org/packages/78/b7/15fb7a9d52e112a25b621c67b69c167805cb1f2ab8f1708a5c490d1b52fe/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3b13560160d07e047a93f23aaa30718606493036253d5430887514715b67c9d9", size = 1772072, upload-time = "2026-03-31T22:00:07.494Z" }, + { url = "https://files.pythonhosted.org/packages/7e/df/57ba7f0c4a553fc2bd8b6321df236870ec6fd64a2a473a8a13d4f733214e/aiohttp-3.13.5-cp314-cp314t-win32.whl", hash = "sha256:9a0f4474b6ea6818b41f82172d799e4b3d29e22c2c520ce4357856fced9af2f8", size = 471819, upload-time = "2026-03-31T22:00:10.277Z" }, + { url = "https://files.pythonhosted.org/packages/62/29/2f8418269e46454a26171bfdd6a055d74febf32234e474930f2f60a17145/aiohttp-3.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:18a2f6c1182c51baa1d28d68fea51513cb2a76612f038853c0ad3c145423d3d9", size = 505441, upload-time = "2026-03-31T22:00:12.791Z" }, +] + +[[package]] +name = "aiosignal" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, +] + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, +] + +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + +[[package]] +name = "bcrypt" +version = "5.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/36/3329e2518d70ad8e2e5817d5a4cac6bba05a47767ec416c7d020a965f408/bcrypt-5.0.0.tar.gz", hash = "sha256:f748f7c2d6fd375cc93d3fba7ef4a9e3a092421b8dbf34d8d4dc06be9492dfdd", size = 25386, upload-time = "2025-09-25T19:50:47.829Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/14/c18006f91816606a4abe294ccc5d1e6f0e42304df5a33710e9e8e95416e1/bcrypt-5.0.0-cp314-cp314t-macosx_10_12_universal2.whl", hash = "sha256:4870a52610537037adb382444fefd3706d96d663ac44cbb2f37e3919dca3d7ef", size = 481862, upload-time = "2025-09-25T19:49:28.365Z" }, + { url = "https://files.pythonhosted.org/packages/67/49/dd074d831f00e589537e07a0725cf0e220d1f0d5d8e85ad5bbff251c45aa/bcrypt-5.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48f753100931605686f74e27a7b49238122aa761a9aefe9373265b8b7aa43ea4", size = 268544, upload-time = "2025-09-25T19:49:30.39Z" }, + { url = "https://files.pythonhosted.org/packages/f5/91/50ccba088b8c474545b034a1424d05195d9fcbaaf802ab8bfe2be5a4e0d7/bcrypt-5.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f70aadb7a809305226daedf75d90379c397b094755a710d7014b8b117df1ebbf", size = 271787, upload-time = "2025-09-25T19:49:32.144Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e7/d7dba133e02abcda3b52087a7eea8c0d4f64d3e593b4fffc10c31b7061f3/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:744d3c6b164caa658adcb72cb8cc9ad9b4b75c7db507ab4bc2480474a51989da", size = 269753, upload-time = "2025-09-25T19:49:33.885Z" }, + { url = "https://files.pythonhosted.org/packages/33/fc/5b145673c4b8d01018307b5c2c1fc87a6f5a436f0ad56607aee389de8ee3/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a28bc05039bdf3289d757f49d616ab3efe8cf40d8e8001ccdd621cd4f98f4fc9", size = 289587, upload-time = "2025-09-25T19:49:35.144Z" }, + { url = "https://files.pythonhosted.org/packages/27/d7/1ff22703ec6d4f90e62f1a5654b8867ef96bafb8e8102c2288333e1a6ca6/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:7f277a4b3390ab4bebe597800a90da0edae882c6196d3038a73adf446c4f969f", size = 272178, upload-time = "2025-09-25T19:49:36.793Z" }, + { url = "https://files.pythonhosted.org/packages/c8/88/815b6d558a1e4d40ece04a2f84865b0fef233513bd85fd0e40c294272d62/bcrypt-5.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:79cfa161eda8d2ddf29acad370356b47f02387153b11d46042e93a0a95127493", size = 269295, upload-time = "2025-09-25T19:49:38.164Z" }, + { url = "https://files.pythonhosted.org/packages/51/8c/e0db387c79ab4931fc89827d37608c31cc57b6edc08ccd2386139028dc0d/bcrypt-5.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a5393eae5722bcef046a990b84dff02b954904c36a194f6cfc817d7dca6c6f0b", size = 271700, upload-time = "2025-09-25T19:49:39.917Z" }, + { url = "https://files.pythonhosted.org/packages/06/83/1570edddd150f572dbe9fc00f6203a89fc7d4226821f67328a85c330f239/bcrypt-5.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7f4c94dec1b5ab5d522750cb059bb9409ea8872d4494fd152b53cca99f1ddd8c", size = 334034, upload-time = "2025-09-25T19:49:41.227Z" }, + { url = "https://files.pythonhosted.org/packages/c9/f2/ea64e51a65e56ae7a8a4ec236c2bfbdd4b23008abd50ac33fbb2d1d15424/bcrypt-5.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0cae4cb350934dfd74c020525eeae0a5f79257e8a201c0c176f4b84fdbf2a4b4", size = 352766, upload-time = "2025-09-25T19:49:43.08Z" }, + { url = "https://files.pythonhosted.org/packages/d7/d4/1a388d21ee66876f27d1a1f41287897d0c0f1712ef97d395d708ba93004c/bcrypt-5.0.0-cp314-cp314t-win32.whl", hash = "sha256:b17366316c654e1ad0306a6858e189fc835eca39f7eb2cafd6aaca8ce0c40a2e", size = 152449, upload-time = "2025-09-25T19:49:44.971Z" }, + { url = "https://files.pythonhosted.org/packages/3f/61/3291c2243ae0229e5bca5d19f4032cecad5dfb05a2557169d3a69dc0ba91/bcrypt-5.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:92864f54fb48b4c718fc92a32825d0e42265a627f956bc0361fe869f1adc3e7d", size = 149310, upload-time = "2025-09-25T19:49:46.162Z" }, + { url = "https://files.pythonhosted.org/packages/3e/89/4b01c52ae0c1a681d4021e5dd3e45b111a8fb47254a274fa9a378d8d834b/bcrypt-5.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dd19cf5184a90c873009244586396a6a884d591a5323f0e8a5922560718d4993", size = 143761, upload-time = "2025-09-25T19:49:47.345Z" }, + { url = "https://files.pythonhosted.org/packages/84/29/6237f151fbfe295fe3e074ecc6d44228faa1e842a81f6d34a02937ee1736/bcrypt-5.0.0-cp38-abi3-macosx_10_12_universal2.whl", hash = "sha256:fc746432b951e92b58317af8e0ca746efe93e66555f1b40888865ef5bf56446b", size = 494553, upload-time = "2025-09-25T19:49:49.006Z" }, + { url = "https://files.pythonhosted.org/packages/45/b6/4c1205dde5e464ea3bd88e8742e19f899c16fa8916fb8510a851fae985b5/bcrypt-5.0.0-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c2388ca94ffee269b6038d48747f4ce8df0ffbea43f31abfa18ac72f0218effb", size = 275009, upload-time = "2025-09-25T19:49:50.581Z" }, + { url = "https://files.pythonhosted.org/packages/3b/71/427945e6ead72ccffe77894b2655b695ccf14ae1866cd977e185d606dd2f/bcrypt-5.0.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:560ddb6ec730386e7b3b26b8b4c88197aaed924430e7b74666a586ac997249ef", size = 278029, upload-time = "2025-09-25T19:49:52.533Z" }, + { url = "https://files.pythonhosted.org/packages/17/72/c344825e3b83c5389a369c8a8e58ffe1480b8a699f46c127c34580c4666b/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d79e5c65dcc9af213594d6f7f1fa2c98ad3fc10431e7aa53c176b441943efbdd", size = 275907, upload-time = "2025-09-25T19:49:54.709Z" }, + { url = "https://files.pythonhosted.org/packages/0b/7e/d4e47d2df1641a36d1212e5c0514f5291e1a956a7749f1e595c07a972038/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2b732e7d388fa22d48920baa267ba5d97cca38070b69c0e2d37087b381c681fd", size = 296500, upload-time = "2025-09-25T19:49:56.013Z" }, + { url = "https://files.pythonhosted.org/packages/0f/c3/0ae57a68be2039287ec28bc463b82e4b8dc23f9d12c0be331f4782e19108/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0c8e093ea2532601a6f686edbc2c6b2ec24131ff5c52f7610dd64fa4553b5464", size = 278412, upload-time = "2025-09-25T19:49:57.356Z" }, + { url = "https://files.pythonhosted.org/packages/45/2b/77424511adb11e6a99e3a00dcc7745034bee89036ad7d7e255a7e47be7d8/bcrypt-5.0.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5b1589f4839a0899c146e8892efe320c0fa096568abd9b95593efac50a87cb75", size = 275486, upload-time = "2025-09-25T19:49:59.116Z" }, + { url = "https://files.pythonhosted.org/packages/43/0a/405c753f6158e0f3f14b00b462d8bca31296f7ecfc8fc8bc7919c0c7d73a/bcrypt-5.0.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:89042e61b5e808b67daf24a434d89bab164d4de1746b37a8d173b6b14f3db9ff", size = 277940, upload-time = "2025-09-25T19:50:00.869Z" }, + { url = "https://files.pythonhosted.org/packages/62/83/b3efc285d4aadc1fa83db385ec64dcfa1707e890eb42f03b127d66ac1b7b/bcrypt-5.0.0-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:e3cf5b2560c7b5a142286f69bde914494b6d8f901aaa71e453078388a50881c4", size = 310776, upload-time = "2025-09-25T19:50:02.393Z" }, + { url = "https://files.pythonhosted.org/packages/95/7d/47ee337dacecde6d234890fe929936cb03ebc4c3a7460854bbd9c97780b8/bcrypt-5.0.0-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f632fd56fc4e61564f78b46a2269153122db34988e78b6be8b32d28507b7eaeb", size = 312922, upload-time = "2025-09-25T19:50:04.232Z" }, + { url = "https://files.pythonhosted.org/packages/d6/3a/43d494dfb728f55f4e1cf8fd435d50c16a2d75493225b54c8d06122523c6/bcrypt-5.0.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:801cad5ccb6b87d1b430f183269b94c24f248dddbbc5c1f78b6ed231743e001c", size = 341367, upload-time = "2025-09-25T19:50:05.559Z" }, + { url = "https://files.pythonhosted.org/packages/55/ab/a0727a4547e383e2e22a630e0f908113db37904f58719dc48d4622139b5c/bcrypt-5.0.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3cf67a804fc66fc217e6914a5635000259fbbbb12e78a99488e4d5ba445a71eb", size = 359187, upload-time = "2025-09-25T19:50:06.916Z" }, + { url = "https://files.pythonhosted.org/packages/1b/bb/461f352fdca663524b4643d8b09e8435b4990f17fbf4fea6bc2a90aa0cc7/bcrypt-5.0.0-cp38-abi3-win32.whl", hash = "sha256:3abeb543874b2c0524ff40c57a4e14e5d3a66ff33fb423529c88f180fd756538", size = 153752, upload-time = "2025-09-25T19:50:08.515Z" }, + { url = "https://files.pythonhosted.org/packages/41/aa/4190e60921927b7056820291f56fc57d00d04757c8b316b2d3c0d1d6da2c/bcrypt-5.0.0-cp38-abi3-win_amd64.whl", hash = "sha256:35a77ec55b541e5e583eb3436ffbbf53b0ffa1fa16ca6782279daf95d146dcd9", size = 150881, upload-time = "2025-09-25T19:50:09.742Z" }, + { url = "https://files.pythonhosted.org/packages/54/12/cd77221719d0b39ac0b55dbd39358db1cd1246e0282e104366ebbfb8266a/bcrypt-5.0.0-cp38-abi3-win_arm64.whl", hash = "sha256:cde08734f12c6a4e28dc6755cd11d3bdfea608d93d958fffbe95a7026ebe4980", size = 144931, upload-time = "2025-09-25T19:50:11.016Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ba/2af136406e1c3839aea9ecadc2f6be2bcd1eff255bd451dd39bcf302c47a/bcrypt-5.0.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0c418ca99fd47e9c59a301744d63328f17798b5947b0f791e9af3c1c499c2d0a", size = 495313, upload-time = "2025-09-25T19:50:12.309Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ee/2f4985dbad090ace5ad1f7dd8ff94477fe089b5fab2040bd784a3d5f187b/bcrypt-5.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddb4e1500f6efdd402218ffe34d040a1196c072e07929b9820f363a1fd1f4191", size = 275290, upload-time = "2025-09-25T19:50:13.673Z" }, + { url = "https://files.pythonhosted.org/packages/e4/6e/b77ade812672d15cf50842e167eead80ac3514f3beacac8902915417f8b7/bcrypt-5.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7aeef54b60ceddb6f30ee3db090351ecf0d40ec6e2abf41430997407a46d2254", size = 278253, upload-time = "2025-09-25T19:50:15.089Z" }, + { url = "https://files.pythonhosted.org/packages/36/c4/ed00ed32f1040f7990dac7115f82273e3c03da1e1a1587a778d8cea496d8/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f0ce778135f60799d89c9693b9b398819d15f1921ba15fe719acb3178215a7db", size = 276084, upload-time = "2025-09-25T19:50:16.699Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c4/fa6e16145e145e87f1fa351bbd54b429354fd72145cd3d4e0c5157cf4c70/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a71f70ee269671460b37a449f5ff26982a6f2ba493b3eabdd687b4bf35f875ac", size = 297185, upload-time = "2025-09-25T19:50:18.525Z" }, + { url = "https://files.pythonhosted.org/packages/24/b4/11f8a31d8b67cca3371e046db49baa7c0594d71eb40ac8121e2fc0888db0/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f8429e1c410b4073944f03bd778a9e066e7fad723564a52ff91841d278dfc822", size = 278656, upload-time = "2025-09-25T19:50:19.809Z" }, + { url = "https://files.pythonhosted.org/packages/ac/31/79f11865f8078e192847d2cb526e3fa27c200933c982c5b2869720fa5fce/bcrypt-5.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:edfcdcedd0d0f05850c52ba3127b1fce70b9f89e0fe5ff16517df7e81fa3cbb8", size = 275662, upload-time = "2025-09-25T19:50:21.567Z" }, + { url = "https://files.pythonhosted.org/packages/d4/8d/5e43d9584b3b3591a6f9b68f755a4da879a59712981ef5ad2a0ac1379f7a/bcrypt-5.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:611f0a17aa4a25a69362dcc299fda5c8a3d4f160e2abb3831041feb77393a14a", size = 278240, upload-time = "2025-09-25T19:50:23.305Z" }, + { url = "https://files.pythonhosted.org/packages/89/48/44590e3fc158620f680a978aafe8f87a4c4320da81ed11552f0323aa9a57/bcrypt-5.0.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:db99dca3b1fdc3db87d7c57eac0c82281242d1eabf19dcb8a6b10eb29a2e72d1", size = 311152, upload-time = "2025-09-25T19:50:24.597Z" }, + { url = "https://files.pythonhosted.org/packages/5f/85/e4fbfc46f14f47b0d20493669a625da5827d07e8a88ee460af6cd9768b44/bcrypt-5.0.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:5feebf85a9cefda32966d8171f5db7e3ba964b77fdfe31919622256f80f9cf42", size = 313284, upload-time = "2025-09-25T19:50:26.268Z" }, + { url = "https://files.pythonhosted.org/packages/25/ae/479f81d3f4594456a01ea2f05b132a519eff9ab5768a70430fa1132384b1/bcrypt-5.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3ca8a166b1140436e058298a34d88032ab62f15aae1c598580333dc21d27ef10", size = 341643, upload-time = "2025-09-25T19:50:28.02Z" }, + { url = "https://files.pythonhosted.org/packages/df/d2/36a086dee1473b14276cd6ea7f61aef3b2648710b5d7f1c9e032c29b859f/bcrypt-5.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:61afc381250c3182d9078551e3ac3a41da14154fbff647ddf52a769f588c4172", size = 359698, upload-time = "2025-09-25T19:50:31.347Z" }, + { url = "https://files.pythonhosted.org/packages/c0/f6/688d2cd64bfd0b14d805ddb8a565e11ca1fb0fd6817175d58b10052b6d88/bcrypt-5.0.0-cp39-abi3-win32.whl", hash = "sha256:64d7ce196203e468c457c37ec22390f1a61c85c6f0b8160fd752940ccfb3a683", size = 153725, upload-time = "2025-09-25T19:50:34.384Z" }, + { url = "https://files.pythonhosted.org/packages/9f/b9/9d9a641194a730bda138b3dfe53f584d61c58cd5230e37566e83ec2ffa0d/bcrypt-5.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:64ee8434b0da054d830fa8e89e1c8bf30061d539044a39524ff7dec90481e5c2", size = 150912, upload-time = "2025-09-25T19:50:35.69Z" }, + { url = "https://files.pythonhosted.org/packages/27/44/d2ef5e87509158ad2187f4dd0852df80695bb1ee0cfe0a684727b01a69e0/bcrypt-5.0.0-cp39-abi3-win_arm64.whl", hash = "sha256:f2347d3534e76bf50bca5500989d6c1d05ed64b440408057a37673282c654927", size = 144953, upload-time = "2025-09-25T19:50:37.32Z" }, +] + +[[package]] +name = "brotli" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/16/c92ca344d646e71a43b8bb353f0a6490d7f6e06210f8554c8f874e454285/brotli-1.2.0.tar.gz", hash = "sha256:e310f77e41941c13340a95976fe66a8a95b01e783d430eeaf7a2f87e0a57dd0a", size = 7388632, upload-time = "2025-11-05T18:39:42.86Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/e1/298c2ddf786bb7347a1cd71d63a347a79e5712a7c0cba9e3c3458ebd976f/brotli-1.2.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:6c12dad5cd04530323e723787ff762bac749a7b256a5bece32b2243dd5c27b21", size = 863080, upload-time = "2025-11-05T18:38:45.503Z" }, + { url = "https://files.pythonhosted.org/packages/84/0c/aac98e286ba66868b2b3b50338ffbd85a35c7122e9531a73a37a29763d38/brotli-1.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3219bd9e69868e57183316ee19c84e03e8f8b5a1d1f2667e1aa8c2f91cb061ac", size = 445453, upload-time = "2025-11-05T18:38:46.433Z" }, + { url = "https://files.pythonhosted.org/packages/ec/f1/0ca1f3f99ae300372635ab3fe2f7a79fa335fee3d874fa7f9e68575e0e62/brotli-1.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:963a08f3bebd8b75ac57661045402da15991468a621f014be54e50f53a58d19e", size = 1528168, upload-time = "2025-11-05T18:38:47.371Z" }, + { url = "https://files.pythonhosted.org/packages/d6/a6/2ebfc8f766d46df8d3e65b880a2e220732395e6d7dc312c1e1244b0f074a/brotli-1.2.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9322b9f8656782414b37e6af884146869d46ab85158201d82bab9abbcb971dc7", size = 1627098, upload-time = "2025-11-05T18:38:48.385Z" }, + { url = "https://files.pythonhosted.org/packages/f3/2f/0976d5b097ff8a22163b10617f76b2557f15f0f39d6a0fe1f02b1a53e92b/brotli-1.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cf9cba6f5b78a2071ec6fb1e7bd39acf35071d90a81231d67e92d637776a6a63", size = 1419861, upload-time = "2025-11-05T18:38:49.372Z" }, + { url = "https://files.pythonhosted.org/packages/9c/97/d76df7176a2ce7616ff94c1fb72d307c9a30d2189fe877f3dd99af00ea5a/brotli-1.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7547369c4392b47d30a3467fe8c3330b4f2e0f7730e45e3103d7d636678a808b", size = 1484594, upload-time = "2025-11-05T18:38:50.655Z" }, + { url = "https://files.pythonhosted.org/packages/d3/93/14cf0b1216f43df5609f5b272050b0abd219e0b54ea80b47cef9867b45e7/brotli-1.2.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:fc1530af5c3c275b8524f2e24841cbe2599d74462455e9bae5109e9ff42e9361", size = 1593455, upload-time = "2025-11-05T18:38:51.624Z" }, + { url = "https://files.pythonhosted.org/packages/b3/73/3183c9e41ca755713bdf2cc1d0810df742c09484e2e1ddd693bee53877c1/brotli-1.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d2d085ded05278d1c7f65560aae97b3160aeb2ea2c0b3e26204856beccb60888", size = 1488164, upload-time = "2025-11-05T18:38:53.079Z" }, + { url = "https://files.pythonhosted.org/packages/64/6a/0c78d8f3a582859236482fd9fa86a65a60328a00983006bcf6d83b7b2253/brotli-1.2.0-cp314-cp314-win32.whl", hash = "sha256:832c115a020e463c2f67664560449a7bea26b0c1fdd690352addad6d0a08714d", size = 339280, upload-time = "2025-11-05T18:38:54.02Z" }, + { url = "https://files.pythonhosted.org/packages/f5/10/56978295c14794b2c12007b07f3e41ba26acda9257457d7085b0bb3bb90c/brotli-1.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:e7c0af964e0b4e3412a0ebf341ea26ec767fa0b4cf81abb5e897c9338b5ad6a3", size = 375639, upload-time = "2025-11-05T18:38:55.67Z" }, +] + +[[package]] +name = "brotlicffi" +version = "1.2.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/b6/017dc5f852ed9b8735af77774509271acbf1de02d238377667145fcee01d/brotlicffi-1.2.0.1.tar.gz", hash = "sha256:c20d5c596278307ad06414a6d95a892377ea274a5c6b790c2548c009385d621c", size = 478156, upload-time = "2026-03-05T19:54:11.547Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/f9/dfa56316837fa798eac19358351e974de8e1e2ca9475af4cb90293cd6576/brotlicffi-1.2.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c85e65913cf2b79c57a3fdd05b98d9731d9255dc0cb696b09376cc091b9cddd", size = 433046, upload-time = "2026-03-05T19:53:46.209Z" }, + { url = "https://files.pythonhosted.org/packages/4a/f5/f8f492158c76b0d940388801f04f747028971ad5774287bded5f1e53f08d/brotlicffi-1.2.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:535f2d05d0273408abc13fc0eebb467afac17b0ad85090c8913690d40207dac5", size = 1541126, upload-time = "2026-03-05T19:53:48.248Z" }, + { url = "https://files.pythonhosted.org/packages/3b/e1/ff87af10ac419600c63e9287a0649c673673ae6b4f2bcf48e96cb2f89f60/brotlicffi-1.2.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce17eb798ca59ecec67a9bb3fd7a4304e120d1cd02953ce522d959b9a84d58ac", size = 1541983, upload-time = "2026-03-05T19:53:50.317Z" }, + { url = "https://files.pythonhosted.org/packages/47/c0/80ecd9bd45776109fab14040e478bf63e456967c9ddee2353d8330ed8de1/brotlicffi-1.2.0.1-cp314-cp314t-win32.whl", hash = "sha256:3c9544f83cb715d95d7eab3af4adbbef8b2093ad6382288a83b3a25feb1a57ec", size = 349047, upload-time = "2026-03-05T19:53:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/ab/98/13e5b250236a281b6cd9e92a01ee1ae231029fa78faee932ef3766e1cb24/brotlicffi-1.2.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:625f8115d32ae9c0740d01ea51518437c3fbaa3e78d41cb18459f6f7ac326000", size = 385652, upload-time = "2026-03-05T19:53:53.892Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9f/b98dcd4af47994cee97aebac866996a006a2e5fc1fd1e2b82a8ad95cf09c/brotlicffi-1.2.0.1-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:91ba5f0ccc040f6ff8f7efaf839f797723d03ed46acb8ae9408f99ffd2572cf4", size = 432608, upload-time = "2026-03-05T19:53:56.736Z" }, + { url = "https://files.pythonhosted.org/packages/b1/7a/ac4ee56595a061e3718a6d1ea7e921f4df156894acffb28ed88a1fd52022/brotlicffi-1.2.0.1-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be9a670c6811af30a4bd42d7116dc5895d3b41beaa8ed8a89050447a0181f5ce", size = 1534257, upload-time = "2026-03-05T19:53:58.667Z" }, + { url = "https://files.pythonhosted.org/packages/99/39/e7410db7f6f56de57744ea52a115084ceb2735f4d44973f349bb92136586/brotlicffi-1.2.0.1-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f3314a3476f59e5443f9f72a6dff16edc0c3463c9b318feaef04ae3e4683f5a", size = 1536838, upload-time = "2026-03-05T19:54:00.705Z" }, + { url = "https://files.pythonhosted.org/packages/a6/75/6e7977d1935fc3fbb201cbd619be8f2c7aea25d40a096967132854b34708/brotlicffi-1.2.0.1-cp38-abi3-win32.whl", hash = "sha256:82ea52e2b5d3145b6c406ebd3efb0d55db718b7ad996bd70c62cec0439de1187", size = 343337, upload-time = "2026-03-05T19:54:02.446Z" }, + { url = "https://files.pythonhosted.org/packages/d8/ef/e7e485ce5e4ba3843a0a92feb767c7b6098fd6e65ce752918074d175ae71/brotlicffi-1.2.0.1-cp38-abi3-win_amd64.whl", hash = "sha256:da2e82a08e7778b8bc539d27ca03cdd684113e81394bfaaad8d0dfc6a17ddede", size = 379026, upload-time = "2026-03-05T19:54:04.322Z" }, +] + +[[package]] +name = "build" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "os_name == 'nt'" }, + { name = "packaging" }, + { name = "pyproject-hooks" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/e0/df5e171f685f82f37b12e1f208064e24244911079d7b767447d1af7e0d70/build-1.5.0.tar.gz", hash = "sha256:302c22c3ba2a0fd5f3911918651341ebb3896176cbdec15bd421f80b1afc7647", size = 89796, upload-time = "2026-04-30T03:18:25.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/fe/6bea5c9162869c5beba5d9c8abbed835ec85bf1ec1fba05a3822325c45f3/build-1.5.0-py3-none-any.whl", hash = "sha256:13f3eecb844759ab66efec90ca17639bbf14dc06cb2fdf37a9010322d9c50a6f", size = 26018, upload-time = "2026-04-30T03:18:23.644Z" }, +] + +[[package]] +name = "certifi" +version = "2026.2.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, + { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, + { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, + { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, + { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, + { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, + { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, + { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, + { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, + { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, +] + +[[package]] +name = "chromadb" +version = "1.5.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bcrypt" }, + { name = "build" }, + { name = "grpcio" }, + { name = "httpx" }, + { name = "importlib-resources" }, + { name = "jsonschema" }, + { name = "kubernetes" }, + { name = "mmh3" }, + { name = "numpy" }, + { name = "onnxruntime" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-grpc" }, + { name = "opentelemetry-sdk" }, + { name = "orjson" }, + { name = "overrides" }, + { name = "pybase64" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "pypika" }, + { name = "pyyaml" }, + { name = "rich" }, + { name = "tenacity" }, + { name = "tokenizers" }, + { name = "tqdm" }, + { name = "typer" }, + { name = "typing-extensions" }, + { name = "uvicorn", extra = ["standard"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/92/d1/5e33b26985f0c7046a0be1cee2158ada1748ee700d2545057fde1468d74d/chromadb-1.5.9.tar.gz", hash = "sha256:5c20e62a455c28bacac927f26116a73fd8e1799e0d908be8e8a4f02197a54731", size = 2595635, upload-time = "2026-05-05T05:54:51.713Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dd/5b/3cced915244f43ed14b53fe9f63a37f05f865064f4e4fe7d9448d3f2a352/chromadb-1.5.9-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:60701011b5e6409647fa40d12c7c5a66b2b0bfcf33a52db2ad53a30a2abc4957", size = 22564540, upload-time = "2026-05-05T05:54:48.906Z" }, + { url = "https://files.pythonhosted.org/packages/34/4c/adcef1f4e82a2ef69ccd3711d55fc289193d54c4c0ff7a0292a3631db46f/chromadb-1.5.9-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:814b9c95617377f6501e5757d63dfddb554a283a7739c87b9fa573850174e6f3", size = 21699698, upload-time = "2026-05-05T05:54:45.078Z" }, + { url = "https://files.pythonhosted.org/packages/38/4e/937bc4d2e6f8ab9664ec79931fbbd69efff47e513ec2924b071e4b0ff774/chromadb-1.5.9-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9192d111bd662241625867962333d99369a00769a50f8b2f58cb388731274d7e", size = 22680924, upload-time = "2026-05-05T05:54:36.25Z" }, + { url = "https://files.pythonhosted.org/packages/e6/ec/0c42039e80b9acc534f67b73b7a42471948042859b3a64867b50a4a77fa3/chromadb-1.5.9-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc09b3df76e5a5cb386aed2715a2eea152e3949f9e1ba93c7119505377749929", size = 23316203, upload-time = "2026-05-05T05:54:41.157Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ce/0f7be6e5d0feafa2cda54b12e6542afeea7dea89d2d411e14da90f8abb96/chromadb-1.5.9-cp39-abi3-win_amd64.whl", hash = "sha256:4fd0b560e56761b7f3cb4d5c6205fd5f20814484b4a3e4e9af9038c2b428fc6c", size = 23542454, upload-time = "2026-05-05T05:54:54.942Z" }, +] + +[[package]] +name = "click" +version = "8.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/75/31212c6bf2503fdf920d87fee5d7a86a2e3bcf444984126f13d8e4016804/click-8.3.2.tar.gz", hash = "sha256:14162b8b3b3550a7d479eafa77dfd3c38d9dc8951f6f69c78913a8f9a7540fd5", size = 302856, upload-time = "2026-04-03T19:14:45.118Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/20/71885d8b97d4f3dde17b1fdb92dbd4908b00541c5a3379787137285f602e/click-8.3.2-py3-none-any.whl", hash = "sha256:1924d2c27c5653561cd2cae4548d1406039cb79b858b747cfea24924bbc1616d", size = 108379, upload-time = "2026-04-03T19:14:43.505Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "cryptography" +version = "49.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/99/d1c90d6041656cc6ee229dc99cd67fd0cd5aec3c5f7d72fffc27cc750054/cryptography-49.0.0.tar.gz", hash = "sha256:f89660a348f4f78a92366240a61404e337586ef7f5909a2fef59ca88ef505493", size = 854345, upload-time = "2026-06-12T20:02:30.512Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/22/adf66990e63584a68dfb50c24f48a125c07b1699899381c8151e63ed458c/cryptography-49.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:966fe0e9c67490071f14c0d2b1cb2dfb3023c5ce39457343931415f08382f2db", size = 4032100, upload-time = "2026-06-12T20:02:32.143Z" }, + { url = "https://files.pythonhosted.org/packages/09/41/3797cfaf69cae04a13ee78ebd83f0678d9c02b4779d21ce24445326f1a69/cryptography-49.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:36d1709f992593689b45bda411498d62c6e365f2ca00b84657d4dadd24de16db", size = 4692978, upload-time = "2026-06-12T20:01:21.305Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8b/43011f7ebe515a8aa20d61f290a326cd890c2e738e16e59eaff8d9c3a412/cryptography-49.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0e959b578856a3924bc0cbb710fc12c387b9412a951389f3ca61704a9e25f325", size = 4716422, upload-time = "2026-06-12T20:01:48.566Z" }, + { url = "https://files.pythonhosted.org/packages/4a/91/01ce7303a4579e6d3a6abef01bd322848e9ea7a219adcabc5048b9033571/cryptography-49.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:53ecee2e23f7169b6117e99fc8a944e5e50f79e69758a83b52a00cb98ab2b2d2", size = 4700503, upload-time = "2026-06-12T20:02:47.091Z" }, + { url = "https://files.pythonhosted.org/packages/62/99/a2c95cf8293f07491e9e27c20cc4dcd18176d944e674679adeb1d0173fd6/cryptography-49.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2eda353d8a27bcbcaa4cbed18994a74ab4d19a2ca897db188ea269ab9b71419b", size = 5309779, upload-time = "2026-06-12T20:02:08.987Z" }, + { url = "https://files.pythonhosted.org/packages/20/2c/0622f20ff02b2ef32558733443805dc82fd4c275be01b2d19d14676f3a1b/cryptography-49.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2afe9051da7ae7bd5905da5a949280c7d2bb75682e188f650a9d0f2756b834c6", size = 4749683, upload-time = "2026-06-12T20:02:03.335Z" }, + { url = "https://files.pythonhosted.org/packages/a3/5b/c5246635d5fd3b64e0d45ae10e99fd32fe9676a79915ccfe5a61ba9af1a5/cryptography-49.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:0b82e28ee398a386f0807bba7884d30f25218855690f45115831bcce5d90822c", size = 4337874, upload-time = "2026-06-12T20:02:54.323Z" }, + { url = "https://files.pythonhosted.org/packages/6d/88/05563c7fe2e914e87d1a536d06fe83e66b4e1d95cb593e05aea375531da8/cryptography-49.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ccac2bfebc306b862133e3bb71f3f6ee8bb525240089b2d952e4144b3a6d5da7", size = 4700283, upload-time = "2026-06-12T20:01:34.822Z" }, + { url = "https://files.pythonhosted.org/packages/c4/b6/d7696e4e890d6ae1469935164c9e5215c557671cb78d6e3f458ccceaa632/cryptography-49.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:d0527ce944105f257f605a827d6ebead966c752038b6e8656abb9c5edee6fc68", size = 5265844, upload-time = "2026-06-12T20:01:24.09Z" }, + { url = "https://files.pythonhosted.org/packages/a9/3c/f3ad17eecc1a57b0ba236dc01f90e783c51f4a2f35f64777cc4f47a184b2/cryptography-49.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:cbc77da8c523d5abd028635ba850a6966fcee2c82e2bf65a41d1d8afe0f98be9", size = 4749290, upload-time = "2026-06-12T20:01:30.848Z" }, + { url = "https://files.pythonhosted.org/packages/4f/01/339573cf1023163a400b0b5d16f6d507de413b9f60be6fd1b77feeaf6737/cryptography-49.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b87e65d263b3e5d3bb92a57e2a6638e2f31110fa7aa890c7b2dbba42248d0a3f", size = 4834612, upload-time = "2026-06-12T20:01:29.246Z" }, + { url = "https://files.pythonhosted.org/packages/71/fd/577302e213a1be9468f92d1afef66fcf1ef83d516819d9992ca547f592bd/cryptography-49.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:66ec79c3904820572d7e987abdf304281f141d37ad9a489b8e97066e7b9b6459", size = 4980804, upload-time = "2026-06-12T20:01:42.853Z" }, + { url = "https://files.pythonhosted.org/packages/1f/09/f42b1d190c5ba75f72062a387f8030d1d75f6ab035788f1d9c4b01de6525/cryptography-49.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:e5dfc1e64de5677cec922ffa8da89c546d0415bf6efdf081842e5d44c84e1f0e", size = 3810026, upload-time = "2026-06-12T20:02:39.262Z" }, + { url = "https://files.pythonhosted.org/packages/ec/9e/db72b3ae7fc9cfad53e630e56c6ae83b9b6ff0bf3718ffb8012d20b3aabf/cryptography-49.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:73a205dce83953d131a4aa1e0fd917a2fd1c5b1eef251e9d7152efefcbf5caf7", size = 4013892, upload-time = "2026-06-12T20:02:10.735Z" }, + { url = "https://files.pythonhosted.org/packages/86/12/c48a424f38db03027be9f7ed5c7dc5de9933dbee992865f98b13727a009d/cryptography-49.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:196ecd6a36e4e9aa10270393bb98d8df88fccee0bf1e5128b91ae4eb4375896d", size = 4678835, upload-time = "2026-06-12T20:02:48.743Z" }, + { url = "https://files.pythonhosted.org/packages/68/28/8a3ad4653662c93fc44dc4e5d8fd374c25c42e07b34bbfbadf49cf57a5a8/cryptography-49.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7abcee80084cda3f7691f3eb1ce480d8df49cec637b429aa35986c1de71738aa", size = 4697239, upload-time = "2026-06-12T20:02:56.03Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b2/2193fc74f81aee4f9b62733133b73b5176718932ed8f2e4b03fa040480a6/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:4ae387c9cb68ea569ca17e490d66d8142b81c3cc814bf179974b7d146e490bbb", size = 4685593, upload-time = "2026-06-12T20:02:50.666Z" }, + { url = "https://files.pythonhosted.org/packages/47/f1/1d3eaa243bfc5de4a187b22aa8c048b3e4980bfbe830ac46e6bac2e66947/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:f37d847238971164fdbc68ade6f6574aecc9c0af714190e2083429ff68f4ce9d", size = 5289961, upload-time = "2026-06-12T20:01:46.468Z" }, + { url = "https://files.pythonhosted.org/packages/58/39/2d51306721330c486495853eda1c567880ff036de15a14c4b74f399934af/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:c2bc30226390d60ea19d9f82b19db005fe0452154a23c1c410c12ea801e43561", size = 4731145, upload-time = "2026-06-12T20:02:16.832Z" }, + { url = "https://files.pythonhosted.org/packages/17/50/983e838c7fd0d87fd8c969bcdd328edaf5f756e38df5281637424c155873/cryptography-49.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:07cab27cc7b7e0fd28e5e26bb9eeedde5c135c868b46de4a27845abe94af6122", size = 4321719, upload-time = "2026-06-12T20:02:52.611Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f5/8f571d7e27c55bce9f76f026143bcb1e040a4233149ecca0bea5fa5dd5f7/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:b20133d204d2bb56ba047642199603876c872026ca53e79c35b83772ab2cc505", size = 4685209, upload-time = "2026-06-12T20:02:07.282Z" }, + { url = "https://files.pythonhosted.org/packages/e7/84/0e27016a6fc5a0886f797018b26aa42f40c09a82332bff77822a451deaaa/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b970c6da94d5bb18629db453d14f2a1300f6bf59b61e9b82377931ef95504866", size = 5246285, upload-time = "2026-06-12T20:01:32.439Z" }, + { url = "https://files.pythonhosted.org/packages/11/2d/5e1fb307cb5931881516b464c98774b3f2c36b5d4bb9a2830253cf553cad/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d8ecde755e2e91bf773fc94e8c9d730cd7f2007004cb492263a794ec3899a1c8", size = 4730441, upload-time = "2026-06-12T20:02:01.469Z" }, + { url = "https://files.pythonhosted.org/packages/e4/c0/bff5a02ee731d207d6a1ed51732549d8c53d2bc8da1d10ec6f2844201d68/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e3fb64c420688e5319ae25113a354015abbd8dffbfbc41781a1ea66fc7622ac3", size = 4815869, upload-time = "2026-06-12T20:01:36.574Z" }, + { url = "https://files.pythonhosted.org/packages/b9/26/814681d14248d95d73d5c3eea0c39a94eb8302df966f670a2c60de90974b/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32703d93296f5c1f4b53349ad3a250c2cae0fdecd3a3dd5d47e616d8d616af27", size = 4960948, upload-time = "2026-06-12T20:02:18.688Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fe/93ecac273d3738939d023612ad12cca9a3740a5345d69fda04134c43fd96/cryptography-49.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:33cd0565932807baddb67b96dbee92f2c374b5c89dee09fd74079aeb8c8dba61", size = 3799153, upload-time = "2026-06-12T20:01:39.059Z" }, + { url = "https://files.pythonhosted.org/packages/19/2a/5bb823f5bedcf80718cea7fbc95ec5515cca3769633c4b01a32be7f30e7c/cryptography-49.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ec5e529fb80935c94fe7b729f9972b50e351a0e6b50aa294fd5cabb109fcc29a", size = 4025947, upload-time = "2026-06-12T20:01:25.745Z" }, + { url = "https://files.pythonhosted.org/packages/3d/df/40577043ca124e17012f408ddddaeb213b856336ac82ddb3bc915f39e29f/cryptography-49.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f78ff2c9ed8dc2d036b0f4d640e22522213d047c1b14e61205a7e55c80a494d4", size = 4692429, upload-time = "2026-06-12T20:01:53.628Z" }, + { url = "https://files.pythonhosted.org/packages/2c/99/2d13299eb3dd27b02dcfaafcc91d6b5cb3329f7cbd6d8f51921acd566c1a/cryptography-49.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:35b151772baff2c74cba7fa290ceaff4c3b11c0c881eb93eb5dbc05a7cfbba18", size = 4700968, upload-time = "2026-06-12T20:02:45.383Z" }, + { url = "https://files.pythonhosted.org/packages/a5/4d/9c0cd02f95e2602dd5e563da149ee0830abef3537be8b34dc56281ebe27a/cryptography-49.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0f21641cf4b30fca7aee061ced0ec7ad7b073518088b7c9969a297c0ae796c69", size = 4697758, upload-time = "2026-06-12T20:01:41.13Z" }, + { url = "https://files.pythonhosted.org/packages/24/01/186c825898477d77e2324d5360fefe622ff1d8d1963ec0554e2cada8ec77/cryptography-49.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9e82dcc8e56052715fb18b2429e3bca4823b1629136a2084fc45a9a5cecb9b64", size = 5298863, upload-time = "2026-06-12T20:02:24.579Z" }, + { url = "https://files.pythonhosted.org/packages/b8/7b/62cbbab75d0659865bf0273790031544a0b16c8072d258f9428dcd8190dc/cryptography-49.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6f2debedf9ca60cf1d5bd466475638af5130f89965605cd818484d19987d3a21", size = 4735983, upload-time = "2026-06-12T20:01:50.14Z" }, + { url = "https://files.pythonhosted.org/packages/6c/72/3e798c064bc39e471008075d0f9bc9daf77a80879c092e4a8e170c585ed4/cryptography-49.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:8c25ceb16df5b9435f3f6a9829204985b0e0cbee3b48aacd432c7d2c850b44d9", size = 4334173, upload-time = "2026-06-12T20:01:44.743Z" }, + { url = "https://files.pythonhosted.org/packages/f0/ee/6fca21d1ac73e06f8bef71940abfd4d2f6472b4bca284d770f32bd4086f6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:28d8b15e6275f12c8a207dc309dfa957903c927d08d0cc937ee3f63f200693cc", size = 4697298, upload-time = "2026-06-12T20:02:20.918Z" }, + { url = "https://files.pythonhosted.org/packages/67/d0/a5fcd3515f0bae49a7b6d0413cc1bdccdcc1fc0047037a0d480642cdc5d6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6fc361c34fb6aac015ce19435876635e5c6d21db31998b0920f675f131e043b8", size = 5254338, upload-time = "2026-06-12T20:02:22.737Z" }, + { url = "https://files.pythonhosted.org/packages/a0/84/84fe36f19caf857d61cb7fc9c63035a47ffabd84ea12d1d393148efa3615/cryptography-49.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2400ef9c9e2299a25614eb1dea3db54a69b1349efd043bfac9c67630d136df36", size = 4735650, upload-time = "2026-06-12T20:02:41.389Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a0/db537264e234f7273a73ec020873d6d6b39dfd8a53db78b550ca8320440e/cryptography-49.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:67e1d20ad9ef3a563c59ef22e7a8a0b8210bd26604369ea4a30a7c66aefe504e", size = 4834820, upload-time = "2026-06-12T20:01:51.847Z" }, + { url = "https://files.pythonhosted.org/packages/93/77/8df9eb486495979bccecd1062e2eaf435250e84437040295b57d09048b0b/cryptography-49.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:42b0684e0e40cf26122427802486f6d93aea593612603a94fbf260c7eb1e9c1b", size = 4967968, upload-time = "2026-06-12T20:02:12.524Z" }, + { url = "https://files.pythonhosted.org/packages/c2/e6/f60198ea8d9dfa15fff9ed4ca02ce362f6eadd9ba757dcc50634c4257b63/cryptography-49.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001", size = 3785547, upload-time = "2026-06-12T20:02:26.847Z" }, +] + +[[package]] +name = "cuda-bindings" +version = "13.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-pathfinder", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/87/87a014f045b77c6de5c8527b0757fe644417b184e5367db977236a141602/cuda_bindings-13.2.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6464b30f46692d6c7f65d4a0e0450d81dd29de3afc1bb515653973d01c2cd6e", size = 5685673, upload-time = "2026-03-11T00:12:56.371Z" }, + { url = "https://files.pythonhosted.org/packages/ee/5e/c0fe77a73aaefd3fff25ffaccaac69c5a63eafdf8b9a4c476626ef0ac703/cuda_bindings-13.2.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4af9f3e1be603fa12d5ad6cfca7844c9d230befa9792b5abdf7dd79979c3626", size = 6191386, upload-time = "2026-03-11T00:12:58.965Z" }, + { url = "https://files.pythonhosted.org/packages/5f/58/ed2c3b39c8dd5f96aa7a4abef0d47a73932c7a988e30f5fa428f00ed0da1/cuda_bindings-13.2.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df850a1ff8ce1b3385257b08e47b70e959932f5f432d0a4e46a355962b4e4771", size = 5507469, upload-time = "2026-03-11T00:13:04.063Z" }, + { url = "https://files.pythonhosted.org/packages/1f/01/0c941b112ceeb21439b05895eace78ca1aa2eaaf695c8521a068fd9b4c00/cuda_bindings-13.2.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8a16384c6494e5485f39314b0b4afb04bee48d49edb16d5d8593fd35bbd231b", size = 6059693, upload-time = "2026-03-11T00:13:06.003Z" }, +] + +[[package]] +name = "cuda-pathfinder" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/74/8c66861b873d8eed51fde56d3091baa4906a56f0d4390cae991f2d41dda5/cuda_pathfinder-1.5.1-py3-none-any.whl", hash = "sha256:b3718097fb57cf9e8a904dd072d806f2c9a27627e35c020b06ab9454bcec08c0", size = 49861, upload-time = "2026-04-03T16:41:22.203Z" }, +] + +[[package]] +name = "cuda-toolkit" +version = "13.0.2" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/b2/453099f5f3b698d7d0eab38916aac44c7f76229f451709e2eb9db6615dcd/cuda_toolkit-13.0.2-py2.py3-none-any.whl", hash = "sha256:b198824cf2f54003f50d64ada3a0f184b42ca0846c1c94192fa269ecd97a66eb", size = 2364, upload-time = "2025-12-19T23:24:07.328Z" }, +] + +[package.optional-dependencies] +cublas = [ + { name = "nvidia-cublas", marker = "sys_platform == 'linux'" }, +] +cudart = [ + { name = "nvidia-cuda-runtime", marker = "sys_platform == 'linux'" }, +] +cufft = [ + { name = "nvidia-cufft", marker = "sys_platform == 'linux'" }, +] +cufile = [ + { name = "nvidia-cufile", marker = "sys_platform == 'linux'" }, +] +cupti = [ + { name = "nvidia-cuda-cupti", marker = "sys_platform == 'linux'" }, +] +curand = [ + { name = "nvidia-curand", marker = "sys_platform == 'linux'" }, +] +cusolver = [ + { name = "nvidia-cusolver", marker = "sys_platform == 'linux'" }, +] +cusparse = [ + { name = "nvidia-cusparse", marker = "sys_platform == 'linux'" }, +] +nvjitlink = [ + { name = "nvidia-nvjitlink", marker = "sys_platform == 'linux'" }, +] +nvrtc = [ + { name = "nvidia-cuda-nvrtc", marker = "sys_platform == 'linux'" }, +] +nvtx = [ + { name = "nvidia-nvtx", marker = "sys_platform == 'linux'" }, +] + +[[package]] +name = "dataclasses-json" +version = "0.6.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "marshmallow" }, + { name = "typing-inspect" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/64/a4/f71d9cf3a5ac257c993b5ca3f93df5f7fb395c725e7f1e6479d2514173c3/dataclasses_json-0.6.7.tar.gz", hash = "sha256:b6b3e528266ea45b9535223bc53ca645f5208833c29229e847b3f26a1cc55fc0", size = 32227, upload-time = "2024-06-09T16:20:19.103Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/be/d0d44e092656fe7a06b55e6103cbce807cdbdee17884a5367c68c9860853/dataclasses_json-0.6.7-py3-none-any.whl", hash = "sha256:0dbf33f26c8d5305befd61b39d2b3414e8a407bedc2834dea9b8d642666fb40a", size = 28686, upload-time = "2024-06-09T16:20:16.715Z" }, +] + +[[package]] +name = "ddgs" +version = "9.14.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "fake-useragent" }, + { name = "httpx", extra = ["brotli", "http2", "socks"] }, + { name = "lxml" }, + { name = "primp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/77/24/9d29eeb7dd4852c27c3673adcaf30c4dc55ced76b303c1fbb792ce7cae52/ddgs-9.14.4.tar.gz", hash = "sha256:f7b118a2b709a9e9c04a1dca6e96b98c25d4dfaca1a4b0a244d74454fcca48ef", size = 59742, upload-time = "2026-05-15T06:53:45.946Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/5f/32de4d99220eb559b7b1cd1c529a1856efa8097f7a3e10b6c207aa95e36c/ddgs-9.14.4-py3-none-any.whl", hash = "sha256:acb084c34bf1110c974caf7e5e5a2c1973beb4bd9e170bfd191fe5ed2d2b2d6c", size = 70638, upload-time = "2026-05-15T06:53:44.761Z" }, +] + +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, +] + +[[package]] +name = "dnspython" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, +] + +[[package]] +name = "durationpy" +version = "0.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/a4/e44218c2b394e31a6dd0d6b095c4e1f32d0be54c2a4b250032d717647bab/durationpy-0.10.tar.gz", hash = "sha256:1fa6893409a6e739c9c72334fc65cca1f355dbdd93405d30f726deb5bde42fba", size = 3335, upload-time = "2025-05-17T13:52:37.26Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/0d/9feae160378a3553fa9a339b0e9c1a048e147a4127210e286ef18b730f03/durationpy-0.10-py3-none-any.whl", hash = "sha256:3b41e1b601234296b4fb368338fdcd3e13e0b4fb5b67345948f4f2bf9868b286", size = 3922, upload-time = "2025-05-17T13:52:36.463Z" }, +] + +[[package]] +name = "email-validator" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426", size = 51238, upload-time = "2025-08-26T13:09:06.831Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" }, +] + +[[package]] +name = "fake-useragent" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/43/948d10bf42735709edb5ae51e23297d034086f17fc7279fef385a7acb473/fake_useragent-2.2.0.tar.gz", hash = "sha256:4e6ab6571e40cc086d788523cf9e018f618d07f9050f822ff409a4dfe17c16b2", size = 158898, upload-time = "2025-04-14T15:32:19.238Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/37/b3ea9cd5558ff4cb51957caca2193981c6b0ff30bd0d2630ac62505d99d0/fake_useragent-2.2.0-py3-none-any.whl", hash = "sha256:67f35ca4d847b0d298187443aaf020413746e56acd985a611908c73dba2daa24", size = 161695, upload-time = "2025-04-14T15:32:17.732Z" }, +] + +[[package]] +name = "fastapi" +version = "0.135.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f7/e6/7adb4c5fa231e82c35b8f5741a9f2d055f520c29af5546fd70d3e8e1cd2e/fastapi-0.135.3.tar.gz", hash = "sha256:bd6d7caf1a2bdd8d676843cdcd2287729572a1ef524fc4d65c17ae002a1be654", size = 396524, upload-time = "2026-04-01T16:23:58.188Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/a4/5caa2de7f917a04ada20018eccf60d6cc6145b0199d55ca3711b0fc08312/fastapi-0.135.3-py3-none-any.whl", hash = "sha256:9b0f590c813acd13d0ab43dd8494138eb58e484bfac405db1f3187cfc5810d98", size = 117734, upload-time = "2026-04-01T16:23:59.328Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "email-validator" }, + { name = "fastapi-cli", extra = ["standard"] }, + { name = "httpx" }, + { name = "jinja2" }, + { name = "pydantic-extra-types" }, + { name = "pydantic-settings" }, + { name = "python-multipart" }, + { name = "uvicorn", extra = ["standard"] }, +] + +[[package]] +name = "fastapi-cli" +version = "0.0.24" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "rich-toolkit" }, + { name = "typer" }, + { name = "uvicorn", extra = ["standard"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6e/58/74797ae9e4610cfa0c6b34c8309096d3b20bb29be3b8b5fbf1004d10fa5f/fastapi_cli-0.0.24.tar.gz", hash = "sha256:1afc9c9e21d7ebc8a3ca5e31790cd8d837742be7e4f8b9236e99cb3451f0de00", size = 19043, upload-time = "2026-02-24T10:45:10.476Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/4b/68f9fe268e535d79c76910519530026a4f994ce07189ac0dded45c6af825/fastapi_cli-0.0.24-py3-none-any.whl", hash = "sha256:4a1f78ed798f106b4fee85ca93b85d8fe33c0a3570f775964d37edb80b8f0edc", size = 12304, upload-time = "2026-02-24T10:45:09.552Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "fastapi-cloud-cli" }, + { name = "uvicorn", extra = ["standard"] }, +] + +[[package]] +name = "fastapi-cloud-cli" +version = "0.15.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastar" }, + { name = "httpx" }, + { name = "pydantic", extra = ["email"] }, + { name = "rich-toolkit" }, + { name = "rignore" }, + { name = "sentry-sdk" }, + { name = "typer" }, + { name = "uvicorn", extra = ["standard"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7f/f2/fcd66ce245b7e3c3d84ca8717eda8896945fbc17c87a9b03f490ff06ace7/fastapi_cloud_cli-0.15.1.tar.gz", hash = "sha256:71a46f8a1d9fea295544113d6b79f620dc5768b24012887887306d151165745d", size = 43851, upload-time = "2026-03-26T10:23:12.932Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/11/ecb0d5e1d114e8aaec1cdc8ee2d7b0f54292585067effe2756bde7e7a4b0/fastapi_cloud_cli-0.15.1-py3-none-any.whl", hash = "sha256:b1e8b3b26dc314e180fc0ab67dfd39d7d9fe160d3951081d09184eafaacf5649", size = 32284, upload-time = "2026-03-26T10:23:14.151Z" }, +] + +[[package]] +name = "fastar" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dd/00/dab9ca274cf1fde19223fea7104631bea254751026e75bf99f2b6d0d1568/fastar-0.9.0.tar.gz", hash = "sha256:d49114d5f0b76c5cc242875d90fa4706de45e0456ddedf416608ecd0787fb410", size = 70124, upload-time = "2026-03-20T14:26:34.503Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/00/99700dd33273c118d7d9ab7ad5db6650b430448d4cfae62aec6ef6ca4cb7/fastar-0.9.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:ccb2289f24ee6555330eb77149486d3a2ec8926450a96157dd20c636a0eec085", size = 707059, upload-time = "2026-03-20T14:25:35.086Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a4/4808dcfa8dddb9d7f50d830a39a9084d9d148ed06fcac8b040620848bc24/fastar-0.9.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2bfee749a46666785151b33980aef8f916e6e0341c3d241bde4d3de6be23f00c", size = 627135, upload-time = "2026-03-20T14:25:23.134Z" }, + { url = "https://files.pythonhosted.org/packages/da/cb/9c92e97d760d769846cae6ce53332a5f2a9246eb07b369ac2a4ebf10480c/fastar-0.9.0-cp314-cp314-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f6096ec3f216a21fa9ac430ce509447f56c5bd979170c4c0c3b4f3cb2051c1a8", size = 864974, upload-time = "2026-03-20T14:24:58.624Z" }, + { url = "https://files.pythonhosted.org/packages/84/38/9dadebd0b7408b4f415827db35169bbd0741e726e38e3afd3e491b589c61/fastar-0.9.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7a806e54d429f7f57e35dc709e801da8c0ba9095deb7331d6574c05ae4537ea", size = 760262, upload-time = "2026-03-20T14:23:53.275Z" }, + { url = "https://files.pythonhosted.org/packages/d6/7d/7afc5721429515aa0873b268513f656f905d27ff1ca54d875af6be9e9bc6/fastar-0.9.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f9a06abf8c7f74643a75003334683eb6e94fabef05f60449b7841eeb093a47b0", size = 757575, upload-time = "2026-03-20T14:24:06.143Z" }, + { url = "https://files.pythonhosted.org/packages/fc/5d/7498842c62bd6057553aa598cd175a0db41fdfeda7bdfde48dab63ffb285/fastar-0.9.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1e9b5c155946f20ce3f999fb1362ed102876156ad6539e1b73a921f14efb758c", size = 924827, upload-time = "2026-03-20T14:24:19.364Z" }, + { url = "https://files.pythonhosted.org/packages/69/ab/13322e98fe1a00ed6efbfa5bf06fcfff8a6979804ef7fcef884b5e0c6f85/fastar-0.9.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbdedac6a84ef9ebc1cee6d777599ad51c9e98ceb8ebb386159483dcd60d0e16", size = 816536, upload-time = "2026-03-20T14:24:44.844Z" }, + { url = "https://files.pythonhosted.org/packages/fe/fd/0aa5b9994c8dba75b73a9527be4178423cb926db9f7eca562559e27ccdfd/fastar-0.9.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51df60a2f7af09f75b2a4438b25cb903d8774e24c492acf2bca8b0863026f34c", size = 818686, upload-time = "2026-03-20T14:25:10.799Z" }, + { url = "https://files.pythonhosted.org/packages/46/d6/e000cd49ef85c11a8350e461e6c48a4345ace94fb52242ac8c1d5dad1dfc/fastar-0.9.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:15016d0da7dbc664f09145fc7db549ba8fe32628c6e44e20926655b82de10658", size = 885043, upload-time = "2026-03-20T14:24:32.231Z" }, + { url = "https://files.pythonhosted.org/packages/68/28/ee734fe273475b9b25554370d92a21fc809376cf79aa072de29d23c17518/fastar-0.9.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c66a8e1f7dae6357be8c1f83ce6330febbc08e49fc40a5a2e91061e7867bbcbf", size = 967965, upload-time = "2026-03-20T14:25:48.397Z" }, + { url = "https://files.pythonhosted.org/packages/c1/35/165b3a75f1ee8045af9478c8aae5b5e20913cca2d4a5adb1be445e8d015a/fastar-0.9.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:1c6829be3f55d2978cb62921ef4d7c3dd58fe68ee994f81d49bd0a3c5240c977", size = 1034507, upload-time = "2026-03-20T14:26:01.518Z" }, + { url = "https://files.pythonhosted.org/packages/ba/4e/4097b5015da02484468c16543db2f8dec2fe827d321a798acbd9068e0f13/fastar-0.9.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:68db849e01d49543f31d56ef2fe15527afe2b9e0fb21794edc4d772553d83407", size = 1073388, upload-time = "2026-03-20T14:26:14.448Z" }, + { url = "https://files.pythonhosted.org/packages/07/d7/3b86af4e63a551398763a1bbbbac91e1c0754ece7ac7157218b33a065f4c/fastar-0.9.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5569510407c0ded580cfeec99e46ebe85ce27e199e020c5c1ea6f570e302c946", size = 1025190, upload-time = "2026-03-20T14:26:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/39/07/8c50a60f03e095053306fcf57d9d99343bce0e99d5b758bf96de31aec849/fastar-0.9.0-cp314-cp314-win32.whl", hash = "sha256:3f7be0a34ffbead52ab5f4a1e445e488bf39736acb006298d3b3c5b4f2c5915e", size = 452301, upload-time = "2026-03-20T14:26:59.234Z" }, + { url = "https://files.pythonhosted.org/packages/ee/69/aa6d67b09485ba031408296d6ff844c7d83cdcb9f8fcc240422c6f83be87/fastar-0.9.0-cp314-cp314-win_amd64.whl", hash = "sha256:cf7f68b98ed34ce628994c9bbd4f56cf6b4b175b3f7b8cbe35c884c8efec0a5b", size = 484948, upload-time = "2026-03-20T14:26:48.45Z" }, + { url = "https://files.pythonhosted.org/packages/20/6d/dba29d87ca929f95a5a7025c7d30720ad8478beed29fff482f29e1e8b045/fastar-0.9.0-cp314-cp314-win_arm64.whl", hash = "sha256:155dae97aca4b245eabb25e23fd16bfd42a0447f9db7f7789ab1299b02d94487", size = 461170, upload-time = "2026-03-20T14:26:39.191Z" }, + { url = "https://files.pythonhosted.org/packages/96/8f/c3ea0adac50a8037987ee7f15ff94767ebb604faf6008cbd2b8efa46c372/fastar-0.9.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:a63df018232623e136178953031057c7ac0dbf0acc6f0e8c1dc7dbc19e64c22f", size = 705857, upload-time = "2026-03-20T14:25:36.842Z" }, + { url = "https://files.pythonhosted.org/packages/ae/b3/e0e1aad1778065559680a73cdf982ed07b04300c2e5bf778dec8668eda6f/fastar-0.9.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6fb44f8675ef87087cb08f9bf4dfa15e818571a5f567ff692f3ea007cff867b5", size = 626210, upload-time = "2026-03-20T14:25:24.361Z" }, + { url = "https://files.pythonhosted.org/packages/94/f3/3c117335cbea26b3bc05382c27e6028278ed048d610b8de427c68f2fec84/fastar-0.9.0-cp314-cp314t-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:81092daa991d0f095424e0e28ed589e03c81a21eeddc9b981184ddda5869bf9d", size = 864879, upload-time = "2026-03-20T14:25:00.131Z" }, + { url = "https://files.pythonhosted.org/packages/26/5d/e8d00ec3b2692d14ea111ddae25bf10e0cb60d5d79915c3d8ea393a87d5c/fastar-0.9.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e8793e2618d0d6d5a7762d6007371f57f02544364864e40e6b9d304b0f151b2", size = 759117, upload-time = "2026-03-20T14:23:54.826Z" }, + { url = "https://files.pythonhosted.org/packages/1a/61/6e080fdbc28c72dded8b6ff396035d6dc292f9b1c67b8797ac2372ca5733/fastar-0.9.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:83f7ef7056791fc95b6afa987238368c9a73ad0edcedc6bc80076f9fbd3a2a78", size = 756527, upload-time = "2026-03-20T14:24:07.494Z" }, + { url = "https://files.pythonhosted.org/packages/e8/97/2cf1a07884d171c028bd4ae5ecf7ded6f31581f79ab26711dcdad0a3d5ab/fastar-0.9.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b3a456230fcc0e560823f5d04ae8e4c867300d8ee710b14ddcdd1b316ac3dd8d", size = 921763, upload-time = "2026-03-20T14:24:20.787Z" }, + { url = "https://files.pythonhosted.org/packages/f6/e3/c1d698a45f9f5dc892ed7d64badc9c38f1e5c1667048191969c438d2b428/fastar-0.9.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a60b117ebadc46c10c87852d2158a4d6489adbfbbec37be036b4cfbeca07b449", size = 815493, upload-time = "2026-03-20T14:24:46.482Z" }, + { url = "https://files.pythonhosted.org/packages/25/38/e124a404043fba75a8cb2f755ca49e4f01e18400bb6607a5f76526e07164/fastar-0.9.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a6199b4ca0c092a7ae47f5f387492d46a0a2d82cb3b7aa0bf50d7f7d5d8d57f", size = 819166, upload-time = "2026-03-20T14:25:12.027Z" }, + { url = "https://files.pythonhosted.org/packages/85/4a/5b1ea5c8d0dbdfcec2fd1e6a243d6bb5a1c7cd55e132cc532eb8b1cbd6d9/fastar-0.9.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:34efe114caf10b4d5ea404069ff1f6cc0e55a708c7091059b0fc087f65c0a331", size = 883618, upload-time = "2026-03-20T14:24:33.552Z" }, + { url = "https://files.pythonhosted.org/packages/d3/0b/ae46e5722a67a3c2e0ff83d539b0907d6e5092f6395840c0eb6ede81c5d6/fastar-0.9.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4d44c1f8d9c5a3e4e58e6ffb77f4ca023ba9d9ddd88e7c613b3419a8feaa3db7", size = 966294, upload-time = "2026-03-20T14:25:50.024Z" }, + { url = "https://files.pythonhosted.org/packages/98/58/b161cf8711f4a50a3e57b6f89bc703c1aed282cad50434b3bc8524738b20/fastar-0.9.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:d2af970a1f773965b05f1765017a417380ad080ea49590516eb25b23c039158a", size = 1033177, upload-time = "2026-03-20T14:26:02.868Z" }, + { url = "https://files.pythonhosted.org/packages/e2/76/faac7292bce9b30106a6b6a9f5ddb658fdb03abe2644688b82023c8f76b9/fastar-0.9.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:1675346d7cbdde0d21869c3b597be19b5e31a36442bdf3a48d83a49765b269dc", size = 1073620, upload-time = "2026-03-20T14:26:16.121Z" }, + { url = "https://files.pythonhosted.org/packages/b8/be/dd55ffcc302d6f0ff4aba1616a0da3edc8fcefb757869cad81de74604a35/fastar-0.9.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dc440daa28591aeb4d387c171e824f179ad2ab256ce7a315472395b8d5f80392", size = 1025147, upload-time = "2026-03-20T14:26:28.767Z" }, + { url = "https://files.pythonhosted.org/packages/4b/c7/080bbb2b3c4e739fe6486fd765a09905f6c16c1068b2fcf2bb51a5e83937/fastar-0.9.0-cp314-cp314t-win32.whl", hash = "sha256:32787880600a988d11547628034993ef948499ae4514a30509817242c4eb98b1", size = 452317, upload-time = "2026-03-20T14:27:03.243Z" }, + { url = "https://files.pythonhosted.org/packages/42/39/00553739a7e9e35f78a0c5911d181acf6b6e132337adc9bbc3575f5f6f04/fastar-0.9.0-cp314-cp314t-win_amd64.whl", hash = "sha256:92fa18ec4958f33473259980685d29248ac44c96eed34026ad7550f93dd9ee23", size = 483994, upload-time = "2026-03-20T14:26:52.76Z" }, + { url = "https://files.pythonhosted.org/packages/4f/36/a7af08d233624515d9a0f5d41b7a01a51fd825b8c795e41800215a3200e7/fastar-0.9.0-cp314-cp314t-win_arm64.whl", hash = "sha256:34f646ac4f5bed3661a106ca56c1744e7146a02aacf517d47b24fd3f25dc1ff6", size = 460604, upload-time = "2026-03-20T14:26:40.771Z" }, +] + +[[package]] +name = "filelock" +version = "3.25.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/b8/00651a0f559862f3bb7d6f7477b192afe3f583cc5e26403b44e59a55ab34/filelock-3.25.2.tar.gz", hash = "sha256:b64ece2b38f4ca29dd3e810287aa8c48182bbecd1ae6e9ae126c9b35f1382694", size = 40480, upload-time = "2026-03-11T20:45:38.487Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/a5/842ae8f0c08b61d6484b52f99a03510a3a72d23141942d216ebe81fefbce/filelock-3.25.2-py3-none-any.whl", hash = "sha256:ca8afb0da15f229774c9ad1b455ed96e85a81373065fb10446672f64444ddf70", size = 26759, upload-time = "2026-03-11T20:45:37.437Z" }, +] + +[[package]] +name = "flatbuffers" +version = "25.12.19" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl", hash = "sha256:7634f50c427838bb021c2d66a3d1168e9d199b0607e6329399f04846d42e20b4", size = 26661, upload-time = "2025-12-19T23:16:13.622Z" }, +] + +[[package]] +name = "frozenlist" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" }, + { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" }, + { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" }, + { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" }, + { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" }, + { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" }, + { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" }, + { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" }, + { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" }, + { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" }, + { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" }, + { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" }, + { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" }, + { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" }, + { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" }, + { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" }, + { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" }, + { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" }, + { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" }, + { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" }, + { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, +] + +[[package]] +name = "fsspec" +version = "2026.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/cf/b50ddf667c15276a9ab15a70ef5f257564de271957933ffea49d2cdbcdfb/fsspec-2026.3.0.tar.gz", hash = "sha256:1ee6a0e28677557f8c2f994e3eea77db6392b4de9cd1f5d7a9e87a0ae9d01b41", size = 313547, upload-time = "2026-03-27T19:11:14.892Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/1f/5f4a3cd9e4440e9d9bc78ad0a91a1c8d46b4d429d5239ebe6793c9fe5c41/fsspec-2026.3.0-py3-none-any.whl", hash = "sha256:d2ceafaad1b3457968ed14efa28798162f1638dbb5d2a6868a2db002a5ee39a4", size = 202595, upload-time = "2026-03-27T19:11:13.595Z" }, +] + +[[package]] +name = "google-api-core" +version = "2.31.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-auth" }, + { name = "googleapis-common-protos" }, + { name = "proto-plus" }, + { name = "protobuf" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/22/155cadf1d49272a9cf48f3168c0f3874fa13397297e611a5ea00cd093880/google_api_core-2.31.0.tar.gz", hash = "sha256:2be84ee0f584c48e6bde1b36766e23348b361fb7e55e56135fc76ce1c397f9c2", size = 176492, upload-time = "2026-06-03T14:52:17.257Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/40/9bdbb60b03a332bd45acb8703da08bbc27d991d35286b62e42acc86d243a/google_api_core-2.31.0-py3-none-any.whl", hash = "sha256:ef79fb3784c71cbac89cbd03301ba0c8fb8ad2aa95d7f9204dd9628f7adf59ab", size = 173102, upload-time = "2026-06-03T14:51:26.729Z" }, +] + +[package.optional-dependencies] +grpc = [ + { name = "grpcio" }, + { name = "grpcio-status" }, +] + +[[package]] +name = "google-api-python-client" +version = "2.198.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core" }, + { name = "google-auth" }, + { name = "google-auth-httplib2" }, + { name = "httplib2" }, + { name = "uritemplate" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6b/53/0cd38e3a29d72ce45e27feba2ce1cd8049d69af9c48cb14fb164f1be9133/google_api_python_client-2.198.0.tar.gz", hash = "sha256:dfe3e16fb241af6e9c460a33f65085b3450e05cea09364f6b5d8997fb7e43e2a", size = 15060142, upload-time = "2026-06-25T14:32:42.953Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/92/0fc9e7a09eb240c31b879bd8d2e43f81ed1f86c4798b79ead4a083921ab3/google_api_python_client-2.198.0-py3-none-any.whl", hash = "sha256:fabac935474e817da5e662ff61bf7139439d6f92b32d332a7318a2d45931e03e", size = 15644203, upload-time = "2026-06-25T14:32:39.963Z" }, +] + +[[package]] +name = "google-auth" +version = "2.55.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "pyasn1-modules" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/6f/f3f4ac177c67bbee8fe8e88f2ab4f36af88c44a096e165c5217accf6e5d3/google_auth-2.55.1.tar.gz", hash = "sha256:fb2d9b730f2c9b8d326ec8d7222f21aef2ead15bf0513793d6442485d87af0a1", size = 349527, upload-time = "2026-06-25T23:39:27.182Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/1d/f6d3ca1ad0725f2e08a1c6915640748a52de2e66596160a4d53b010cccf0/google_auth-2.55.1-py3-none-any.whl", hash = "sha256:eada68dfd52b3b81191827601e2a0c3fa12540c818534b630ddc5355769c3995", size = 252349, upload-time = "2026-06-25T23:38:52.946Z" }, +] + +[[package]] +name = "google-auth-httplib2" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-auth" }, + { name = "httplib2" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/b3/f192c8bc7e41e0ebdbd95afcae4783417a34b6a6af62d22daf22c3fd38fc/google_auth_httplib2-0.4.0.tar.gz", hash = "sha256:d5b030a204b7a4b4d553ba9ca701b62481ee2b74419325580be70f7d85ffed35", size = 11161, upload-time = "2026-05-07T08:03:46.878Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/be/954c35a62b9e31de66b0a43c225c9b6bb9e0f98d6b1dc110a2308e3644f5/google_auth_httplib2-0.4.0-py3-none-any.whl", hash = "sha256:8e55cfafa3358cba85f6cad4a886138e88e158d71e7e5c9ee5936a5c1507fb91", size = 9529, upload-time = "2026-05-07T08:02:12.375Z" }, +] + +[[package]] +name = "google-cloud-vision" +version = "3.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core", extra = ["grpc"] }, + { name = "google-auth" }, + { name = "grpcio" }, + { name = "proto-plus" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/15/7b24e071fa8ee431ea44663ef3c18351cecae45c6433d25fbc64cdb3836b/google_cloud_vision-3.15.0.tar.gz", hash = "sha256:ed2b79ca05a58a929ec112259a9caa698509f6348762a8eced3310e6572a6c70", size = 589231, upload-time = "2026-06-22T23:22:47.231Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/c1/29130149389b654c206e50772a845ff3a375686238b5e73c0c0d94e05f99/google_cloud_vision-3.15.0-py3-none-any.whl", hash = "sha256:6293186bb824a986cdf73bcc83ad0e44a1ef26942347b7e01598061619bb66d4", size = 542216, upload-time = "2026-06-22T23:20:49.89Z" }, +] + +[[package]] +name = "googleapis-common-protos" +version = "1.75.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b5/c8/f439cffde755cffa462bfbb156278fa6f9d09119719af9814b858fd4f81f/googleapis_common_protos-1.75.0.tar.gz", hash = "sha256:53a062ff3c32552fbd62c11fe23768b78e4ddf0494d5e5fd97d3f4689c75fbbd", size = 151035, upload-time = "2026-05-07T08:04:49.423Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/c8/e2645aa8ed02fd4c7a2f59d68783b65b1f3cbdfe39a6308e156509d1fee8/googleapis_common_protos-1.75.0-py3-none-any.whl", hash = "sha256:961ed60399c457ceb0ee8f285a84c870aabc9c6a832b9d37bb281b5bebde43ed", size = 300631, upload-time = "2026-05-07T08:03:30.345Z" }, +] + +[[package]] +name = "greenlet" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3c/3f/dbf99fb14bfeb88c28f16729215478c0e265cacd6dc22270c8f31bb6892f/greenlet-3.5.0.tar.gz", hash = "sha256:d419647372241bc68e957bf38d5c1f98852155e4146bd1e4121adea81f4f01e4", size = 196995, upload-time = "2026-04-27T13:37:15.544Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/5e/a70f31e3e8d961c4ce589c15b28e4225d63704e431a23932a3808cbcc867/greenlet-3.5.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:f35807464c4c58c55f0d31dfa83c541a5615d825c2fe3d2b95360cf7c4e3c0a8", size = 285564, upload-time = "2026-04-27T12:23:08.555Z" }, + { url = "https://files.pythonhosted.org/packages/af/a6/046c0a28e21833e4086918218cfb3d8bed51c075a1b700f20b9d7861c0f4/greenlet-3.5.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55fa7ea52771be44af0de27d8b80c02cd18c2c3cddde6c847ecebdf72418b6a1", size = 651166, upload-time = "2026-04-27T12:52:43.644Z" }, + { url = "https://files.pythonhosted.org/packages/47/f8/4af27f71c5ff32a7fbc516adb46370d9c4ae2bc7bd3dc7d066ac542b4b15/greenlet-3.5.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a97e4821aa710603f94de0da25f25096454d78ffdace5dc77f3a006bc01abba3", size = 663792, upload-time = "2026-04-27T12:59:44.93Z" }, + { url = "https://files.pythonhosted.org/packages/a3/59/1bd6d7428d6ed9106efbb8c52310c60fd04f6672490f452aeaa3829aa436/greenlet-3.5.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f52a464e4ed91780bdfbbdd2b97197f3accaa629b98c200f4dffada759f3ae7", size = 660933, upload-time = "2026-04-27T12:25:33.276Z" }, + { url = "https://files.pythonhosted.org/packages/83/e4/b903e5a5fae1e8a28cdd32a0cfbfd560b668c25b692f67768822ddc5f40f/greenlet-3.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:762612baf1161ccb8437c0161c668a688223cba28e1bf038f4eb47b13e39ccdf", size = 1618401, upload-time = "2026-04-27T12:53:31.062Z" }, + { url = "https://files.pythonhosted.org/packages/0e/e3/5ec408a329acb854fb607a122e1ee5fb3ff649f9a97952948a90803c0d8e/greenlet-3.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:57a43c6079a89713522bc4bcb9f75070ecf5d3dbad7792bfe42239362cbf2a16", size = 1682038, upload-time = "2026-04-27T12:25:31.838Z" }, + { url = "https://files.pythonhosted.org/packages/91/20/6b165108058767ee643c55c5c4904d591a830ee2b3c7dbd359828fbc829f/greenlet-3.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:3bc59be3945ae9750b9e7d45067d01ae3fe90ea5f9ade99239dabdd6e28a5033", size = 239835, upload-time = "2026-04-27T12:24:54.136Z" }, + { url = "https://files.pythonhosted.org/packages/4e/62/1c498375cee177b55d980c1db319f26470e5309e54698c8f8fc06c0fd539/greenlet-3.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:a96fcee45e03fe30a62669fd16ab5c9d3c172660d3085605cb1e2d1280d3c988", size = 236862, upload-time = "2026-04-27T12:23:24.957Z" }, + { url = "https://files.pythonhosted.org/packages/78/a8/4522939255bb5409af4e87132f915446bf3622c2c292d14d3c38d128ae82/greenlet-3.5.0-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:a10a732421ab4fec934783ce3e54763470d0181db6e3468f9103a275c3ed1853", size = 293614, upload-time = "2026-04-27T12:24:12.874Z" }, + { url = "https://files.pythonhosted.org/packages/15/5e/8744c52e2c027b5a8772a01561934c8835f869733e101f62075c60430340/greenlet-3.5.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fc391b1566f2907d17aaebe78f8855dc45675159a775fcf9e61f8ee0078e87f", size = 650723, upload-time = "2026-04-27T12:52:45.412Z" }, + { url = "https://files.pythonhosted.org/packages/00/ef/7b4c39c03cf46ceca512c5d3f914afd85aa30b2cc9a93015b0dd73e4be6c/greenlet-3.5.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:680bd0e7ad5e8daa8a4aa89f68fd6adc834b8a8036dc256533f7e08f4a4b01f7", size = 656529, upload-time = "2026-04-27T12:59:46.295Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b5/c7768f352f5c010f92064d0063f987e7dc0cd290a6d92a34109015ce4aa1/greenlet-3.5.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddb36c7d6c9c0a65f18c7258634e0c416c6ab59caac8c987b96f80c2ebda0112", size = 654364, upload-time = "2026-04-27T12:25:35.64Z" }, + { url = "https://files.pythonhosted.org/packages/ef/d0/079ebe12e4b1fc758857ce5be1a5e73f06870f2101e52611d1e71925ce54/greenlet-3.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e5ddf316ced87539144621453c3aef229575825fe60c604e62bedc4003f372b2", size = 1614204, upload-time = "2026-04-27T12:53:32.618Z" }, + { url = "https://files.pythonhosted.org/packages/6d/89/6c2fb63df3596552d20e58fb4d96669243388cf680cff222758812c7bfaa/greenlet-3.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4a448128607be0de65342dc9b31be7f948ef4cc0bc8832069350abefd310a8f2", size = 1675480, upload-time = "2026-04-27T12:25:34.168Z" }, + { url = "https://files.pythonhosted.org/packages/15/32/77ee8a6c1564fc345a491a4e85b3bf360e4cf26eac98c4532d2fdb96e01f/greenlet-3.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d60097128cb0a1cab9ea541186ea13cd7b847b8449a7787c2e2350da0cb82d86", size = 245324, upload-time = "2026-04-27T12:24:40.295Z" }, +] + +[[package]] +name = "groq" +version = "0.37.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/78/18948a9056e1509c87e10ab8316a90ecce87035fbd53342dffdf97f4de00/groq-0.37.1.tar.gz", hash = "sha256:7353d6dfb60834fd7aacbb86af106e2dc2aeaff6d0edd65fb2fd0f16bd39314c", size = 145289, upload-time = "2025-12-04T18:08:07.118Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/d6/645a081750e43f858b7d09dce5d8e1e76cf11e7e4bdba81252e04f78963d/groq-0.37.1-py3-none-any.whl", hash = "sha256:b49f8c8898c55eaec9f71f1342f3fcacc9560d67a08ce5f35fbfb84e8dacd3da", size = 137494, upload-time = "2025-12-04T18:08:05.801Z" }, +] + +[[package]] +name = "grpcio" +version = "1.80.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b7/48/af6173dbca4454f4637a4678b67f52ca7e0c1ed7d5894d89d434fecede05/grpcio-1.80.0.tar.gz", hash = "sha256:29aca15edd0688c22ba01d7cc01cb000d72b2033f4a3c72a81a19b56fd143257", size = 12978905, upload-time = "2026-03-30T08:49:10.502Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/6d/e65307ce20f5a09244ba9e9d8476e99fb039de7154f37fb85f26978b59c3/grpcio-1.80.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:3d4147a97c8344d065d01bbf8b6acec2cf86fb0400d40696c8bdad34a64ffc0e", size = 6017376, upload-time = "2026-03-30T08:48:10.005Z" }, + { url = "https://files.pythonhosted.org/packages/69/10/9cef5d9650c72625a699c549940f0abb3c4bfdb5ed45a5ce431f92f31806/grpcio-1.80.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:d8e11f167935b3eb089ac9038e1a063e6d7dbe995c0bb4a661e614583352e76f", size = 12018133, upload-time = "2026-03-30T08:48:12.927Z" }, + { url = "https://files.pythonhosted.org/packages/04/82/983aabaad82ba26113caceeb9091706a0696b25da004fe3defb5b346e15b/grpcio-1.80.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f14b618fc30de822681ee986cfdcc2d9327229dc4c98aed16896761cacd468b9", size = 6574748, upload-time = "2026-03-30T08:48:16.386Z" }, + { url = "https://files.pythonhosted.org/packages/07/d7/031666ef155aa0bf399ed7e19439656c38bbd143779ae0861b038ce82abd/grpcio-1.80.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:4ed39fbdcf9b87370f6e8df4e39ca7b38b3e5e9d1b0013c7b6be9639d6578d14", size = 7277711, upload-time = "2026-03-30T08:48:19.627Z" }, + { url = "https://files.pythonhosted.org/packages/e8/43/f437a78f7f4f1d311804189e8f11fb311a01049b2e08557c1068d470cb2e/grpcio-1.80.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2dcc70e9f0ba987526e8e8603a610fb4f460e42899e74e7a518bf3c68fe1bf05", size = 6785372, upload-time = "2026-03-30T08:48:22.373Z" }, + { url = "https://files.pythonhosted.org/packages/93/3d/f6558e9c6296cb4227faa5c43c54a34c68d32654b829f53288313d16a86e/grpcio-1.80.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:448c884b668b868562b1bda833c5fce6272d26e1926ec46747cda05741d302c1", size = 7395268, upload-time = "2026-03-30T08:48:25.638Z" }, + { url = "https://files.pythonhosted.org/packages/06/21/0fdd77e84720b08843c371a2efa6f2e19dbebf56adc72df73d891f5506f0/grpcio-1.80.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a1dc80fe55685b4a543555e6eef975303b36c8db1023b1599b094b92aa77965f", size = 8392000, upload-time = "2026-03-30T08:48:28.974Z" }, + { url = "https://files.pythonhosted.org/packages/f5/68/67f4947ed55d2e69f2cc199ab9fd85e0a0034d813bbeef84df6d2ba4d4b7/grpcio-1.80.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:31b9ac4ad1aa28ffee5503821fafd09e4da0a261ce1c1281c6c8da0423c83b6e", size = 7828477, upload-time = "2026-03-30T08:48:32.054Z" }, + { url = "https://files.pythonhosted.org/packages/44/b6/8d4096691b2e385e8271911a0de4f35f0a6c7d05aff7098e296c3de86939/grpcio-1.80.0-cp314-cp314-win32.whl", hash = "sha256:367ce30ba67d05e0592470428f0ec1c31714cab9ef19b8f2e37be1f4c7d32fae", size = 4218563, upload-time = "2026-03-30T08:48:34.538Z" }, + { url = "https://files.pythonhosted.org/packages/e5/8c/bbe6baf2557262834f2070cf668515fa308b2d38a4bbf771f8f7872a7036/grpcio-1.80.0-cp314-cp314-win_amd64.whl", hash = "sha256:3b01e1f5464c583d2f567b2e46ff0d516ef979978f72091fd81f5ab7fa6e2e7f", size = 5019457, upload-time = "2026-03-30T08:48:37.308Z" }, +] + +[[package]] +name = "grpcio-status" +version = "1.80.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "grpcio" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/ed/105f619bdd00cb47a49aa2feea6232ea2bbb04199d52a22cc6a7d603b5cb/grpcio_status-1.80.0.tar.gz", hash = "sha256:df73802a4c89a3ea88aa2aff971e886fccce162bc2e6511408b3d67a144381cd", size = 13901, upload-time = "2026-03-30T08:54:34.784Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/80/58cd2dfc19a07d022abe44bde7c365627f6c7cb6f692ada6c65ca437d09a/grpcio_status-1.80.0-py3-none-any.whl", hash = "sha256:4b56990363af50dbf2c2ebb80f1967185c07d87aa25aa2bea45ddb75fc181dbe", size = 14638, upload-time = "2026-03-30T08:54:01.569Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "h2" +version = "4.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "hpack" }, + { name = "hyperframe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1d/17/afa56379f94ad0fe8defd37d6eb3f89a25404ffc71d4d848893d270325fc/h2-4.3.0.tar.gz", hash = "sha256:6c59efe4323fa18b47a632221a1888bd7fde6249819beda254aeca909f221bf1", size = 2152026, upload-time = "2025-08-23T18:12:19.778Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/b2/119f6e6dcbd96f9069ce9a2665e0146588dc9f88f29549711853645e736a/h2-4.3.0-py3-none-any.whl", hash = "sha256:c438f029a25f7945c69e0ccf0fb951dc3f73a5f6412981daee861431b70e2bdd", size = 61779, upload-time = "2025-08-23T18:12:17.779Z" }, +] + +[[package]] +name = "hf-xet" +version = "1.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/92/ec9ad04d0b5728dca387a45af7bc98fbb0d73b2118759f5f6038b61a57e8/hf_xet-1.4.3.tar.gz", hash = "sha256:8ddedb73c8c08928c793df2f3401ec26f95be7f7e516a7bee2fbb546f6676113", size = 670477, upload-time = "2026-03-31T22:40:07.874Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/36/3e8f85ca9fe09b8de2b2e10c63b3b3353d7dda88a0b3d426dffbe7b8313b/hf_xet-1.4.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:5251d5ece3a81815bae9abab41cf7ddb7bcb8f56411bce0827f4a3071c92fdc6", size = 3801019, upload-time = "2026-03-31T22:39:56.651Z" }, + { url = "https://files.pythonhosted.org/packages/b5/9c/defb6cb1de28bccb7bd8d95f6e60f72a3d3fa4cb3d0329c26fb9a488bfe7/hf_xet-1.4.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1feb0f3abeacee143367c326a128a2e2b60868ec12a36c225afb1d6c5a05e6d2", size = 3558746, upload-time = "2026-03-31T22:39:54.766Z" }, + { url = "https://files.pythonhosted.org/packages/c1/bd/8d001191893178ff8e826e46ad5299446e62b93cd164e17b0ffea08832ec/hf_xet-1.4.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8b301fc150290ca90b4fccd079829b84bb4786747584ae08b94b4577d82fb791", size = 4207692, upload-time = "2026-03-31T22:39:46.246Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/6790b402803250e9936435613d3a78b9aaeee7973439f0918848dde58309/hf_xet-1.4.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:d972fbe95ddc0d3c0fc49b31a8a69f47db35c1e3699bf316421705741aab6653", size = 3986281, upload-time = "2026-03-31T22:39:44.648Z" }, + { url = "https://files.pythonhosted.org/packages/51/56/ea62552fe53db652a9099eda600b032d75554d0e86c12a73824bfedef88b/hf_xet-1.4.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c5b48db1ee344a805a1b9bd2cda9b6b65fe77ed3787bd6e87ad5521141d317cd", size = 4187414, upload-time = "2026-03-31T22:40:04.951Z" }, + { url = "https://files.pythonhosted.org/packages/7d/f5/bc1456d4638061bea997e6d2db60a1a613d7b200e0755965ec312dc1ef79/hf_xet-1.4.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:22bdc1f5fb8b15bf2831440b91d1c9bbceeb7e10c81a12e8d75889996a5c9da8", size = 4424368, upload-time = "2026-03-31T22:40:06.347Z" }, + { url = "https://files.pythonhosted.org/packages/e4/76/ab597bae87e1f06d18d3ecb8ed7f0d3c9a37037fc32ce76233d369273c64/hf_xet-1.4.3-cp314-cp314t-win_amd64.whl", hash = "sha256:0392c79b7cf48418cd61478c1a925246cf10639f4cd9d94368d8ca1e8df9ea07", size = 3672280, upload-time = "2026-03-31T22:40:16.401Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/2e462d34e23a09a74d73785dbed71cc5dbad82a72eee2ad60a72a554155d/hf_xet-1.4.3-cp314-cp314t-win_arm64.whl", hash = "sha256:681c92a07796325778a79d76c67011764ecc9042a8c3579332b61b63ae512075", size = 3528945, upload-time = "2026-03-31T22:40:14.995Z" }, + { url = "https://files.pythonhosted.org/packages/ac/9f/9c23e4a447b8f83120798f9279d0297a4d1360bdbf59ef49ebec78fe2545/hf_xet-1.4.3-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:d0da85329eaf196e03e90b84c2d0aca53bd4573d097a75f99609e80775f98025", size = 3805048, upload-time = "2026-03-31T22:39:53.105Z" }, + { url = "https://files.pythonhosted.org/packages/0b/f8/7aacb8e5f4a7899d39c787b5984e912e6c18b11be136ef13947d7a66d265/hf_xet-1.4.3-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:e23717ce4186b265f69afa66e6f0069fe7efbf331546f5c313d00e123dc84583", size = 3562178, upload-time = "2026-03-31T22:39:51.295Z" }, + { url = "https://files.pythonhosted.org/packages/df/9a/a24b26dc8a65f0ecc0fe5be981a19e61e7ca963b85e062c083f3a9100529/hf_xet-1.4.3-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc360b70c815bf340ed56c7b8c63aacf11762a4b099b2fe2c9bd6d6068668c08", size = 4212320, upload-time = "2026-03-31T22:39:42.922Z" }, + { url = "https://files.pythonhosted.org/packages/53/60/46d493db155d2ee2801b71fb1b0fd67696359047fdd8caee2c914cc50c79/hf_xet-1.4.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:39f2d2e9654cd9b4319885733993807aab6de9dfbd34c42f0b78338d6617421f", size = 3991546, upload-time = "2026-03-31T22:39:41.335Z" }, + { url = "https://files.pythonhosted.org/packages/bc/f5/067363e1c96c6b17256910830d1b54099d06287e10f4ec6ec4e7e08371fc/hf_xet-1.4.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:49ad8a8cead2b56051aa84d7fce3e1335efe68df3cf6c058f22a65513885baac", size = 4193200, upload-time = "2026-03-31T22:40:01.936Z" }, + { url = "https://files.pythonhosted.org/packages/42/4b/53951592882d9c23080c7644542fda34a3813104e9e11fa1a7d82d419cb8/hf_xet-1.4.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7716d62015477a70ea272d2d68cd7cad140f61c52ee452e133e139abfe2c17ba", size = 4429392, upload-time = "2026-03-31T22:40:03.492Z" }, + { url = "https://files.pythonhosted.org/packages/8a/21/75a6c175b4e79662ad8e62f46a40ce341d8d6b206b06b4320d07d55b188c/hf_xet-1.4.3-cp37-abi3-win_amd64.whl", hash = "sha256:6b591fcad34e272a5b02607485e4f2a1334aebf1bc6d16ce8eb1eb8978ac2021", size = 3677359, upload-time = "2026-03-31T22:40:13.619Z" }, + { url = "https://files.pythonhosted.org/packages/8a/7c/44314ecd0e89f8b2b51c9d9e5e7a60a9c1c82024ac471d415860557d3cd8/hf_xet-1.4.3-cp37-abi3-win_arm64.whl", hash = "sha256:7c2c7e20bcfcc946dc67187c203463f5e932e395845d098cc2a93f5b67ca0b47", size = 3533664, upload-time = "2026-03-31T22:40:12.152Z" }, +] + +[[package]] +name = "hpack" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/5b/fcabf6028144a8723726318b07a32c2f3314acdff6265743cf08a344b18e/hpack-4.2.0.tar.gz", hash = "sha256:0895cfa3b5531fc65fe439c05eb65144f123bf7a394fcaa56aa423548d8e45c0", size = 51300, upload-time = "2026-06-23T18:34:46.667Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/b4/4a9fcfb2aef6ba44d9073ecd301443aa00b3dac95de5619f2a7de7ec8a91/hpack-4.2.0-py3-none-any.whl", hash = "sha256:858ac0b02280fa582b5080d68db0899c62a80375e0e5413a74970c5e518b6986", size = 34246, upload-time = "2026-06-23T18:34:45.472Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httplib2" +version = "0.32.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyparsing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/84/f5/ccf58de92d61e3ad921119668f54ed36ca1d0cf5dcc5c1657dfb164fd78b/httplib2-0.32.0.tar.gz", hash = "sha256:48a0ef30a42db65d8f3399045e1d09ab0ba66e3b9efc360d07f80ea55d286025", size = 254283, upload-time = "2026-06-26T10:13:56.265Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/a0/550eec327e5f5c7b732531c489f5307efec41f047b0d703bd4ca1e5ad2db/httplib2-0.32.0-py3-none-any.whl", hash = "sha256:dc6705cacdf3fb0a2aba7629fa33c90fd93e30035db0c157325826be177e4816", size = 93148, upload-time = "2026-06-26T10:13:54.985Z" }, +] + +[[package]] +name = "httptools" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/46/120a669232c7bdedb9d52d4aeae7e6c7dfe151e99dc70802e2fc7a5e1993/httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9", size = 258961, upload-time = "2025-10-10T03:55:08.559Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/50/9d095fcbb6de2d523e027a2f304d4551855c2f46e0b82befd718b8b20056/httptools-0.7.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:c08fe65728b8d70b6923ce31e3956f859d5e1e8548e6f22ec520a962c6757270", size = 203619, upload-time = "2025-10-10T03:54:54.321Z" }, + { url = "https://files.pythonhosted.org/packages/07/f0/89720dc5139ae54b03f861b5e2c55a37dba9a5da7d51e1e824a1f343627f/httptools-0.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7aea2e3c3953521c3c51106ee11487a910d45586e351202474d45472db7d72d3", size = 108714, upload-time = "2025-10-10T03:54:55.163Z" }, + { url = "https://files.pythonhosted.org/packages/b3/cb/eea88506f191fb552c11787c23f9a405f4c7b0c5799bf73f2249cd4f5228/httptools-0.7.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0e68b8582f4ea9166be62926077a3334064d422cf08ab87d8b74664f8e9058e1", size = 472909, upload-time = "2025-10-10T03:54:56.056Z" }, + { url = "https://files.pythonhosted.org/packages/e0/4a/a548bdfae6369c0d078bab5769f7b66f17f1bfaa6fa28f81d6be6959066b/httptools-0.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df091cf961a3be783d6aebae963cc9b71e00d57fa6f149025075217bc6a55a7b", size = 470831, upload-time = "2025-10-10T03:54:57.219Z" }, + { url = "https://files.pythonhosted.org/packages/4d/31/14df99e1c43bd132eec921c2e7e11cda7852f65619bc0fc5bdc2d0cb126c/httptools-0.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f084813239e1eb403ddacd06a30de3d3e09a9b76e7894dcda2b22f8a726e9c60", size = 452631, upload-time = "2025-10-10T03:54:58.219Z" }, + { url = "https://files.pythonhosted.org/packages/22/d2/b7e131f7be8d854d48cb6d048113c30f9a46dca0c9a8b08fcb3fcd588cdc/httptools-0.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7347714368fb2b335e9063bc2b96f2f87a9ceffcd9758ac295f8bbcd3ffbc0ca", size = 452910, upload-time = "2025-10-10T03:54:59.366Z" }, + { url = "https://files.pythonhosted.org/packages/53/cf/878f3b91e4e6e011eff6d1fa9ca39f7eb17d19c9d7971b04873734112f30/httptools-0.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96", size = 88205, upload-time = "2025-10-10T03:55:00.389Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[package.optional-dependencies] +brotli = [ + { name = "brotli", marker = "platform_python_implementation == 'CPython'" }, + { name = "brotlicffi", marker = "platform_python_implementation != 'CPython'" }, +] +http2 = [ + { name = "h2" }, +] +socks = [ + { name = "socksio" }, +] + +[[package]] +name = "httpx-sse" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, +] + +[[package]] +name = "huggingface-hub" +version = "0.36.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "fsspec" }, + { name = "hf-xet", marker = "platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7c/b7/8cb61d2eece5fb05a83271da168186721c450eb74e3c31f7ef3169fa475b/huggingface_hub-0.36.2.tar.gz", hash = "sha256:1934304d2fb224f8afa3b87007d58501acfda9215b334eed53072dd5e815ff7a", size = 649782, upload-time = "2026-02-06T09:24:13.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/af/48ac8483240de756d2438c380746e7130d1c6f75802ef22f3c6d49982787/huggingface_hub-0.36.2-py3-none-any.whl", hash = "sha256:48f0c8eac16145dfce371e9d2d7772854a4f591bcb56c9cf548accf531d54270", size = 566395, upload-time = "2026-02-06T09:24:11.133Z" }, +] + +[[package]] +name = "hyperframe" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/e7/94f8232d4a74cc99514c13a9f995811485a6903d48e5d952771ef6322e30/hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08", size = 26566, upload-time = "2025-01-22T21:41:49.302Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/30/47d0bf6072f7252e6521f3447ccfa40b421b6824517f82854703d0f5a98b/hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5", size = 13007, upload-time = "2025-01-22T21:41:47.295Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, +] + +[[package]] +name = "importlib-resources" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/06/b56dfa750b44e86157093bc8fca0ab81dccbf5260510de4eaf1cb69b5b99/importlib_resources-7.1.0.tar.gz", hash = "sha256:0722d4c6212489c530f2a145a34c0a7a3b4721bc96a15fada5930e2a0b760708", size = 44985, upload-time = "2026-04-12T16:36:09.232Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/db/55a262f3606bebcae07cc14095338471ad7c0bbcaa37707e6f0ee49725b7/importlib_resources-7.1.0-py3-none-any.whl", hash = "sha256:1bd7b48b4088eddb2cd16382150bb515af0bd2c70128194392725f82ad2c96a1", size = 37232, upload-time = "2026-04-12T16:36:08.219Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "joblib" +version = "1.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/f2/d34e8b3a08a9cc79a50b2208a93dce981fe615b64d5a4d4abee421d898df/joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3", size = 331603, upload-time = "2025-12-15T08:41:46.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" }, +] + +[[package]] +name = "jsonpatch" +version = "1.33" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonpointer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/78/18813351fe5d63acad16aec57f94ec2b70a09e53ca98145589e185423873/jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c", size = 21699, upload-time = "2023-06-26T12:07:29.144Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/07/02e16ed01e04a374e644b575638ec7987ae846d25ad97bcc9945a3ee4b0e/jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade", size = 12898, upload-time = "2023-06-16T21:01:28.466Z" }, +] + +[[package]] +name = "jsonpointer" +version = "3.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/c7/af399a2e7a67fd18d63c40c5e62d3af4e67b836a2107468b6a5ea24c4304/jsonpointer-3.1.1.tar.gz", hash = "sha256:0b801c7db33a904024f6004d526dcc53bbb8a4a0f4e32bfd10beadf60adf1900", size = 9068, upload-time = "2026-03-23T22:32:32.458Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/6a/a83720e953b1682d2d109d3c2dbb0bc9bf28cc1cbc205be4ef4be5da709d/jsonpointer-3.1.1-py3-none-any.whl", hash = "sha256:8ff8b95779d071ba472cf5bc913028df06031797532f08a7d5b602d8b2a488ca", size = 7659, upload-time = "2026-03-23T22:32:31.568Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + +[[package]] +name = "kubernetes" +version = "35.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "durationpy" }, + { name = "python-dateutil" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "requests-oauthlib" }, + { name = "six" }, + { name = "urllib3" }, + { name = "websocket-client" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2c/8f/85bf51ad4150f64e8c665daf0d9dfe9787ae92005efb9a4d1cba592bd79d/kubernetes-35.0.0.tar.gz", hash = "sha256:3d00d344944239821458b9efd484d6df9f011da367ecb155dadf9513f05f09ee", size = 1094642, upload-time = "2026-01-16T01:05:27.76Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/70/05b685ea2dffcb2adbf3cdcea5d8865b7bc66f67249084cf845012a0ff13/kubernetes-35.0.0-py2.py3-none-any.whl", hash = "sha256:39e2b33b46e5834ef6c3985ebfe2047ab39135d41de51ce7641a7ca5b372a13d", size = 2017602, upload-time = "2026-01-16T01:05:25.991Z" }, +] + +[[package]] +name = "langchain-chroma" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "chromadb" }, + { name = "langchain-core" }, + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fa/0e/54896830b7331c90788cf96b2c37858977c199da9ecdaf85cf11eb6e6bc1/langchain_chroma-1.1.0.tar.gz", hash = "sha256:8069685e7848041e998d16c8a4964256b031fd20551bf59429173415bc2adc12", size = 220382, upload-time = "2025-12-12T16:23:01.399Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/35/2a6d1191acaad043647e28313b0ecd161d61f09d8be37d1996a90d752c13/langchain_chroma-1.1.0-py3-none-any.whl", hash = "sha256:ff65e4a2ccefb0fb9fde2ff38705022ace402f979d557f018f6e623f7288f0fc", size = 12981, upload-time = "2025-12-12T16:23:00.196Z" }, +] + +[[package]] +name = "langchain-classic" +version = "1.0.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain-core" }, + { name = "langchain-text-splitters" }, + { name = "langsmith" }, + { name = "pydantic" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "sqlalchemy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9b/78/84b5065816f348c39fefa4316f209f0135e8410216340a953bec17d9e4e4/langchain_classic-1.0.7.tar.gz", hash = "sha256:debbec8065e69b95108d2652e8d5c44f4516e19aa8d716c02ed2211c3aee099d", size = 10554118, upload-time = "2026-05-07T15:46:56.8Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/78/2d9980d028ff0523eea503a77c200e2ff252a3a75eb6e7842bcf5f9c979b/langchain_classic-1.0.7-py3-none-any.whl", hash = "sha256:d9d9be38f7aa534ed0259c2410432e34a1f80b1d491e686749bb55af56479be3", size = 1041386, upload-time = "2026-05-07T15:46:54.845Z" }, +] + +[[package]] +name = "langchain-community" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "dataclasses-json" }, + { name = "httpx-sse" }, + { name = "langchain-classic" }, + { name = "langchain-core" }, + { name = "langsmith" }, + { name = "numpy" }, + { name = "pydantic-settings" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "sqlalchemy" }, + { name = "tenacity" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/53/97/a03585d42b9bdb6fbd935282d6e3348b10322a24e6ce12d0c99eb461d9af/langchain_community-0.4.1.tar.gz", hash = "sha256:f3b211832728ee89f169ddce8579b80a085222ddb4f4ed445a46e977d17b1e85", size = 33241144, upload-time = "2025-10-27T15:20:32.504Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/a4/c4fde67f193401512337456cabc2148f2c43316e445f5decd9f8806e2992/langchain_community-0.4.1-py3-none-any.whl", hash = "sha256:2135abb2c7748a35c84613108f7ebf30f8505b18c3c18305ffaecfc7651f6c6a", size = 2533285, upload-time = "2025-10-27T15:20:30.767Z" }, +] + +[[package]] +name = "langchain-core" +version = "1.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonpatch" }, + { name = "langchain-protocol" }, + { name = "langsmith" }, + { name = "packaging" }, + { name = "pydantic" }, + { name = "pyyaml" }, + { name = "tenacity" }, + { name = "typing-extensions" }, + { name = "uuid-utils" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d3/ae/8b74458fc3850ec3d150eb9f45e857db129dafa801fb5cf173dfc9f8bbf3/langchain_core-1.3.3.tar.gz", hash = "sha256:fa510a5db8efdc0c6ff41c0939fb5c00a0183c11f6b84233e892e3227ff69182", size = 915041, upload-time = "2026-05-05T19:02:36.612Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/01/4771b7ab2af1d1aba5b710bd8f13d9225c609425214b357590a17b01be77/langchain_core-1.3.3-py3-none-any.whl", hash = "sha256:18aae8506f37da7f74398492279a7d6efcee4f8e23c4c41c7af080eeb7ef7bd1", size = 543857, upload-time = "2026-05-05T19:02:34.52Z" }, +] + +[[package]] +name = "langchain-groq" +version = "1.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "groq" }, + { name = "langchain-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/d9/bbaa43598fcaffb669c5aea4088d92c77a426c46d25a013c21c979772a54/langchain_groq-1.1.2.tar.gz", hash = "sha256:67d1d752fb6590be517735947ec49b4ab9ed9191c7cca79f105d227775c67ae5", size = 178337, upload-time = "2026-02-02T15:57:29.435Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/11/71a35db3ed8ac2c7129eb69f0d590e4961be67ac84b6c470fc097d0dd7c8/langchain_groq-1.1.2-py3-none-any.whl", hash = "sha256:1f59f12233e8e6280c968bca6c40a7e5434e971e9a5387ad23c4c64ec776de10", size = 19450, upload-time = "2026-02-02T15:57:28.6Z" }, +] + +[[package]] +name = "langchain-huggingface" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, + { name = "langchain-core" }, + { name = "tokenizers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2c/e8/4068ad02179253f55958e59e442e5b6e8cb95ffc5e805cc4db0b1ef61d4e/langchain_huggingface-1.2.2.tar.gz", hash = "sha256:1dd91ec415190d2704e93ec149618e3145075863ba37e74afc9080d685dc2743", size = 255513, upload-time = "2026-04-16T19:57:41.046Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/ed/648b87f9b67153ade616f360bf4145b76ed428b4adb89525938f611e9828/langchain_huggingface-1.2.2-py3-none-any.whl", hash = "sha256:f94944b0c0d5afc687568d426c87ed5236907464c41e72108ed76eee1a690f6d", size = 31926, upload-time = "2026-04-16T19:57:40.079Z" }, +] + +[[package]] +name = "langchain-protocol" +version = "0.0.15" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4f/24/9777489d6fbbee64af0c8f96d4f840239c408cf694f3394672807dafc490/langchain_protocol-0.0.15.tar.gz", hash = "sha256:9ab2d11ee73944754f10e037e717098d3a6796f0e58afa9cadda6154e7655ade", size = 5862, upload-time = "2026-05-01T22:30:04.748Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/7a/9c97a7b9cbe4c5dc6a44cdb1545450c28f0c8ce89b9c1f0ee7fbad896263/langchain_protocol-0.0.15-py3-none-any.whl", hash = "sha256:461eb794358f83d5e42635a5797799ffec7b4702314e34edf73ac21e75d3ef79", size = 6982, upload-time = "2026-05-01T22:30:03.877Z" }, +] + +[[package]] +name = "langchain-text-splitters" +version = "1.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/26/9f/6c545900fefb7b00ddfa3f16b80d61338a0ec68c31c5451eeeab99082760/langchain_text_splitters-1.1.2.tar.gz", hash = "sha256:782a723db0a4746ac91e251c7c1d57fd23636e4f38ed733074e28d7a86f41627", size = 293580, upload-time = "2026-04-16T14:20:39.162Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/26/1ef06f56198d631296d646a6223de35bcc6cf9795ceb2442816bc963b84c/langchain_text_splitters-1.1.2-py3-none-any.whl", hash = "sha256:a2de0d799ff31886429fd6e2e0032df275b60ec817c19059a7b46181cc1c2f10", size = 35903, upload-time = "2026-04-16T14:20:38.243Z" }, +] + +[[package]] +name = "langsmith" +version = "0.8.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "orjson", marker = "platform_python_implementation != 'PyPy'" }, + { name = "packaging" }, + { name = "pydantic" }, + { name = "requests" }, + { name = "requests-toolbelt" }, + { name = "uuid-utils" }, + { name = "xxhash" }, + { name = "zstandard" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/8a/1e8ea5e8bab2a65fa95bd36229ef38e8723ec46e430e20ca2d953487a7f1/langsmith-0.8.3.tar.gz", hash = "sha256:767ff7a8d136ed42926bf99059ac631dc6883542d6e3104b32e71c7625e1fa05", size = 4460330, upload-time = "2026-05-07T19:56:56.18Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/a9/51e644c1f1dbc3dd7d22dfd6412eab206d538c81e024e4f287373544bdcb/langsmith-0.8.3-py3-none-any.whl", hash = "sha256:b2e40e308222fa0beb2dccee3b4b30bfee9062d7a4f20a3e3e93df3c51a08ab4", size = 399048, upload-time = "2026-05-07T19:56:53.994Z" }, +] + +[[package]] +name = "lxml" +version = "6.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/3b/aab6728cae887456f409b4d75e8a01856e4f04bd510de38052a47768b680/lxml-6.1.1.tar.gz", hash = "sha256:ba96ae44888e0185281e937633a743ea90d5a196c6000f82565ebb0580012d40", size = 4197430, upload-time = "2026-05-18T19:19:06.424Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/e2/2e325795566de01d0d7c3bb57d3c370616b2d07b01214e84eec5d3b10963/lxml-6.1.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:19b7ab10b210b0b3ad7985d9ac4eb66ab09a90b20fe6e2f7ba55d01a234345d0", size = 8577146, upload-time = "2026-05-18T19:18:17.765Z" }, + { url = "https://files.pythonhosted.org/packages/93/cf/5630b5e4be7d2e6bee8efe83865c925221103cf0221303b104ce134b01e2/lxml-6.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c08e5c694306507275f2290073350c4f32e383db15213b2c69e7ff39c1193840", size = 4623866, upload-time = "2026-05-18T19:18:30.669Z" }, + { url = "https://files.pythonhosted.org/packages/d2/51/3904907c063451cf8d4a5c9fe0cad95fa1f4ec57f4e3884fa0731bd7a305/lxml-6.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:74a9717fd0d82effef5c2854f0d917231d5324b5a3eb7275c43ac9fa32f97a14", size = 4950022, upload-time = "2026-05-18T19:19:31.958Z" }, + { url = "https://files.pythonhosted.org/packages/94/cd/9c7611a51c37a2830928405817cc5d56a97f64fab83cc3f628748b135749/lxml-6.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:efe0374196335f93b53269acd811b944f2e6bdc88e8894f214bd636455484909", size = 5086695, upload-time = "2026-05-18T19:19:34.764Z" }, + { url = "https://files.pythonhosted.org/packages/da/d6/24e3b5906abb0b674ff2ae195bc3ce59708df2bcd17cf17703b2d7dd643a/lxml-6.1.1-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac931cdc9442c1763b8a8f6cd62c0c938737eafc5be75eff88df55fc73bc0d00", size = 5031642, upload-time = "2026-05-18T19:19:37.771Z" }, + { url = "https://files.pythonhosted.org/packages/2d/db/6ec54f99019838bff54785c51da07f189eb4676861c5f2730962b0d8d665/lxml-6.1.1-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:aee395f5d0927f947758b4ec119fd5fc8ec71f07a1c5c52077b30b04c0fa6955", size = 5647338, upload-time = "2026-05-18T19:19:40.553Z" }, + { url = "https://files.pythonhosted.org/packages/42/3d/ef4dcfffd22d27a61805d8ed9f7fb888495bc6aa88648fa07c1eaa5586b6/lxml-6.1.1-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9395002973c827b3ed67db77e6ec09f092919a587022174554096a269378fb13", size = 5239528, upload-time = "2026-05-18T19:19:43.657Z" }, + { url = "https://files.pythonhosted.org/packages/62/bb/37fb3f0dff146bdcfa78eec47879273820b2a0bf350ec236ce14bd0b1c26/lxml-6.1.1-cp314-cp314-manylinux_2_28_i686.whl", hash = "sha256:73bc2086f141224ebddb7fc5c6a36ca58b31b94b561e1dfe8e073e3270fad1e7", size = 5350730, upload-time = "2026-05-18T19:19:46.307Z" }, + { url = "https://files.pythonhosted.org/packages/90/42/43253f168388df4fae1f38c01df36ddb9bee39e2048167b54cdcbae85ea3/lxml-6.1.1-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:3779def59032b81e44a5f70096ef6bf2082f8d901937dca354474ba09782e245", size = 4697530, upload-time = "2026-05-18T19:19:49.889Z" }, + { url = "https://files.pythonhosted.org/packages/eb/a8/c5a8504f81bbdfc8e7094c2c850cdb4ed6777fc4d5ddd9e5ab819f3b0d54/lxml-6.1.1-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:86c89b9d55ebf820ad7c90bc533410f0d098054f293351f10603c0c46ff598f5", size = 5250670, upload-time = "2026-05-18T19:19:53.199Z" }, + { url = "https://files.pythonhosted.org/packages/77/b7/c7e76ab18744d75e21f320ebf9ff9d1ceae2b54dd431ea5a64caf26c9672/lxml-6.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19607c6bbff2a44cf3fe8250abccd20942d3462473e0a721d01d379ed017e462", size = 5084485, upload-time = "2026-05-18T19:19:08.422Z" }, + { url = "https://files.pythonhosted.org/packages/31/31/b35c53f8ef7b7c31cacd23d3638652fff7bcd1deb6eedb709ab43b685908/lxml-6.1.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:c6ed5141a5c7507cf3ee76bd363b0d6f801e3321adc35b5d825a23115faa5465", size = 4737635, upload-time = "2026-05-18T19:19:12.321Z" }, + { url = "https://files.pythonhosted.org/packages/d9/06/31f23c813a7fe8e0cb1b175e915b08c9bf4e86d225b210feadbdbe519667/lxml-6.1.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:62aeb7e85b5d60320b9d77eef2e773994e2c0ce10121b277e0a19804e1654a5a", size = 5670681, upload-time = "2026-05-18T19:19:15.001Z" }, + { url = "https://files.pythonhosted.org/packages/1a/bc/ce619bccc89b1fd9ad8a8e1330ee3f3beff9f2ff95b712d7bbcdd6e22fc3/lxml-6.1.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b1b963fd8f5caa68e99dfae060d54de1fe9cba899b8718b44a00cdca53c3e590", size = 5238229, upload-time = "2026-05-18T19:19:18.131Z" }, + { url = "https://files.pythonhosted.org/packages/2f/5d/b329acbbedc0b619ebc2be6cf7ee9ed07e80892c88d4dfd612c33805789a/lxml-6.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:63876be28efefa04a1df615b46770e82042cce445cfdce55160522f57b231ccb", size = 5264191, upload-time = "2026-05-18T19:19:21.118Z" }, + { url = "https://files.pythonhosted.org/packages/d6/85/be36fb1425b30db3c3f9df75fe86343ebffb79e6320bd7f588e25bfeac39/lxml-6.1.1-cp314-cp314-win32.whl", hash = "sha256:7f7a92e8583f06b1fd49d01158143b8461cfcd135dcb10ec807270a3051bd603", size = 3657202, upload-time = "2026-05-18T19:17:39.509Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ce/3cf9a827342269f54d405a6202397de63f07c69cbd6ce7d183a3f0cba1e9/lxml-6.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:b2d444f2e66624d68e9c6b211e28a76e22fff5fcabcfff4deac18b529b7d4137", size = 4064497, upload-time = "2026-05-18T19:18:14.662Z" }, + { url = "https://files.pythonhosted.org/packages/d9/3e/1a957bde8f0760039e627f94699f82caa782c9d838d86c3d28245ee67212/lxml-6.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:3fd9728a2735fda14f4e8235830c86b539e9661e849665bf926d3f867943b4bf", size = 3741991, upload-time = "2026-05-19T19:22:59.111Z" }, + { url = "https://files.pythonhosted.org/packages/78/b2/00ed55b3a2efa4658fb795c38d1090ec9b3e8a6c3683d4441fa517f09c3b/lxml-6.1.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:787b2496d0dbe8cd180984e8d29e3a6f76e7ea34db781cb3bd55e4ba1ef8b4ee", size = 8827545, upload-time = "2026-05-18T19:18:41.193Z" }, + { url = "https://files.pythonhosted.org/packages/c0/73/74573db19baa618d5f266f2407898b087ff6927115b00b71e5fc1b700847/lxml-6.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2c8daa471358dc2d6fcf02165e80ec68f77871a286df95bc5cc3816153b0fd2c", size = 4735736, upload-time = "2026-05-18T19:18:46.761Z" }, + { url = "https://files.pythonhosted.org/packages/16/02/6f7061f4f95f51e545d48e87647c54791d204a4e881be4156e7a26ba5338/lxml-6.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:acd7d70b64c0aae0c7922cca83d288a16f5f6da523637697872253415269baef", size = 4970291, upload-time = "2026-05-18T19:19:56.215Z" }, + { url = "https://files.pythonhosted.org/packages/b0/02/55fc057d8283427dea7d6edb102e7a840239c77a64a983d92f62a304c0e9/lxml-6.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4f0dd2f01f9f8a89f565d000e03abcf0a13d692a346c8d22f628d49af098777a", size = 5102822, upload-time = "2026-05-18T19:19:59.223Z" }, + { url = "https://files.pythonhosted.org/packages/e4/48/8e1cf78d89d66850121d9255a2a24414c98f775da93b90cf976956c24b14/lxml-6.1.1-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b7e8a14c8634bf6f7a568634cb395305a6d964aeb5b7ee32248094bed3a7e2c", size = 5027923, upload-time = "2026-05-18T19:20:01.549Z" }, + { url = "https://files.pythonhosted.org/packages/ed/00/0632a0647612c8af24d26997b3b961397daa9d5b2581444805933629a4cb/lxml-6.1.1-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:86281fbdd6a8162756f8d603f37e3435bfa38043adb79c6dc6a2dfee065e7525", size = 5595843, upload-time = "2026-05-18T19:20:03.93Z" }, + { url = "https://files.pythonhosted.org/packages/bc/86/ab008a7dc360711b66858d61c80a5979a70a09f2aa2b05d9698df80b803d/lxml-6.1.1-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5d7152ec39ca7c402d8fb9bad86140a15b9503bd0c54484e3f1bbe3dd37ceca", size = 5224515, upload-time = "2026-05-18T19:20:06.381Z" }, + { url = "https://files.pythonhosted.org/packages/75/c6/2702ff375e728e34f56d9a45339a9cf7e4427e917f542225242d63a05afa/lxml-6.1.1-cp314-cp314t-manylinux_2_28_i686.whl", hash = "sha256:88d8cb75b9d82858497a5393e3c63cfbf03035225e4b35a49ed7ccb151e4dc0e", size = 5312511, upload-time = "2026-05-18T19:20:09.308Z" }, + { url = "https://files.pythonhosted.org/packages/b7/57/a5807c98f87a86f10ef9ffab35516df7c0f0c4b6d5d33e9f608ab9c04a31/lxml-6.1.1-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f64ec5397ea6a41fc1b4af0380d79b44a755b5531dcaccd9940fb260dca93038", size = 4639206, upload-time = "2026-05-18T19:20:11.704Z" }, + { url = "https://files.pythonhosted.org/packages/1f/e1/8a0a2c35734812395f4da4eaf33748a7e5705bfb2a58b128da764339d5ec/lxml-6.1.1-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d34bbf07dbc7ca5970671b1512e928991fb5e9d95365636c9b2d8b4f53af405e", size = 5232404, upload-time = "2026-05-18T19:20:14.064Z" }, + { url = "https://files.pythonhosted.org/packages/c2/e2/0e6a4dd5ad84d01d99aa7bae7cfefd4a760a0e0f8176818241de17d9b6c0/lxml-6.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:17e0e18d4ad8adbd0399291bc44845b69d9dd68439a3cdebdf35ff902ec05072", size = 5083769, upload-time = "2026-05-18T19:19:23.758Z" }, + { url = "https://files.pythonhosted.org/packages/a0/7e/161f33d463f6ffc1c7679104b65086dea120080d49dde4d238f015aaee2f/lxml-6.1.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:3ab541146f1f6968c462d6c2ac495148e8cdba2f8347700b2141b6ec5a75bf52", size = 4758936, upload-time = "2026-05-18T19:19:27.256Z" }, + { url = "https://files.pythonhosted.org/packages/f1/fb/2369825e3f6ca99305bf9f7b7085fda91c8b0922a89e54d900974aa3ef85/lxml-6.1.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2a0217714657e023ef4293500f65aa20fce6164c8fd6b08fa5bd4a859fb14b9b", size = 5620296, upload-time = "2026-05-18T19:19:29.993Z" }, + { url = "https://files.pythonhosted.org/packages/30/90/d61e383146f74c5ab683947ea14dc7b82778838ab9b95ea73a23b60d0191/lxml-6.1.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:05a82eb6e1530a64f26225b55cbd178113bd0b5af1c2b625f25e5296742c26d2", size = 5228598, upload-time = "2026-05-18T19:19:33.523Z" }, + { url = "https://files.pythonhosted.org/packages/76/2d/2dafd8149e94b05bb070690efd5bb2680720681e03ff03fc57d2b70a1105/lxml-6.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9e36f163528fc50cbef305f02a5fd66d404edf7049cdaff211dbc2cba5a7013e", size = 5247845, upload-time = "2026-05-18T19:19:36.649Z" }, + { url = "https://files.pythonhosted.org/packages/ce/68/b30e913340c380ddac9580c6e6230991fc37240ec4f64704833e4f3e2769/lxml-6.1.1-cp314-cp314t-win32.whl", hash = "sha256:649dda677cf3bd6ac9ae14007ba0c824ded8ce5808b53fc7431d9140399118c1", size = 3897345, upload-time = "2026-05-18T19:17:33.562Z" }, + { url = "https://files.pythonhosted.org/packages/3c/4e/9eb2af5335545f9fbcd7af57bcf87c6025d31eaa31b14ec184a6c8675328/lxml-6.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:793033d6c5cdf33a573f910d9bea14ef8f5771820411d118da8e1182edb53d5e", size = 4393350, upload-time = "2026-05-18T19:18:10.076Z" }, + { url = "https://files.pythonhosted.org/packages/7f/2c/0f1e93c636720e8a3eb59af2bfda99d98b55891e1c53bc30c2e0e865f01b/lxml-6.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:58bb955caba94e467d2a96da17660d2d704e0675894cba21ab8a775b8621fd1c", size = 3817223, upload-time = "2026-05-19T19:22:56.823Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "marshmallow" +version = "3.26.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/79/de6c16cc902f4fc372236926b0ce2ab7845268dcc30fb2fbb7f71b418631/marshmallow-3.26.2.tar.gz", hash = "sha256:bbe2adb5a03e6e3571b573f42527c6fe926e17467833660bebd11593ab8dfd57", size = 222095, upload-time = "2025-12-22T06:53:53.309Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/2f/5108cb3ee4ba6501748c4908b908e55f42a5b66245b4cfe0c99326e1ef6e/marshmallow-3.26.2-py3-none-any.whl", hash = "sha256:013fa8a3c4c276c24d26d84ce934dc964e2aa794345a0f8c7e5a7191482c8a73", size = 50964, upload-time = "2025-12-22T06:53:51.801Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "ml-dtypes" +version = "0.5.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/4a/c27b42ed9b1c7d13d9ba8b6905dece787d6259152f2309338aed29b2447b/ml_dtypes-0.5.4.tar.gz", hash = "sha256:8ab06a50fb9bf9666dd0fe5dfb4676fa2b0ac0f31ecff72a6c3af8e22c063453", size = 692314, upload-time = "2025-11-17T22:32:31.031Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/4e/1339dc6e2557a344f5ba5590872e80346f76f6cb2ac3dd16e4666e88818c/ml_dtypes-0.5.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:2b857d3af6ac0d39db1de7c706e69c7f9791627209c3d6dedbfca8c7e5faec22", size = 673781, upload-time = "2025-11-17T22:32:11.364Z" }, + { url = "https://files.pythonhosted.org/packages/04/f9/067b84365c7e83bda15bba2b06c6ca250ce27b20630b1128c435fb7a09aa/ml_dtypes-0.5.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:805cef3a38f4eafae3a5bf9ebdcdb741d0bcfd9e1bd90eb54abd24f928cd2465", size = 5036145, upload-time = "2025-11-17T22:32:12.783Z" }, + { url = "https://files.pythonhosted.org/packages/c6/bb/82c7dcf38070b46172a517e2334e665c5bf374a262f99a283ea454bece7c/ml_dtypes-0.5.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14a4fd3228af936461db66faccef6e4f41c1d82fcc30e9f8d58a08916b1d811f", size = 5010230, upload-time = "2025-11-17T22:32:14.38Z" }, + { url = "https://files.pythonhosted.org/packages/e9/93/2bfed22d2498c468f6bcd0d9f56b033eaa19f33320389314c19ef6766413/ml_dtypes-0.5.4-cp314-cp314-win_amd64.whl", hash = "sha256:8c6a2dcebd6f3903e05d51960a8058d6e131fe69f952a5397e5dbabc841b6d56", size = 221032, upload-time = "2025-11-17T22:32:15.763Z" }, + { url = "https://files.pythonhosted.org/packages/76/a3/9c912fe6ea747bb10fe2f8f54d027eb265db05dfb0c6335e3e063e74e6e8/ml_dtypes-0.5.4-cp314-cp314-win_arm64.whl", hash = "sha256:5a0f68ca8fd8d16583dfa7793973feb86f2fbb56ce3966daf9c9f748f52a2049", size = 163353, upload-time = "2025-11-17T22:32:16.932Z" }, + { url = "https://files.pythonhosted.org/packages/cd/02/48aa7d84cc30ab4ee37624a2fd98c56c02326785750cd212bc0826c2f15b/ml_dtypes-0.5.4-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:bfc534409c5d4b0bf945af29e5d0ab075eae9eecbb549ff8a29280db822f34f9", size = 702085, upload-time = "2025-11-17T22:32:18.175Z" }, + { url = "https://files.pythonhosted.org/packages/5a/e7/85cb99fe80a7a5513253ec7faa88a65306be071163485e9a626fce1b6e84/ml_dtypes-0.5.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2314892cdc3fcf05e373d76d72aaa15fda9fb98625effa73c1d646f331fcecb7", size = 5355358, upload-time = "2025-11-17T22:32:19.7Z" }, + { url = "https://files.pythonhosted.org/packages/79/2b/a826ba18d2179a56e144aef69e57fb2ab7c464ef0b2111940ee8a3a223a2/ml_dtypes-0.5.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d2ffd05a2575b1519dc928c0b93c06339eb67173ff53acb00724502cda231cf", size = 5366332, upload-time = "2025-11-17T22:32:21.193Z" }, + { url = "https://files.pythonhosted.org/packages/84/44/f4d18446eacb20ea11e82f133ea8f86e2bf2891785b67d9da8d0ab0ef525/ml_dtypes-0.5.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4381fe2f2452a2d7589689693d3162e876b3ddb0a832cde7a414f8e1adf7eab1", size = 236612, upload-time = "2025-11-17T22:32:22.579Z" }, + { url = "https://files.pythonhosted.org/packages/ad/3f/3d42e9a78fe5edf792a83c074b13b9b770092a4fbf3462872f4303135f09/ml_dtypes-0.5.4-cp314-cp314t-win_arm64.whl", hash = "sha256:11942cbf2cf92157db91e5022633c0d9474d4dfd813a909383bd23ce828a4b7d", size = 168825, upload-time = "2025-11-17T22:32:23.766Z" }, +] + +[[package]] +name = "mmh3" +version = "5.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/91/1a/edb23803a168f070ded7a3014c6d706f63b90c84ccc024f89d794a3b7a6d/mmh3-5.2.1.tar.gz", hash = "sha256:bbea5b775f0ac84945191fb83f845a6fd9a21a03ea7f2e187defac7e401616ad", size = 33775, upload-time = "2026-03-05T15:55:57.716Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/b4/65bc1fb2bb7f83e91c30865023b1847cf89a5f237165575e8c83aa536584/mmh3-5.2.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:d771f085fcdf4035786adfb1d8db026df1eb4b41dac1c3d070d1e49512843227", size = 40794, upload-time = "2026-03-05T15:55:09.773Z" }, + { url = "https://files.pythonhosted.org/packages/c4/86/7168b3d83be8eb553897b1fac9da8bbb06568e5cfe555ffc329ebb46f59d/mmh3-5.2.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:7f196cd7910d71e9d9860da0ff7a77f64d22c1ad931f1dd18559a06e03109fc0", size = 41923, upload-time = "2026-03-05T15:55:10.924Z" }, + { url = "https://files.pythonhosted.org/packages/bf/9b/b653ab611c9060ce8ff0ba25c0226757755725e789292f3ca138a58082cd/mmh3-5.2.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b1f12bd684887a0a5d55e6363ca87056f361e45451105012d329b86ec19dbe0b", size = 39131, upload-time = "2026-03-05T15:55:11.961Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b4/5a2e0d34ab4d33543f01121e832395ea510132ea8e52cdf63926d9d81754/mmh3-5.2.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d106493a60dcb4aef35a0fac85105e150a11cf8bc2b0d388f5a33272d756c966", size = 39825, upload-time = "2026-03-05T15:55:13.013Z" }, + { url = "https://files.pythonhosted.org/packages/bd/69/81699a8f39a3f8d368bec6443435c0c392df0d200ad915bf0d222b588e03/mmh3-5.2.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:44983e45310ee5b9f73397350251cdf6e63a466406a105f1d16cb5baa659270b", size = 40344, upload-time = "2026-03-05T15:55:14.026Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b3/71c8c775807606e8fd8acc5c69016e1caf3200d50b50b6dd4b40ce10b76c/mmh3-5.2.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:368625fb01666655985391dbad3860dc0ba7c0d6b9125819f3121ee7292b4ac8", size = 56291, upload-time = "2026-03-05T15:55:15.137Z" }, + { url = "https://files.pythonhosted.org/packages/6f/75/2c24517d4b2ce9e4917362d24f274d3d541346af764430249ddcc4cb3a08/mmh3-5.2.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:72d1cc63bcc91e14933f77d51b3df899d6a07d184ec515ea7f56bff659e124d7", size = 40575, upload-time = "2026-03-05T15:55:16.518Z" }, + { url = "https://files.pythonhosted.org/packages/bf/b9/e4a360164365ac9f07a25f0f7928e3a66eb9ecc989384060747aa170e6aa/mmh3-5.2.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e8b4b5580280b9265af3e0409974fb79c64cf7523632d03fbf11df18f8b0181e", size = 40052, upload-time = "2026-03-05T15:55:17.735Z" }, + { url = "https://files.pythonhosted.org/packages/97/ca/120d92223a7546131bbbc31c9174168ee7a73b1366f5463ffe69d9e691fe/mmh3-5.2.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4cbbde66f1183db040daede83dd86c06d663c5bb2af6de1142b7c8c37923dd74", size = 97311, upload-time = "2026-03-05T15:55:18.959Z" }, + { url = "https://files.pythonhosted.org/packages/b6/71/c1a60c1652b8813ef9de6d289784847355417ee0f2980bca002fe87f4ae5/mmh3-5.2.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8ff038d52ef6aa0f309feeba00c5095c9118d0abf787e8e8454d6048db2037fc", size = 103279, upload-time = "2026-03-05T15:55:20.448Z" }, + { url = "https://files.pythonhosted.org/packages/48/29/ad97f4be1509cdcb28ae32c15593ce7c415db47ace37f8fad35b493faa9a/mmh3-5.2.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4130d0b9ce5fad6af07421b1aecc7e079519f70d6c05729ab871794eded8617", size = 106290, upload-time = "2026-03-05T15:55:21.6Z" }, + { url = "https://files.pythonhosted.org/packages/77/29/1f86d22e281bd8827ba373600a4a8b0c0eae5ca6aa55b9a8c26d2a34decc/mmh3-5.2.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6e0bfe77d238308839699944164b96a2eeccaf55f2af400f54dc20669d8d5f2", size = 113116, upload-time = "2026-03-05T15:55:22.826Z" }, + { url = "https://files.pythonhosted.org/packages/a7/7c/339971ea7ed4c12d98f421f13db3ea576a9114082ccb59d2d1a0f00ccac1/mmh3-5.2.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f963eafc0a77a6c0562397da004f5876a9bcf7265a7bcc3205e29636bc4a1312", size = 120740, upload-time = "2026-03-05T15:55:24.3Z" }, + { url = "https://files.pythonhosted.org/packages/e4/92/3c7c4bdb8e926bb3c972d1e2907d77960c1c4b250b41e8366cf20c6e4373/mmh3-5.2.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:92883836caf50d5255be03d988d75bc93e3f86ba247b7ca137347c323f731deb", size = 99143, upload-time = "2026-03-05T15:55:25.456Z" }, + { url = "https://files.pythonhosted.org/packages/df/0a/33dd8706e732458c8375eae63c981292de07a406bad4ec03e5269654aa2c/mmh3-5.2.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:57b52603e89355ff318025dd55158f6e71396c0f1f609d548e9ea9c94cc6ce0a", size = 98703, upload-time = "2026-03-05T15:55:26.723Z" }, + { url = "https://files.pythonhosted.org/packages/51/04/76bbce05df76cbc3d396f13b2ea5b1578ef02b6a5187e132c6c33f99d596/mmh3-5.2.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f40a95186a72fa0b67d15fef0f157bfcda00b4f59c8a07cbe5530d41ac35d105", size = 106484, upload-time = "2026-03-05T15:55:28.214Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8f/c6e204a2c70b719c1f62ffd9da27aef2dddcba875ea9c31ca0e87b975a46/mmh3-5.2.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:58370d05d033ee97224c81263af123dea3d931025030fd34b61227a768a8858a", size = 110012, upload-time = "2026-03-05T15:55:29.532Z" }, + { url = "https://files.pythonhosted.org/packages/e3/37/7181efd8e39db386c1ebc3e6b7d1f702a09d7c1197a6f2742ed6b5c16597/mmh3-5.2.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7be6dfb49e48fd0a7d91ff758a2b51336f1cd21f9d44b20f6801f072bd080cdd", size = 97508, upload-time = "2026-03-05T15:55:31.01Z" }, + { url = "https://files.pythonhosted.org/packages/42/0f/afa7ca2615fd85e1469474bb860e381443d0b868c083b62b41cb1d7ca32f/mmh3-5.2.1-cp314-cp314-win32.whl", hash = "sha256:54fe8518abe06a4c3852754bfd498b30cc58e667f376c513eac89a244ce781a4", size = 41387, upload-time = "2026-03-05T15:55:32.403Z" }, + { url = "https://files.pythonhosted.org/packages/71/0d/46d42a260ee1357db3d486e6c7a692e303c017968e14865e00efa10d09fc/mmh3-5.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:3f796b535008708846044c43302719c6956f39ca2d93f2edda5319e79a29efbb", size = 42101, upload-time = "2026-03-05T15:55:33.646Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7b/848a8378059d96501a41159fca90d6a99e89736b0afbe8e8edffeac8c74b/mmh3-5.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:cd471ede0d802dd936b6fab28188302b2d497f68436025857ca72cd3810423fe", size = 39836, upload-time = "2026-03-05T15:55:35.026Z" }, + { url = "https://files.pythonhosted.org/packages/27/61/1dabea76c011ba8547c25d30c91c0ec22544487a8750997a27a0c9e1180b/mmh3-5.2.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:5174a697ce042fa77c407e05efe41e03aa56dae9ec67388055820fb48cf4c3ba", size = 57727, upload-time = "2026-03-05T15:55:36.162Z" }, + { url = "https://files.pythonhosted.org/packages/b7/32/731185950d1cf2d5e28979cc8593016ba1619a295faba10dda664a4931b5/mmh3-5.2.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:0a3984146e414684a6be2862d84fcb1035f4984851cb81b26d933bab6119bf00", size = 41308, upload-time = "2026-03-05T15:55:37.254Z" }, + { url = "https://files.pythonhosted.org/packages/76/aa/66c76801c24b8c9418b4edde9b5e57c75e72c94e29c48f707e3962534f18/mmh3-5.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bd6e7d363aa93bd3421b30b6af97064daf47bc96005bddba67c5ffbc6df426b8", size = 40758, upload-time = "2026-03-05T15:55:38.61Z" }, + { url = "https://files.pythonhosted.org/packages/9e/bb/79a1f638a02f0ae389f706d13891e2fbf7d8c0a22ecde67ba828951bb60a/mmh3-5.2.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:113f78e7463a36dbbcea05bfe688efd7fa759d0f0c56e73c974d60dcfec3dfcc", size = 109670, upload-time = "2026-03-05T15:55:40.13Z" }, + { url = "https://files.pythonhosted.org/packages/26/94/8cd0e187a288985bcfc79bf5144d1d712df9dee74365f59d26e3a1865be6/mmh3-5.2.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7e8ec5f606e0809426d2440e0683509fb605a8820a21ebd120dcdba61b74ef7f", size = 117399, upload-time = "2026-03-05T15:55:42.076Z" }, + { url = "https://files.pythonhosted.org/packages/42/94/dfea6059bd5c5beda565f58a4096e43f4858fb6d2862806b8bbd12cbb284/mmh3-5.2.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22b0f9971ec4e07e8223f2beebe96a6cfc779d940b6f27d26604040dd74d3a44", size = 120386, upload-time = "2026-03-05T15:55:43.481Z" }, + { url = "https://files.pythonhosted.org/packages/47/cb/f9c45e62aaa67220179f487772461d891bb582bb2f9783c944832c60efd9/mmh3-5.2.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:85ffc9920ffc39c5eee1e3ac9100c913a0973996fbad5111f939bbda49204bb7", size = 125924, upload-time = "2026-03-05T15:55:44.638Z" }, + { url = "https://files.pythonhosted.org/packages/a5/83/fe54a4a7c11bc9f623dfc1707decd034245602b076dfc1dcc771a4163170/mmh3-5.2.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7aec798c2b01aaa65a55f1124f3405804184373abb318a3091325aece235f67c", size = 135280, upload-time = "2026-03-05T15:55:45.866Z" }, + { url = "https://files.pythonhosted.org/packages/97/67/fe7e9e9c143daddd210cd22aef89cbc425d58ecf238d2b7d9eb0da974105/mmh3-5.2.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:55dbbd8ffbc40d1697d5e2d0375b08599dae8746b0b08dea05eee4ce81648fac", size = 110050, upload-time = "2026-03-05T15:55:47.074Z" }, + { url = "https://files.pythonhosted.org/packages/43/c4/6d4b09fcbef80794de447c9378e39eefc047156b290fa3dd2d5257ca8227/mmh3-5.2.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:6c85c38a279ca9295a69b9b088a2e48aa49737bb1b34e6a9dc6297c110e8d912", size = 111158, upload-time = "2026-03-05T15:55:48.239Z" }, + { url = "https://files.pythonhosted.org/packages/81/a6/ca51c864bdb30524beb055a6d8826db3906af0834ec8c41d097a6e8573d5/mmh3-5.2.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:6290289fa5fb4c70fd7f72016e03633d60388185483ff3b162912c81205ae2cf", size = 116890, upload-time = "2026-03-05T15:55:49.405Z" }, + { url = "https://files.pythonhosted.org/packages/cc/04/5a1fe2e2ad843d03e89af25238cbc4f6840a8bb6c4329a98ab694c71deda/mmh3-5.2.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:4fc6cd65dc4d2fdb2625e288939a3566e36127a84811a4913f02f3d5931da52d", size = 123121, upload-time = "2026-03-05T15:55:50.61Z" }, + { url = "https://files.pythonhosted.org/packages/af/4d/3c820c6f4897afd25905270a9f2330a23f77a207ea7356f7aadace7273c0/mmh3-5.2.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:623f938f6a039536cc02b7582a07a080f13fdfd48f87e63201d92d7e34d09a18", size = 110187, upload-time = "2026-03-05T15:55:52.143Z" }, + { url = "https://files.pythonhosted.org/packages/21/54/1d71cd143752361c0aebef16ad3f55926a6faf7b112d355745c1f8a25f7f/mmh3-5.2.1-cp314-cp314t-win32.whl", hash = "sha256:29bc3973676ae334412efdd367fcd11d036b7be3efc1ce2407ef8676dabfeb82", size = 41934, upload-time = "2026-03-05T15:55:53.564Z" }, + { url = "https://files.pythonhosted.org/packages/9d/e4/63a2a88f31d93dea03947cccc2a076946857e799ea4f7acdecbf43b324aa/mmh3-5.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:28cfab66577000b9505a0d068c731aee7ca85cd26d4d63881fab17857e0fe1fb", size = 43036, upload-time = "2026-03-05T15:55:55.252Z" }, + { url = "https://files.pythonhosted.org/packages/a0/0f/59204bf136d1201f8d7884cfbaf7498c5b4674e87a4c693f9bde63741ce1/mmh3-5.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:dfd51b4c56b673dfbc43d7d27ef857dd91124801e2806c69bb45585ce0fa019b", size = 40391, upload-time = "2026-03-05T15:55:56.697Z" }, +] + +[[package]] +name = "mpmath" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, +] + +[[package]] +name = "multidict" +version = "6.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/cc/db74228a8be41884a567e88a62fd589a913708fcf180d029898c17a9a371/multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee", size = 75190, upload-time = "2026-01-26T02:45:10.651Z" }, + { url = "https://files.pythonhosted.org/packages/d5/22/492f2246bb5b534abd44804292e81eeaf835388901f0c574bac4eeec73c5/multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2", size = 44486, upload-time = "2026-01-26T02:45:11.938Z" }, + { url = "https://files.pythonhosted.org/packages/f1/4f/733c48f270565d78b4544f2baddc2fb2a245e5a8640254b12c36ac7ac68e/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1", size = 43219, upload-time = "2026-01-26T02:45:14.346Z" }, + { url = "https://files.pythonhosted.org/packages/24/bb/2c0c2287963f4259c85e8bcbba9182ced8d7fca65c780c38e99e61629d11/multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d", size = 245132, upload-time = "2026-01-26T02:45:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f9/44d4b3064c65079d2467888794dea218d1601898ac50222ab8a9a8094460/multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31", size = 252420, upload-time = "2026-01-26T02:45:17.293Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/78f7275e73fa17b24c9a51b0bd9d73ba64bb32d0ed51b02a746eb876abe7/multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048", size = 233510, upload-time = "2026-01-26T02:45:19.356Z" }, + { url = "https://files.pythonhosted.org/packages/4b/25/8167187f62ae3cbd52da7893f58cb036b47ea3fb67138787c76800158982/multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362", size = 264094, upload-time = "2026-01-26T02:45:20.834Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e7/69a3a83b7b030cf283fb06ce074a05a02322359783424d7edf0f15fe5022/multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37", size = 260786, upload-time = "2026-01-26T02:45:22.818Z" }, + { url = "https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709", size = 248483, upload-time = "2026-01-26T02:45:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/48/5a/d5a99e3acbca0e29c5d9cba8f92ceb15dce78bab963b308ae692981e3a5d/multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0", size = 248403, upload-time = "2026-01-26T02:45:25.982Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/e58cd31f6c7d5102f2a4bf89f96b9cf7e00b6c6f3d04ecc44417c00a5a3c/multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb", size = 240315, upload-time = "2026-01-26T02:45:27.487Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/1cd210229559cb90b6786c30676bb0c58249ff42f942765f88793b41fdce/multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd", size = 245528, upload-time = "2026-01-26T02:45:28.991Z" }, + { url = "https://files.pythonhosted.org/packages/64/f2/6e1107d226278c876c783056b7db43d800bb64c6131cec9c8dfb6903698e/multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601", size = 258784, upload-time = "2026-01-26T02:45:30.503Z" }, + { url = "https://files.pythonhosted.org/packages/4d/c1/11f664f14d525e4a1b5327a82d4de61a1db604ab34c6603bb3c2cc63ad34/multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1", size = 251980, upload-time = "2026-01-26T02:45:32.603Z" }, + { url = "https://files.pythonhosted.org/packages/e1/9f/75a9ac888121d0c5bbd4ecf4eead45668b1766f6baabfb3b7f66a410e231/multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b", size = 243602, upload-time = "2026-01-26T02:45:34.043Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e7/50bf7b004cc8525d80dbbbedfdc7aed3e4c323810890be4413e589074032/multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d", size = 40930, upload-time = "2026-01-26T02:45:36.278Z" }, + { url = "https://files.pythonhosted.org/packages/e0/bf/52f25716bbe93745595800f36fb17b73711f14da59ed0bb2eba141bc9f0f/multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f", size = 45074, upload-time = "2026-01-26T02:45:37.546Z" }, + { url = "https://files.pythonhosted.org/packages/97/ab/22803b03285fa3a525f48217963da3a65ae40f6a1b6f6cf2768879e208f9/multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5", size = 42471, upload-time = "2026-01-26T02:45:38.889Z" }, + { url = "https://files.pythonhosted.org/packages/e0/6d/f9293baa6146ba9507e360ea0292b6422b016907c393e2f63fc40ab7b7b5/multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581", size = 82401, upload-time = "2026-01-26T02:45:40.254Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/53b5494738d83558d87c3c71a486504d8373421c3e0dbb6d0db48ad42ee0/multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a", size = 48143, upload-time = "2026-01-26T02:45:41.635Z" }, + { url = "https://files.pythonhosted.org/packages/37/e8/5284c53310dcdc99ce5d66563f6e5773531a9b9fe9ec7a615e9bc306b05f/multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c", size = 46507, upload-time = "2026-01-26T02:45:42.99Z" }, + { url = "https://files.pythonhosted.org/packages/e4/fc/6800d0e5b3875568b4083ecf5f310dcf91d86d52573160834fb4bfcf5e4f/multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262", size = 239358, upload-time = "2026-01-26T02:45:44.376Z" }, + { url = "https://files.pythonhosted.org/packages/41/75/4ad0973179361cdf3a113905e6e088173198349131be2b390f9fa4da5fc6/multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59", size = 246884, upload-time = "2026-01-26T02:45:47.167Z" }, + { url = "https://files.pythonhosted.org/packages/c3/9c/095bb28b5da139bd41fb9a5d5caff412584f377914bd8787c2aa98717130/multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889", size = 225878, upload-time = "2026-01-26T02:45:48.698Z" }, + { url = "https://files.pythonhosted.org/packages/07/d0/c0a72000243756e8f5a277b6b514fa005f2c73d481b7d9e47cd4568aa2e4/multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4", size = 253542, upload-time = "2026-01-26T02:45:50.164Z" }, + { url = "https://files.pythonhosted.org/packages/c0/6b/f69da15289e384ecf2a68837ec8b5ad8c33e973aa18b266f50fe55f24b8c/multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d", size = 252403, upload-time = "2026-01-26T02:45:51.779Z" }, + { url = "https://files.pythonhosted.org/packages/a2/76/b9669547afa5a1a25cd93eaca91c0da1c095b06b6d2d8ec25b713588d3a1/multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609", size = 244889, upload-time = "2026-01-26T02:45:53.27Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a9/a50d2669e506dad33cfc45b5d574a205587b7b8a5f426f2fbb2e90882588/multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489", size = 241982, upload-time = "2026-01-26T02:45:54.919Z" }, + { url = "https://files.pythonhosted.org/packages/c5/bb/1609558ad8b456b4827d3c5a5b775c93b87878fd3117ed3db3423dfbce1b/multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c", size = 232415, upload-time = "2026-01-26T02:45:56.981Z" }, + { url = "https://files.pythonhosted.org/packages/d8/59/6f61039d2aa9261871e03ab9dc058a550d240f25859b05b67fd70f80d4b3/multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e", size = 240337, upload-time = "2026-01-26T02:45:58.698Z" }, + { url = "https://files.pythonhosted.org/packages/a1/29/fdc6a43c203890dc2ae9249971ecd0c41deaedfe00d25cb6564b2edd99eb/multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c", size = 248788, upload-time = "2026-01-26T02:46:00.862Z" }, + { url = "https://files.pythonhosted.org/packages/a9/14/a153a06101323e4cf086ecee3faadba52ff71633d471f9685c42e3736163/multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9", size = 242842, upload-time = "2026-01-26T02:46:02.824Z" }, + { url = "https://files.pythonhosted.org/packages/41/5f/604ae839e64a4a6efc80db94465348d3b328ee955e37acb24badbcd24d83/multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2", size = 240237, upload-time = "2026-01-26T02:46:05.898Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/c3a5187bf66f6fb546ff4ab8fb5a077cbdd832d7b1908d4365c7f74a1917/multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7", size = 48008, upload-time = "2026-01-26T02:46:07.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f7/addf1087b860ac60e6f382240f64fb99f8bfb532bb06f7c542b83c29ca61/multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5", size = 53542, upload-time = "2026-01-26T02:46:08.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/81/4629d0aa32302ef7b2ec65c75a728cc5ff4fa410c50096174c1632e70b3e/multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2", size = 44719, upload-time = "2026-01-26T02:46:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "networkx" +version = "3.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, +] + +[[package]] +name = "numpy" +version = "2.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/9f/b8cef5bffa569759033adda9481211426f12f53299629b410340795c2514/numpy-2.4.4.tar.gz", hash = "sha256:2d390634c5182175533585cc89f3608a4682ccb173cc9bb940b2881c8d6f8fa0", size = 20731587, upload-time = "2026-03-29T13:22:01.298Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/06/c54062f85f673dd5c04cbe2f14c3acb8c8b95e3384869bb8cc9bff8cb9df/numpy-2.4.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f169b9a863d34f5d11b8698ead99febeaa17a13ca044961aa8e2662a6c7766a0", size = 16684353, upload-time = "2026-03-29T13:20:29.504Z" }, + { url = "https://files.pythonhosted.org/packages/4c/39/8a320264a84404c74cc7e79715de85d6130fa07a0898f67fb5cd5bd79908/numpy-2.4.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2483e4584a1cb3092da4470b38866634bafb223cbcd551ee047633fd2584599a", size = 14704914, upload-time = "2026-03-29T13:20:33.547Z" }, + { url = "https://files.pythonhosted.org/packages/91/fb/287076b2614e1d1044235f50f03748f31fa287e3dbe6abeb35cdfa351eca/numpy-2.4.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:2d19e6e2095506d1736b7d80595e0f252d76b89f5e715c35e06e937679ea7d7a", size = 5210005, upload-time = "2026-03-29T13:20:36.45Z" }, + { url = "https://files.pythonhosted.org/packages/63/eb/fcc338595309910de6ecabfcef2419a9ce24399680bfb149421fa2df1280/numpy-2.4.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:6a246d5914aa1c820c9443ddcee9c02bec3e203b0c080349533fae17727dfd1b", size = 6544974, upload-time = "2026-03-29T13:20:39.014Z" }, + { url = "https://files.pythonhosted.org/packages/44/5d/e7e9044032a716cdfaa3fba27a8e874bf1c5f1912a1ddd4ed071bf8a14a6/numpy-2.4.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:989824e9faf85f96ec9c7761cd8d29c531ad857bfa1daa930cba85baaecf1a9a", size = 15684591, upload-time = "2026-03-29T13:20:42.146Z" }, + { url = "https://files.pythonhosted.org/packages/98/7c/21252050676612625449b4807d6b695b9ce8a7c9e1c197ee6216c8a65c7c/numpy-2.4.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27a8d92cd10f1382a67d7cf4db7ce18341b66438bdd9f691d7b0e48d104c2a9d", size = 16637700, upload-time = "2026-03-29T13:20:46.204Z" }, + { url = "https://files.pythonhosted.org/packages/b1/29/56d2bbef9465db24ef25393383d761a1af4f446a1df9b8cded4fe3a5a5d7/numpy-2.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e44319a2953c738205bf3354537979eaa3998ed673395b964c1176083dd46252", size = 17035781, upload-time = "2026-03-29T13:20:50.242Z" }, + { url = "https://files.pythonhosted.org/packages/e3/2b/a35a6d7589d21f44cea7d0a98de5ddcbb3d421b2622a5c96b1edf18707c3/numpy-2.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e892aff75639bbef0d2a2cfd55535510df26ff92f63c92cd84ef8d4ba5a5557f", size = 18362959, upload-time = "2026-03-29T13:20:54.019Z" }, + { url = "https://files.pythonhosted.org/packages/64/c9/d52ec581f2390e0f5f85cbfd80fb83d965fc15e9f0e1aec2195faa142cde/numpy-2.4.4-cp314-cp314-win32.whl", hash = "sha256:1378871da56ca8943c2ba674530924bb8ca40cd228358a3b5f302ad60cf875fc", size = 6008768, upload-time = "2026-03-29T13:20:56.912Z" }, + { url = "https://files.pythonhosted.org/packages/fa/22/4cc31a62a6c7b74a8730e31a4274c5dc80e005751e277a2ce38e675e4923/numpy-2.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:715d1c092715954784bc79e1174fc2a90093dc4dc84ea15eb14dad8abdcdeb74", size = 12449181, upload-time = "2026-03-29T13:20:59.548Z" }, + { url = "https://files.pythonhosted.org/packages/70/2e/14cda6f4d8e396c612d1bf97f22958e92148801d7e4f110cabebdc0eef4b/numpy-2.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:2c194dd721e54ecad9ad387c1d35e63dce5c4450c6dc7dd5611283dda239aabb", size = 10496035, upload-time = "2026-03-29T13:21:02.524Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e8/8fed8c8d848d7ecea092dc3469643f9d10bc3a134a815a3b033da1d2039b/numpy-2.4.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2aa0613a5177c264ff5921051a5719d20095ea586ca88cc802c5c218d1c67d3e", size = 14824958, upload-time = "2026-03-29T13:21:05.671Z" }, + { url = "https://files.pythonhosted.org/packages/05/1a/d8007a5138c179c2bf33ef44503e83d70434d2642877ee8fbb230e7c0548/numpy-2.4.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:42c16925aa5a02362f986765f9ebabf20de75cdefdca827d14315c568dcab113", size = 5330020, upload-time = "2026-03-29T13:21:08.635Z" }, + { url = "https://files.pythonhosted.org/packages/99/64/ffb99ac6ae93faf117bcbd5c7ba48a7f45364a33e8e458545d3633615dda/numpy-2.4.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:874f200b2a981c647340f841730fc3a2b54c9d940566a3c4149099591e2c4c3d", size = 6650758, upload-time = "2026-03-29T13:21:10.949Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6e/795cc078b78a384052e73b2f6281ff7a700e9bf53bcce2ee579d4f6dd879/numpy-2.4.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9b39d38a9bd2ae1becd7eac1303d031c5c110ad31f2b319c6e7d98b135c934d", size = 15729948, upload-time = "2026-03-29T13:21:14.047Z" }, + { url = "https://files.pythonhosted.org/packages/5f/86/2acbda8cc2af5f3d7bfc791192863b9e3e19674da7b5e533fded124d1299/numpy-2.4.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b268594bccac7d7cf5844c7732e3f20c50921d94e36d7ec9b79e9857694b1b2f", size = 16679325, upload-time = "2026-03-29T13:21:17.561Z" }, + { url = "https://files.pythonhosted.org/packages/bc/59/cafd83018f4aa55e0ac6fa92aa066c0a1877b77a615ceff1711c260ffae8/numpy-2.4.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac6b31e35612a26483e20750126d30d0941f949426974cace8e6b5c58a3657b0", size = 17084883, upload-time = "2026-03-29T13:21:21.106Z" }, + { url = "https://files.pythonhosted.org/packages/f0/85/a42548db84e65ece46ab2caea3d3f78b416a47af387fcbb47ec28e660dc2/numpy-2.4.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8e3ed142f2728df44263aaf5fb1f5b0b99f4070c553a0d7f033be65338329150", size = 18403474, upload-time = "2026-03-29T13:21:24.828Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ad/483d9e262f4b831000062e5d8a45e342166ec8aaa1195264982bca267e62/numpy-2.4.4-cp314-cp314t-win32.whl", hash = "sha256:dddbbd259598d7240b18c9d87c56a9d2fb3b02fe266f49a7c101532e78c1d871", size = 6155500, upload-time = "2026-03-29T13:21:28.205Z" }, + { url = "https://files.pythonhosted.org/packages/c7/03/2fc4e14c7bd4ff2964b74ba90ecb8552540b6315f201df70f137faa5c589/numpy-2.4.4-cp314-cp314t-win_amd64.whl", hash = "sha256:a7164afb23be6e37ad90b2f10426149fd75aee07ca55653d2aa41e66c4ef697e", size = 12637755, upload-time = "2026-03-29T13:21:31.107Z" }, + { url = "https://files.pythonhosted.org/packages/58/78/548fb8e07b1a341746bfbecb32f2c268470f45fa028aacdbd10d9bc73aab/numpy-2.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:ba203255017337d39f89bdd58417f03c4426f12beed0440cfd933cb15f8669c7", size = 10566643, upload-time = "2026-03-29T13:21:34.339Z" }, +] + +[[package]] +name = "nvidia-cublas" +version = "13.1.0.3" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/a5/fce49e2ae977e0ccc084e5adafceb4f0ac0c8333cb6863501618a7277f67/nvidia_cublas-13.1.0.3-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:c86fc7f7ae36d7528288c5d88098edcb7b02c633d262e7ddbb86b0ad91be5df2", size = 542851226, upload-time = "2025-10-09T08:59:04.818Z" }, + { url = "https://files.pythonhosted.org/packages/e7/44/423ac00af4dd95a5aeb27207e2c0d9b7118702149bf4704c3ddb55bb7429/nvidia_cublas-13.1.0.3-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:ee8722c1f0145ab246bccb9e452153b5e0515fd094c3678df50b2a0888b8b171", size = 423133236, upload-time = "2025-10-09T08:59:32.536Z" }, +] + +[[package]] +name = "nvidia-cuda-cupti" +version = "13.0.85" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/2a/80353b103fc20ce05ef51e928daed4b6015db4aaa9162ed0997090fe2250/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_aarch64.whl", hash = "sha256:796bd679890ee55fb14a94629b698b6db54bcfd833d391d5e94017dd9d7d3151", size = 10310827, upload-time = "2025-09-04T08:26:42.012Z" }, + { url = "https://files.pythonhosted.org/packages/33/6d/737d164b4837a9bbd202f5ae3078975f0525a55730fe871d8ed4e3b952b0/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_x86_64.whl", hash = "sha256:4eb01c08e859bf924d222250d2e8f8b8ff6d3db4721288cf35d14252a4d933c8", size = 10715597, upload-time = "2025-09-04T08:26:51.312Z" }, +] + +[[package]] +name = "nvidia-cuda-nvrtc" +version = "13.0.88" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/68/483a78f5e8f31b08fb1bb671559968c0ca3a065ac7acabfc7cee55214fd6/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:ad9b6d2ead2435f11cbb6868809d2adeeee302e9bb94bcf0539c7a40d80e8575", size = 90215200, upload-time = "2025-09-04T08:28:44.204Z" }, + { url = "https://files.pythonhosted.org/packages/b7/dc/6bb80850e0b7edd6588d560758f17e0550893a1feaf436807d64d2da040f/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d27f20a0ca67a4bb34268a5e951033496c5b74870b868bacd046b1b8e0c3267b", size = 43015449, upload-time = "2025-09-04T08:28:20.239Z" }, +] + +[[package]] +name = "nvidia-cuda-runtime" +version = "13.0.96" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/4f/17d7b9b8e285199c58ce28e31b5c5bbaa4d8271af06a89b6405258245de2/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ef9bcbe90493a2b9d810e43d249adb3d02e98dd30200d86607d8d02687c43f55", size = 2261060, upload-time = "2025-10-09T08:55:15.78Z" }, + { url = "https://files.pythonhosted.org/packages/2e/24/d1558f3b68b1d26e706813b1d10aa1d785e4698c425af8db8edc3dced472/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f82250d7782aa23b6cfe765ecc7db554bd3c2870c43f3d1821f1d18aebf0548", size = 2243632, upload-time = "2025-10-09T08:55:36.117Z" }, +] + +[[package]] +name = "nvidia-cudnn-cu13" +version = "9.19.0.56" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/84/26025437c1e6b61a707442184fa0c03d083b661adf3a3eecfd6d21677740/nvidia_cudnn_cu13-9.19.0.56-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:6ed29ffaee1176c612daf442e4dd6cfeb6a0caa43ddcbeb59da94953030b1be4", size = 433781201, upload-time = "2026-02-03T20:40:53.805Z" }, + { url = "https://files.pythonhosted.org/packages/a3/22/0b4b932655d17a6da1b92fa92ab12844b053bb2ac2475e179ba6f043da1e/nvidia_cudnn_cu13-9.19.0.56-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:d20e1734305e9d68889a96e3f35094d733ff1f83932ebe462753973e53a572bf", size = 366066321, upload-time = "2026-02-03T20:44:52.837Z" }, +] + +[[package]] +name = "nvidia-cufft" +version = "12.0.0.61" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/ae/f417a75c0259e85c1d2f83ca4e960289a5f814ed0cea74d18c353d3e989d/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2708c852ef8cd89d1d2068bdbece0aa188813a0c934db3779b9b1faa8442e5f5", size = 214053554, upload-time = "2025-09-04T08:31:38.196Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2f/7b57e29836ea8714f81e9898409196f47d772d5ddedddf1592eadb8ab743/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6c44f692dce8fd5ffd3e3df134b6cdb9c2f72d99cf40b62c32dde45eea9ddad3", size = 214085489, upload-time = "2025-09-04T08:31:56.044Z" }, +] + +[[package]] +name = "nvidia-cufile" +version = "1.15.1.6" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/70/4f193de89a48b71714e74602ee14d04e4019ad36a5a9f20c425776e72cd6/nvidia_cufile-1.15.1.6-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:08a3ecefae5a01c7f5117351c64f17c7c62efa5fffdbe24fc7d298da19cd0b44", size = 1223672, upload-time = "2025-09-04T08:32:22.779Z" }, + { url = "https://files.pythonhosted.org/packages/ab/73/cc4a14c9813a8a0d509417cf5f4bdaba76e924d58beb9864f5a7baceefbf/nvidia_cufile-1.15.1.6-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:bdc0deedc61f548bddf7733bdc216456c2fdb101d020e1ab4b88d232d5e2f6d1", size = 1136992, upload-time = "2025-09-04T08:32:14.119Z" }, +] + +[[package]] +name = "nvidia-curand" +version = "10.4.0.35" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/72/7c2ae24fb6b63a32e6ae5d241cc65263ea18d08802aaae087d9f013335a2/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:133df5a7509c3e292aaa2b477afd0194f06ce4ea24d714d616ff36439cee349a", size = 61962106, upload-time = "2025-08-04T10:21:41.128Z" }, + { url = "https://files.pythonhosted.org/packages/a5/9f/be0a41ca4a4917abf5cb9ae0daff1a6060cc5de950aec0396de9f3b52bc5/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:1aee33a5da6e1db083fe2b90082def8915f30f3248d5896bcec36a579d941bfc", size = 59544258, upload-time = "2025-08-04T10:22:03.992Z" }, +] + +[[package]] +name = "nvidia-cusolver" +version = "12.0.4.66" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "nvidia-cusparse", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "nvidia-nvjitlink", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/c3/b30c9e935fc01e3da443ec0116ed1b2a009bb867f5324d3f2d7e533e776b/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:02c2457eaa9e39de20f880f4bd8820e6a1cfb9f9a34f820eb12a155aa5bc92d2", size = 223467760, upload-time = "2025-09-04T08:33:04.222Z" }, + { url = "https://files.pythonhosted.org/packages/5f/67/cba3777620cdacb99102da4042883709c41c709f4b6323c10781a9c3aa34/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:0a759da5dea5c0ea10fd307de75cdeb59e7ea4fcb8add0924859b944babf1112", size = 200941980, upload-time = "2025-09-04T08:33:22.767Z" }, +] + +[[package]] +name = "nvidia-cusparse" +version = "12.6.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/94/5c26f33738ae35276672f12615a64bd008ed5be6d1ebcb23579285d960a9/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:80bcc4662f23f1054ee334a15c72b8940402975e0eab63178fc7e670aa59472c", size = 162155568, upload-time = "2025-09-04T08:33:42.864Z" }, + { url = "https://files.pythonhosted.org/packages/fa/18/623c77619c31d62efd55302939756966f3ecc8d724a14dab2b75f1508850/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b3c89c88d01ee0e477cb7f82ef60a11a4bcd57b6b87c33f789350b59759360b", size = 145942937, upload-time = "2025-09-04T08:33:58.029Z" }, +] + +[[package]] +name = "nvidia-cusparselt-cu13" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/10/8dcd1175260706a2fc92a16a52e306b71d4c1ea0b0cc4a9484183399818a/nvidia_cusparselt_cu13-0.8.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:400c6ed1cf6780fc6efedd64ec9f1345871767e6a1a0a552a1ea0578117ea77c", size = 220791277, upload-time = "2025-08-13T19:22:40.982Z" }, + { url = "https://files.pythonhosted.org/packages/fd/53/43b0d71f4e702fa9733f8b4571fdca50a8813f1e450b656c239beff12315/nvidia_cusparselt_cu13-0.8.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:25e30a8a7323935d4ad0340b95a0b69926eee755767e8e0b1cf8dd85b197d3fd", size = 169884119, upload-time = "2025-08-13T19:23:41.967Z" }, +] + +[[package]] +name = "nvidia-nccl-cu12" +version = "2.30.4" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/2b/1757b6b74ee241de5efee3f35487dcb33e09c07605254809c6ce36aeb783/nvidia_nccl_cu12-2.30.4-py3-none-manylinux_2_18_aarch64.whl", hash = "sha256:606fa9aa9215c00367d060188eb1a5bbd28396aff5e11b9200d99d1a6ab79a71", size = 300091935, upload-time = "2026-04-23T03:22:58.024Z" }, + { url = "https://files.pythonhosted.org/packages/6b/c3/0e45ff4dce8401f6ea7c25d80d75738813a47f5ae2691e2478f2fd1e5e93/nvidia_nccl_cu12-2.30.4-py3-none-manylinux_2_18_x86_64.whl", hash = "sha256:040974b261edec4b8b793e59e92ab7176fe4ab4bc61b800f9f3bfaeec2d436f3", size = 300164158, upload-time = "2026-04-23T03:23:19.589Z" }, +] + +[[package]] +name = "nvidia-nccl-cu13" +version = "2.28.9" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/55/1920646a2e43ffd4fc958536b276197ed740e9e0c54105b4bb3521591fc7/nvidia_nccl_cu13-2.28.9-py3-none-manylinux_2_18_aarch64.whl", hash = "sha256:01c873ba1626b54caa12272ed228dc5b2781545e0ae8ba3f432a8ef1c6d78643", size = 196561677, upload-time = "2025-11-18T05:49:03.45Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b4/878fefaad5b2bcc6fcf8d474a25e3e3774bc5133e4b58adff4d0bca238bc/nvidia_nccl_cu13-2.28.9-py3-none-manylinux_2_18_x86_64.whl", hash = "sha256:e4553a30f34195f3fa1da02a6da3d6337d28f2003943aa0a3d247bbc25fefc42", size = 196493177, upload-time = "2025-11-18T05:49:17.677Z" }, +] + +[[package]] +name = "nvidia-nvjitlink" +version = "13.0.88" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/7a/123e033aaff487c77107195fa5a2b8686795ca537935a24efae476c41f05/nvidia_nvjitlink-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:13a74f429e23b921c1109976abefacc69835f2f433ebd323d3946e11d804e47b", size = 40713933, upload-time = "2025-09-04T08:35:43.553Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2c/93c5250e64df4f894f1cbb397c6fd71f79813f9fd79d7cd61de3f97b3c2d/nvidia_nvjitlink-13.0.88-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e931536ccc7d467a98ba1d8b89ff7fa7f1fa3b13f2b0069118cd7f47bff07d0c", size = 38768748, upload-time = "2025-09-04T08:35:20.008Z" }, +] + +[[package]] +name = "nvidia-nvshmem-cu13" +version = "3.4.5" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/0f/05cc9c720236dcd2db9c1ab97fff629e96821be2e63103569da0c9b72f19/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dc2a197f38e5d0376ad52cd1a2a3617d3cdc150fd5966f4aee9bcebb1d68fe9", size = 60215947, upload-time = "2025-09-06T00:32:20.022Z" }, + { url = "https://files.pythonhosted.org/packages/3c/35/a9bf80a609e74e3b000fef598933235c908fcefcef9026042b8e6dfde2a9/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:290f0a2ee94c9f3687a02502f3b9299a9f9fe826e6d0287ee18482e78d495b80", size = 60412546, upload-time = "2025-09-06T00:32:41.564Z" }, +] + +[[package]] +name = "nvidia-nvtx" +version = "13.0.85" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/f3/d86c845465a2723ad7e1e5c36dcd75ddb82898b3f53be47ebd429fb2fa5d/nvidia_nvtx-13.0.85-py3-none-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4936d1d6780fbe68db454f5e72a42ff64d1fd6397df9f363ae786930fd5c1cd4", size = 148047, upload-time = "2025-09-04T08:29:01.761Z" }, + { url = "https://files.pythonhosted.org/packages/a8/64/3708a90d1ebe202ffdeb7185f878a3c84d15c2b2c31858da2ce0583e2def/nvidia_nvtx-13.0.85-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb7780edb6b14107373c835bf8b72e7a178bac7367e23da7acb108f973f157a6", size = 148878, upload-time = "2025-09-04T08:28:53.627Z" }, +] + +[[package]] +name = "oauthlib" +version = "3.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/5f/19930f824ffeb0ad4372da4812c50edbd1434f678c90c2733e1188edfc63/oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9", size = 185918, upload-time = "2025-06-19T22:48:08.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1", size = 160065, upload-time = "2025-06-19T22:48:06.508Z" }, +] + +[[package]] +name = "onnx" +version = "1.21.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ml-dtypes" }, + { name = "numpy" }, + { name = "protobuf" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c5/93/942d2a0f6a70538eea042ce0445c8aefd46559ad153469986f29a743c01c/onnx-1.21.0.tar.gz", hash = "sha256:4d8b67d0aaec5864c87633188b91cc520877477ec0254eda122bef8be43cd764", size = 12074608, upload-time = "2026-03-27T21:33:36.118Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/ae/cb644ec84c25e63575d9d8790fdcc5d1a11d67d3f62f872edb35fa38d158/onnx-1.21.0-cp312-abi3-macosx_12_0_universal2.whl", hash = "sha256:fc2635400fe39ff37ebc4e75342cc54450eadadf39c540ff132c319bf4960095", size = 17965930, upload-time = "2026-03-27T21:32:48.089Z" }, + { url = "https://files.pythonhosted.org/packages/6f/b6/eeb5903586645ef8a49b4b7892580438741acc3df91d7a5bd0f3a59ea9cb/onnx-1.21.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9003d5206c01fa2ff4b46311566865d8e493e1a6998d4009ec6de39843f1b59b", size = 17531344, upload-time = "2026-03-27T21:32:50.837Z" }, + { url = "https://files.pythonhosted.org/packages/a7/00/4823f06357892d1e60d6f34e7299d2ba4ed2108c487cc394f7ce85a3ff14/onnx-1.21.0-cp312-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9261bd580fb8548c9c37b3c6750387eb8f21ea43c63880d37b2c622e1684285", size = 17613697, upload-time = "2026-03-27T21:32:54.222Z" }, + { url = "https://files.pythonhosted.org/packages/23/1d/391f3c567ae068c8ac4f1d1316bae97c9eb45e702f05975fe0e17ad441f0/onnx-1.21.0-cp312-abi3-win32.whl", hash = "sha256:9ea4e824964082811938a9250451d89c4ec474fe42dd36c038bfa5df31993d1e", size = 16287200, upload-time = "2026-03-27T21:32:57.277Z" }, + { url = "https://files.pythonhosted.org/packages/9c/a6/5eefbe5b40ea96de95a766bd2e0e751f35bdea2d4b951991ec9afaa69531/onnx-1.21.0-cp312-abi3-win_amd64.whl", hash = "sha256:458d91948ad9a7729a347550553b49ab6939f9af2cddf334e2116e45467dc61f", size = 16441045, upload-time = "2026-03-27T21:33:00.081Z" }, + { url = "https://files.pythonhosted.org/packages/63/c4/0ed8dc037a39113d2a4d66e0005e07751c299c46b993f1ad5c2c35664c20/onnx-1.21.0-cp312-abi3-win_arm64.whl", hash = "sha256:ca14bc4842fccc3187eb538f07eabeb25a779b39388b006db4356c07403a7bbb", size = 16403134, upload-time = "2026-03-27T21:33:03.987Z" }, + { url = "https://files.pythonhosted.org/packages/bf/0a/8905b14694def6ad23edf1011fdd581500384062f8c4c567e114be7aa272/onnx-1.21.0-cp314-cp314t-macosx_12_0_universal2.whl", hash = "sha256:7ee9d8fd6a4874a5fa8b44bbcabea104ce752b20469b88bc50c7dcf9030779ad", size = 17975331, upload-time = "2026-03-27T21:33:21.69Z" }, + { url = "https://files.pythonhosted.org/packages/61/28/f4e401e5199d1b9c8b76c7e7ae1169e050515258e877b58fa8bb49d3bdcc/onnx-1.21.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5489f25fe461e7f32128218251a466cabbeeaf1eaa791c79daebf1a80d5a2cc9", size = 17537430, upload-time = "2026-03-27T21:33:24.547Z" }, + { url = "https://files.pythonhosted.org/packages/cf/cf/5d13320eb3660d5af360ea3b43aa9c63a70c92a9b4d1ea0d34501a32fcb8/onnx-1.21.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:db17fc0fec46180b6acbd1d5d8650a04e5527c02b09381da0b5b888d02a204c8", size = 17617662, upload-time = "2026-03-27T21:33:27.418Z" }, + { url = "https://files.pythonhosted.org/packages/4d/50/3eaa1878338247be021e6423696813d61e77e534dccbd15a703a144e703d/onnx-1.21.0-cp314-cp314t-win_amd64.whl", hash = "sha256:19d9971a3e52a12968ae6c70fd0f86c349536de0b0c33922ecdbe52d1972fe60", size = 16463688, upload-time = "2026-03-27T21:33:30.229Z" }, + { url = "https://files.pythonhosted.org/packages/a7/48/38d46b43bbb525e0b6a4c2c4204cc6795d67e45687a2f7403e06d8e7053d/onnx-1.21.0-cp314-cp314t-win_arm64.whl", hash = "sha256:efba467efb316baf2a9452d892c2f982b9b758c778d23e38c7f44fa211b30bb9", size = 16423387, upload-time = "2026-03-27T21:33:33.446Z" }, +] + +[[package]] +name = "onnxruntime" +version = "1.24.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "flatbuffers" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "protobuf" }, + { name = "sympy" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/db/b30dbbd6037847b205ab75d962bc349bf1e46d02a65b30d7047a6893ffd6/onnxruntime-1.24.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:fbff2a248940e3398ae78374c5a839e49a2f39079b488bc64439fa0ec327a3e4", size = 17343300, upload-time = "2026-03-17T22:03:59.223Z" }, + { url = "https://files.pythonhosted.org/packages/61/88/1746c0e7959961475b84c776d35601a21d445f463c93b1433a409ec3e188/onnxruntime-1.24.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e2b7969e72d8cb53ffc88ab6d49dd5e75c1c663bda7be7eb0ece192f127343d1", size = 15175936, upload-time = "2026-03-17T22:03:43.671Z" }, + { url = "https://files.pythonhosted.org/packages/5f/ba/4699cde04a52cece66cbebc85bd8335a0d3b9ad485abc9a2e15946a1349d/onnxruntime-1.24.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14ed1f197fab812b695a5eaddb536c635e58a2fbbe50a517c78f082cc6ce9177", size = 17246432, upload-time = "2026-03-17T22:04:49.58Z" }, + { url = "https://files.pythonhosted.org/packages/ef/60/4590910841bb28bd3b4b388a9efbedf4e2d2cca99ddf0c863642b4e87814/onnxruntime-1.24.4-cp314-cp314-win_amd64.whl", hash = "sha256:311e309f573bf3c12aa5723e23823077f83d5e412a18499d4485c7eb41040858", size = 12903276, upload-time = "2026-03-17T22:05:46.349Z" }, + { url = "https://files.pythonhosted.org/packages/7f/6f/60e2c0acea1e1ac09b3e794b5a19c166eebf91c0b860b3e6db8e74983fda/onnxruntime-1.24.4-cp314-cp314-win_arm64.whl", hash = "sha256:3f0b910e86b759a4732663ec61fd57ac42ee1b0066f68299de164220b660546d", size = 12594365, upload-time = "2026-03-17T22:05:35.795Z" }, + { url = "https://files.pythonhosted.org/packages/cf/68/0c05d10f8f6c40fe0912ebec0d5a33884aaa2af2053507e864dab0883208/onnxruntime-1.24.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa12ddc54c9c4594073abcaa265cd9681e95fb89dae982a6f508a794ca42e661", size = 15176889, upload-time = "2026-03-17T22:03:48.021Z" }, + { url = "https://files.pythonhosted.org/packages/6c/1d/1666dc64e78d8587d168fec4e3b7922b92eb286a2ddeebcf6acb55c7dc82/onnxruntime-1.24.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1cc6a518255f012134bc791975a6294806be9a3b20c4a54cca25194c90cf731", size = 17247021, upload-time = "2026-03-17T22:04:52.377Z" }, +] + +[[package]] +name = "opentelemetry-api" +version = "1.41.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fa/fc/b7564cbef36601aef0d6c9bc01f7badb64be8e862c2e1c3c5c3b43b53e4f/opentelemetry_api-1.41.1.tar.gz", hash = "sha256:0ad1814d73b875f84494387dae86ce0b12c68556331ce6ce8fe789197c949621", size = 71416, upload-time = "2026-04-24T13:15:38.262Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/59/3e7118ed140f76b0982ba4321bdaed1997a0473f9720de2d10788a577033/opentelemetry_api-1.41.1-py3-none-any.whl", hash = "sha256:a22df900e75c76dc08440710e51f52f1aa6b451b429298896023e60db5b3139f", size = 69007, upload-time = "2026-04-24T13:15:15.662Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-common" +version = "1.41.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-proto" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/fa/f9e3bd3c4d692b3ce9a2880a167d1f79681a1bea11f00d5bf76adc03e6ea/opentelemetry_exporter_otlp_proto_common-1.41.1.tar.gz", hash = "sha256:0e253156ea9c36b0bd3d2440c5c9ba7dd1f3fb64ba7a08fc85fbac536b56e1fb", size = 20409, upload-time = "2026-04-24T13:15:40.924Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/48/bce76d3ea772b609757e9bc844e02ab408a6446609bf74fb562062ba6b71/opentelemetry_exporter_otlp_proto_common-1.41.1-py3-none-any.whl", hash = "sha256:10da74dad6a49344b9b7b21b6182e3060373a235fde1528616d5f01f92e66aa9", size = 18366, upload-time = "2026-04-24T13:15:18.917Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-grpc" +version = "1.41.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "grpcio" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-common" }, + { name = "opentelemetry-proto" }, + { name = "opentelemetry-sdk" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1e/9b/e4503060b8695579dbaad187dc8cef4554188de68748c88060599b77489e/opentelemetry_exporter_otlp_proto_grpc-1.41.1.tar.gz", hash = "sha256:b05df8fa1333dc9a3fda36b676b96b5095ab6016d3f0c3296d430d629ba1443b", size = 25755, upload-time = "2026-04-24T13:15:41.93Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/f2/c54f33c92443d087703e57e52e55f22f111373a5c4c4aa349ea60efe512e/opentelemetry_exporter_otlp_proto_grpc-1.41.1-py3-none-any.whl", hash = "sha256:537926dcef951136992479af1d9cd88f25e33d56c530e9f020ed57774dca2f94", size = 20297, upload-time = "2026-04-24T13:15:20.212Z" }, +] + +[[package]] +name = "opentelemetry-proto" +version = "1.41.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/99/e8/633c6d8a9c8840338b105907e55c32d3da1983abab5e52f899f72a82c3d1/opentelemetry_proto-1.41.1.tar.gz", hash = "sha256:4b9d2eb631237ea43b80e16c073af438554e32bc7e9e3f8ca4a9582f900020e5", size = 45670, upload-time = "2026-04-24T13:15:49.768Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/1e/5cd77035e3e82070e2265a63a760f715aacd3cb16dddc7efee913f297fcc/opentelemetry_proto-1.41.1-py3-none-any.whl", hash = "sha256:0496713b804d127a4147e32849fbaf5683fac8ee98550e8e7679cd706c289720", size = 72076, upload-time = "2026-04-24T13:15:32.542Z" }, +] + +[[package]] +name = "opentelemetry-sdk" +version = "1.41.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/d0/54ee30dab82fb0acda23d144502771ff76ef8728459c83c3e89ef9fb1825/opentelemetry_sdk-1.41.1.tar.gz", hash = "sha256:724b615e1215b5aeacda0abb8a6a8922c9a1853068948bd0bd225a56d0c792e6", size = 230180, upload-time = "2026-04-24T13:15:50.991Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/e7/a1420b698aad018e1cf60fdbaaccbe49021fb415e2a0d81c242f4c518f54/opentelemetry_sdk-1.41.1-py3-none-any.whl", hash = "sha256:edee379c126c1bce952b0c812b48fe8ff35b30df0eecf17e98afa4d598b7d85d", size = 180213, upload-time = "2026-04-24T13:15:33.767Z" }, +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.62b1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/de/911ac9e309052aca1b20b2d5549d3db45d1011e1a610e552c6ccdd1b64f8/opentelemetry_semantic_conventions-0.62b1.tar.gz", hash = "sha256:c5cc6e04a7f8c7cdd30be2ed81499fa4e75bfbd52c9cb70d40af1f9cd3619802", size = 145750, upload-time = "2026-04-24T13:15:52.236Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/a6/83dc2ab6fa397ee66fba04fe2e74bdf7be3b3870005359ceb7689103c058/opentelemetry_semantic_conventions-0.62b1-py3-none-any.whl", hash = "sha256:cf506938103d331fbb78eded0d9788095f7fd59016f2bda813c3324e5a74a93c", size = 231620, upload-time = "2026-04-24T13:15:35.454Z" }, +] + +[[package]] +name = "optimum" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "torch" }, + { name = "transformers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f0/69/e1e9fe4d54f6b1b90cc278d6da74dd90eb4d9fd9228882886d7c275712e2/optimum-2.1.0.tar.gz", hash = "sha256:0a2a13f91500e41d34863ffdb08fcb886b3ce68a84a386e59653e3064a45dd4b", size = 125896, upload-time = "2025-12-19T10:47:18.571Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/98/c409ed937331839fdadc03cef6ebd19982bf3834711134db8898eeb31585/optimum-2.1.0-py3-none-any.whl", hash = "sha256:bc3af32e1236a9b2c2ca1d27ed9d3ab1b6591e24c6bcd47f9671a8198a30ea88", size = 161231, upload-time = "2025-12-19T10:47:17.054Z" }, +] + +[package.optional-dependencies] +onnxruntime = [ + { name = "optimum-onnx", extra = ["onnxruntime"] }, +] + +[[package]] +name = "optimum-onnx" +version = "0.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "onnx" }, + { name = "optimum" }, + { name = "transformers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/08/da/3a0073af8f436d72c1e4d9c655c00628b857bd1d9ccc101d35301d5bb2df/optimum_onnx-0.1.0.tar.gz", hash = "sha256:182c54b25eddaded1618af7b58516da34749393a987ec7111f74677f249676f9", size = 165531, upload-time = "2025-12-23T14:20:18.97Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/89/4be9d226bc74fd0eb405d1efea62e86d6f0f31841dae9c5898ee12eb482f/optimum_onnx-0.1.0-py3-none-any.whl", hash = "sha256:0301ec7a6ec5c77a57581e9970d380a6dc104bdb8f15b282e05af40d829c2eda", size = 194155, upload-time = "2025-12-23T14:20:17.741Z" }, +] + +[package.optional-dependencies] +onnxruntime = [ + { name = "onnxruntime" }, +] + +[[package]] +name = "orjson" +version = "3.11.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/0c/964746fcafbd16f8ff53219ad9f6b412b34f345c75f384ad434ceaadb538/orjson-3.11.9.tar.gz", hash = "sha256:4fef17e1f8722c11587a6ef18e35902450221da0028e65dbaaa543619e68e48f", size = 5599163, upload-time = "2026-05-06T15:11:08.309Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/eb/5da01e356015aee6ecfa1187ced87aef51364e306f5e695dd52719bf0e78/orjson-3.11.9-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:b6ef1979adc4bc243523f1a2ba91418030a8e29b0a99cbe7e0e2d6807d4dce6e", size = 228465, upload-time = "2026-05-06T15:10:44.097Z" }, + { url = "https://files.pythonhosted.org/packages/64/62/3e0e0c14c957133bcd855395c62b55ed4e3b0af23ffea11b032cb1dcbdb1/orjson-3.11.9-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:f36b7f32c7c0db4a719f1fc5824db4a9c6f8bd1a354debb91faf26ebf3a4c71e", size = 128364, upload-time = "2026-05-06T15:10:45.839Z" }, + { url = "https://files.pythonhosted.org/packages/5a/5a/07d8aa117211a8ed7630bda80c8c0b14d04e0f8dcf99bcf49656e4a710eb/orjson-3.11.9-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08f4d8ebb44925c794e535b2bebc507cebf32209df81de22ae285fb0d8d66de0", size = 132063, upload-time = "2026-05-06T15:10:47.267Z" }, + { url = "https://files.pythonhosted.org/packages/d6/ec/4acaf21483e18aa945be74a474c74b434f284b549f275a0a39b9f98956e9/orjson-3.11.9-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6cc7923789694fd58f001cbcac7e47abc13af4d560ebbfcf3b41a8b1a0748124", size = 122356, upload-time = "2026-05-06T15:10:48.765Z" }, + { url = "https://files.pythonhosted.org/packages/13/d8/5f0555e7638801323b7a75850f92e7dfa891bc84fe27a1ba4449170d1200/orjson-3.11.9-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea5c46eb2d3af39e806b986f4b09d5c2706a1f5afde3cbf7544ce6616127173c", size = 129592, upload-time = "2026-05-06T15:10:50.13Z" }, + { url = "https://files.pythonhosted.org/packages/b6/30/ed9860412a3603ceb3c5955bfd72d28b9d0e7ba6ed81add14f83d7114236/orjson-3.11.9-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f5d89a2ed90731df3be64bab0aa44f78bff39fdc9d71c291f4a8023aa46425b7", size = 140491, upload-time = "2026-05-06T15:10:51.582Z" }, + { url = "https://files.pythonhosted.org/packages/d0/17/adc514dea7ac7c505527febf884934b815d34f0c7b8693c1a8b39c5c4a57/orjson-3.11.9-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:25e4aed0312d292c09f61af25bba34e0b2c88546041472b09088c39a4d828af1", size = 127309, upload-time = "2026-05-06T15:10:53.329Z" }, + { url = "https://files.pythonhosted.org/packages/76/3e/c0b690253f0b82d86e99949af13533363acfb5432ecb5d53dd5b3bce9c34/orjson-3.11.9-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aaea64f3f467d22e70eeed68bdccb3bc4f83f650446c4a03c59f2cba28a108db", size = 134030, upload-time = "2026-05-06T15:10:54.988Z" }, + { url = "https://files.pythonhosted.org/packages/c1/7a/bc82a0bb25e9faaf92dc4d9ef002732efc09737706af83e346788641d4a7/orjson-3.11.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a028425d1b440c5d92a6be1e1a020739dfe67ea87d96c6dbe828c1b30041728b", size = 141482, upload-time = "2026-05-06T15:10:56.663Z" }, + { url = "https://files.pythonhosted.org/packages/01/55/e69188b939f77d5d32a9833745ace31ea5ccae3ab613a1ec185d3cd2c4fb/orjson-3.11.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5b192c6cf397e4455b11523c5cf2b18ed084c1bbd61b6c0926344d2129481972", size = 415178, upload-time = "2026-05-06T15:10:58.446Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1a/b8a5a7ac527e80b9cb11d51e3f6689b709279183264b9ec5c7bc680bb8b5/orjson-3.11.9-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea407d4ccf5891d667d045fecae97a7a1e5e87b3b97f97ae1803c2e741130be0", size = 148089, upload-time = "2026-05-06T15:11:00.441Z" }, + { url = "https://files.pythonhosted.org/packages/97/4e/00503f64204bf859b37213a63927028f30fb6268cd8677fb0a5ad48155e1/orjson-3.11.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5f63aaf97afd9f6dec5b1a68e1b8da12bfccb4cb9a9a65c3e0b6c847849e7586", size = 136921, upload-time = "2026-05-06T15:11:02.176Z" }, + { url = "https://files.pythonhosted.org/packages/0d/ba/a23b82a0a8d0ed7bed4e5f5035aae751cad4ff6a1e8d2ecd14d8860f5929/orjson-3.11.9-cp314-cp314-win32.whl", hash = "sha256:e30ab17845bb9fa54ccf67fa4f9f5282652d54faa6d17452f47d0f369d038673", size = 131638, upload-time = "2026-05-06T15:11:03.696Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c3/0c6798456bade745c75c452342dabacce5798196483e77e643be1f53877d/orjson-3.11.9-cp314-cp314-win_amd64.whl", hash = "sha256:32ef5f4283a3be81913947d19608eacb7c6608026851123790cd9cc8982af34b", size = 127078, upload-time = "2026-05-06T15:11:05.123Z" }, + { url = "https://files.pythonhosted.org/packages/16/21/5a3f1e8913103b703a436a5664238e5b965ec392b555fe68943ea3691e6b/orjson-3.11.9-cp314-cp314-win_arm64.whl", hash = "sha256:eebdbdeef0094e4f5aefa20dcd4eb2368ab5e7a3b4edea27f1e7b2892e009cf9", size = 126687, upload-time = "2026-05-06T15:11:06.602Z" }, +] + +[[package]] +name = "overrides" +version = "7.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/36/86/b585f53236dec60aba864e050778b25045f857e17f6e5ea0ae95fe80edd2/overrides-7.7.0.tar.gz", hash = "sha256:55158fa3d93b98cc75299b1e67078ad9003ca27945c76162c1c0766d6f91820a", size = 22812, upload-time = "2024-01-27T21:01:33.423Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/ab/fc8290c6a4c722e5514d80f62b2dc4c4df1a68a41d1364e625c35990fcf3/overrides-7.7.0-py3-none-any.whl", hash = "sha256:c7ed9d062f78b8e4c1a7b70bd8796b35ead4d9f510227ef9c5dc7626c60d7e49", size = 17832, upload-time = "2024-01-27T21:01:31.393Z" }, +] + +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + +[[package]] +name = "pandas" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/87/4341c6252d1c47b08768c3d25ac487362bf403f0313ddae4a2a26c9b1b4c/pandas-3.0.3.tar.gz", hash = "sha256:696a4a00a2a2a35d4e5deb3fc946641b96c944f02230e4f76137fe35d806c4fc", size = 4651414, upload-time = "2026-05-11T18:54:29.21Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/54/effdcc3c0ff7a08037889200e148ebe94c16c4f653be078c7b3675955df1/pandas-3.0.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3650109c0f22879df8bd6179ab9ee3d7f1d1d4e7e0094a3f0032d9f51e2e64ac", size = 10336065, upload-time = "2026-05-11T18:53:41.099Z" }, + { url = "https://files.pythonhosted.org/packages/68/10/bf2d6738d72748b961a3751ab89522d58c54efc36a8e1a12161216cd45cf/pandas-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:bab900348131a7db1f69a7309ef141fd5680f1487094193bcbbb61791573bf8f", size = 9926101, upload-time = "2026-05-11T18:53:43.515Z" }, + { url = "https://files.pythonhosted.org/packages/ae/e9/e35cf11c8a136e757b956f5f0efdcaa50aecde85ea055f1898dfc68262f3/pandas-3.0.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba7e08b9ac1d54569cd1e256e3668975ed624d6826f7b68df0342b012007bddb", size = 10457553, upload-time = "2026-05-11T18:53:46.394Z" }, + { url = "https://files.pythonhosted.org/packages/58/3b/1cdec6772bdbaf7b25dab360c59f03cadf05492dd724c6540af905389b07/pandas-3.0.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d71c63ae4ebdbf70209742096f1fc46a83a0613c99d4b23766cced9ff8cd62a", size = 10914065, upload-time = "2026-05-11T18:53:49.134Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c2/1ef644445fcd72e3627bceec77e3560636f87ddce4ed841afe76b83b5bf9/pandas-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e3a2ec42c98ffa2565a67e08e218d06d72576d758d90facb7c00805194d8f360", size = 11459188, upload-time = "2026-05-11T18:53:52.527Z" }, + { url = "https://files.pythonhosted.org/packages/7e/49/4d8d4f42cbc9c4adc7a1870f269c02cbd6cd40d059622c06fb298addcbad/pandas-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:335f62418ed562cfc3c49e9e196375c28b729dcef8543abf4f9438e381bf3c76", size = 11982966, upload-time = "2026-05-11T18:53:55.043Z" }, + { url = "https://files.pythonhosted.org/packages/38/55/792619469bab9882d8bbd5865d45a72f6478762d04a9af4bf0d08c503e95/pandas-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:3c20a521bbb85902f79f7270c80a59e1b5452d96d170c034f207181870f97ac5", size = 9876755, upload-time = "2026-05-11T18:53:58.067Z" }, + { url = "https://files.pythonhosted.org/packages/2a/af/33c469653b0ba03b50c3a98192d4c07f0c75c66b263ceb097fce0ee97d31/pandas-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:a2d2dff8a04f3917b55ab3910c32990f8ddf7eceba114947838cefa976a68977", size = 9198658, upload-time = "2026-05-11T18:54:00.733Z" }, + { url = "https://files.pythonhosted.org/packages/a2/fa/b8c257bd76b8bd060c3a9151c1fca05e9b9c5e3af5d0f549c0356f6d143d/pandas-3.0.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:0d589105b3c14645af1738ff279b2995102d8f7a03b0a66dc8d95550eb513e04", size = 10787242, upload-time = "2026-05-11T18:54:03.564Z" }, + { url = "https://files.pythonhosted.org/packages/54/eb/f19206ffb0bf1919002969aa448b4702c6594845156a6f8050674855aac3/pandas-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:13fc1e853d9e04743d11ba75a985ccbc2a317fe07d8af61e445a6fd24dacd6a6", size = 10436369, upload-time = "2026-05-11T18:54:06.311Z" }, + { url = "https://files.pythonhosted.org/packages/fd/24/c7c39fb4fe22b71a0c2d78bf0c585c600092d85f94f086d2b3b2f6ca27e2/pandas-3.0.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:819959dab7bbd0049c15623fbac4e29a191b9528160a61fb1032242d8ced2d9c", size = 10358306, upload-time = "2026-05-11T18:54:09.085Z" }, + { url = "https://files.pythonhosted.org/packages/16/ec/dd2a9eb7fa1204df88c0864164e35b228ac581062ac612ba0a67fd812e4c/pandas-3.0.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:60ae316d3fd75d1858d450d0db0103ea2be3e7d4a95ec2f064f7e2ae63f7b028", size = 10758394, upload-time = "2026-05-11T18:54:11.956Z" }, + { url = "https://files.pythonhosted.org/packages/95/6e/00c61ea8e85b4f6d8d35e11852a1a4998fc7fafc91c6a602d1cc9c972d64/pandas-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd3a518890b400d32f9023722dc9a9a5c969f00b415419a3c06c043f09bb5d7d", size = 11375717, upload-time = "2026-05-11T18:54:14.539Z" }, + { url = "https://files.pythonhosted.org/packages/31/89/8fc1c268969fac43688d65fd92e67df24bd128d53cb4d2eee534cd307399/pandas-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9c39be2d709d01fa972a0cabc522389fceca4f3969332ba25a7d6c5802cf976a", size = 11828897, upload-time = "2026-05-11T18:54:17.146Z" }, + { url = "https://files.pythonhosted.org/packages/56/3b/e7d20dea247a3e6dc0bd8a6953854afbedc03951def4e7371e05e7263e25/pandas-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4db8c527972a821cf5286b40ccc57642a39bc62e62022b42f99f8a67fca8c3a1", size = 10900855, upload-time = "2026-05-11T18:54:19.72Z" }, + { url = "https://files.pythonhosted.org/packages/0f/54/68a0978d1ef8502b8492099beaa6e7a0c1b32e3b5d4f677f5810cb08711c/pandas-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b2c95f8bfc1ee412bf482605d7bfd30c12d1d26bd59fdd91efeef1d4718decb1", size = 9466464, upload-time = "2026-05-11T18:54:22.754Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "primp" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/4b/7efa54f38da7de8df6b70dfed173bb41a52b740b144e4be24c1172db4209/primp-1.3.1.tar.gz", hash = "sha256:b04a5941bf9c876d011c5defaf5a25be093d56e7270b8da52c9788b9df2a829a", size = 1360029, upload-time = "2026-05-23T17:39:25.568Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/80/c4885a783a7493e396d89a592ba19fce63ef6bd6ad47230924a884a30ec0/primp-1.3.1-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:27b87e6370045a0c65c0e4dfdfacbfe637387d05673ce8ddcce400263f7c27f0", size = 5123967, upload-time = "2026-05-23T17:39:08.586Z" }, + { url = "https://files.pythonhosted.org/packages/58/c1/c965cc23f96a364803d44b4331f33e4465bb6f269add37e39d0ad77ffe33/primp-1.3.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:27a8804eb9a3f641f379ee2b443591428cf85c898816e93d04d3e7b6f229ebcb", size = 4743059, upload-time = "2026-05-23T17:39:15.536Z" }, + { url = "https://files.pythonhosted.org/packages/9c/99/f4248d8d833d43fd8ba78208f2f4bf7fba7d3aec8c516090a95d18d6f550/primp-1.3.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:862974796552a51af8e276bb19c5d5e189168ab8bad216aef7ce3726a8d3b1dd", size = 5100121, upload-time = "2026-05-23T17:39:04.64Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ad/519e32e0184763e1a76c9321fdeac0bb9b30bf85746f12058feec0cc4a27/primp-1.3.1-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ceb24198994799706f4020a00173ba9c1b491aa9805b1e014d87946677bc3c5d", size = 4738042, upload-time = "2026-05-23T17:39:35.967Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7b/723cb40694b47ec79a142ed8492835c0ecae9fef7acbed014f04b018d1de/primp-1.3.1-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3298b8afcf0a88ba6622bfc18e78aeb11afbb7d5afa4774f24acf7491f54a2d", size = 5001773, upload-time = "2026-05-23T17:39:03.01Z" }, + { url = "https://files.pythonhosted.org/packages/52/b8/80a2e3bdab1c51d738b82ea210a5ab93986b443c561e792e42cae296ec10/primp-1.3.1-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6b8d38c5a6d0a863274cbcae9678f265fcdcead3c20d12d152244e88f5f2186b", size = 5334228, upload-time = "2026-05-23T17:39:24.214Z" }, + { url = "https://files.pythonhosted.org/packages/19/70/c95b8054c7d1fe2d84226ec60a5f48ce6c95a08b7c8b1702d7742082f444/primp-1.3.1-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96f831c78ddb5900873f51e294bf9bbb4bbfdac3a2f39ce4023f8c558d299332", size = 5157269, upload-time = "2026-05-23T17:38:48.142Z" }, + { url = "https://files.pythonhosted.org/packages/34/bb/9b66986b7ecf2eff987134cd94bde533142e3085d6f67531f1a369ceaaae/primp-1.3.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:329d0c320841f65b39d80801d8bae126732b84ec1094ca17b14fda0bda1b20ff", size = 5347438, upload-time = "2026-05-23T17:39:17.405Z" }, + { url = "https://files.pythonhosted.org/packages/aa/29/5d127748d06f3c6a3367f3c4974e45b98cda61cd28ea79ef91ad3fe9e093/primp-1.3.1-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6c3c67670c38a03e9e8da45b212243d35afc8efa018317c46ecdce47f05329d1", size = 5264862, upload-time = "2026-05-23T17:39:20.625Z" }, + { url = "https://files.pythonhosted.org/packages/16/f3/1aac229425cac142c48418e2de9f70597161ea936543b5e3c9e7476e1921/primp-1.3.1-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:9409a31028a8c62a609d389554ad4f5339aad075130300cd443beef0336d7179", size = 4969889, upload-time = "2026-05-23T17:39:22.412Z" }, + { url = "https://files.pythonhosted.org/packages/38/86/a94d6e6166139c76ae42eb941328679309ca85139e8753d639657a24474c/primp-1.3.1-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:88ca36c2bd1b7c64b96ad07ca367d2d111ac8e9670549be5f232da8bf795d21e", size = 5082679, upload-time = "2026-05-23T17:39:28.411Z" }, + { url = "https://files.pythonhosted.org/packages/cf/61/21d297db575ed660c6aaf35c9014c1874ace45d6dcb79d1a4d3d2608bffb/primp-1.3.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:74d13800b501aa003fb05c263d38f8d61656c83a60b2951046c0fc412bc73976", size = 5605392, upload-time = "2026-05-23T17:39:38.007Z" }, + { url = "https://files.pythonhosted.org/packages/36/d6/9262a7ebb1d980a2db0cd505bb902bb3e66acd8a1cb763a4c2921f2f6a5b/primp-1.3.1-cp310-abi3-win32.whl", hash = "sha256:09ada1752629fe89d7b128beeb59cb641f404af462e24177ba36aed1cf322299", size = 4270373, upload-time = "2026-05-23T17:38:44.98Z" }, + { url = "https://files.pythonhosted.org/packages/8f/68/f0c6a60fadff0c185aef232b951a6fa4bbb64511facc48d34734db14f16f/primp-1.3.1-cp310-abi3-win_amd64.whl", hash = "sha256:c0d1e294466cd5ec7ef173eedf8df25cbdc050138d40447a906e92b8553e7765", size = 4661498, upload-time = "2026-05-23T17:39:32.213Z" }, + { url = "https://files.pythonhosted.org/packages/7f/1d/232a52abc77384ac66b9c1741691dec3659b1207bb6c5e55c1e9b59d22f1/primp-1.3.1-cp310-abi3-win_arm64.whl", hash = "sha256:43304cb41cbb46f361de49faf1cbdba57f969f628c9297239c7ed8ef0cac420f", size = 4624481, upload-time = "2026-05-23T17:38:42.724Z" }, + { url = "https://files.pythonhosted.org/packages/e5/0b/34333b26c533c3122b936dad829f0a6e04f32065d39673c92b157d97aa16/primp-1.3.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:72249a4540d0a8965f36eb9a86cd16801d1c7e8dac2f0b0fa23a0a5a03402d36", size = 5116098, upload-time = "2026-05-23T17:38:55.219Z" }, + { url = "https://files.pythonhosted.org/packages/7a/56/7fe14708adf9a5cb5d6a15ad840a3de036cebfaf20692a5bc3b72e188a73/primp-1.3.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:db4e2eaa5707e47899eeba6026f420f9b0108a28c08d63f1826d0cab8d50f06f", size = 4736300, upload-time = "2026-05-23T17:38:51.792Z" }, + { url = "https://files.pythonhosted.org/packages/31/cb/521a8c18e8808a75450b6e91dc62cc1149c0178b7d4a8697d3f9b73fa385/primp-1.3.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d62e7609c98b4bc99c9cecc47f16f332fb8fe1a023002176267b0043dedad0c7", size = 5093823, upload-time = "2026-05-23T17:38:50.059Z" }, + { url = "https://files.pythonhosted.org/packages/57/84/90f776fe46aeb0e3b86df72c674c0651326dd6a61846dd86bddbabe903ac/primp-1.3.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3d692e912c2b25271163ba7719df0afdb733a7e7c3073c9094e9001882463543", size = 4734511, upload-time = "2026-05-23T17:39:34.166Z" }, + { url = "https://files.pythonhosted.org/packages/19/79/d9bfbc0df0394f18a98b512a65f4bcf3dd7d17bd871937127e1ce4549172/primp-1.3.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c08693517dc160a12c0f9e2565c5319173cef738893a303ff2fb28ecccbd84d", size = 4999315, upload-time = "2026-05-23T17:39:12.061Z" }, + { url = "https://files.pythonhosted.org/packages/bb/d8/5a986957ee1874d08567d7749668cd78a063048d47d6e46a874742b7fed1/primp-1.3.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d134ebfa31adc619e4e48289fe3e7eebc8310141560e6a6a04269cc94893d9ab", size = 5329375, upload-time = "2026-05-23T17:39:30.307Z" }, + { url = "https://files.pythonhosted.org/packages/5c/1d/321cac9902cc3992174ed530719141a0da2e426f54f8a90b7b971571d104/primp-1.3.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48e27e7c0e015a6de495cf79c0c8d599ba5f69d091af31572bec2de020522d9c", size = 5140921, upload-time = "2026-05-23T17:38:53.528Z" }, + { url = "https://files.pythonhosted.org/packages/a6/19/ccbe6b67e0e91beb5c9d5cf804354225d5a3a7a9adf84fee3d6acc53febd/primp-1.3.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c3d682df08c1b1f37b1f66b21fd173baebcfcb52490830b12292d8fe89b2147", size = 5344288, upload-time = "2026-05-23T17:39:18.965Z" }, + { url = "https://files.pythonhosted.org/packages/ff/e5/a735751bd11558163e83e0961fc866e4f94634df9eb24937c5f59624e393/primp-1.3.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fabaac4280df0802377d34b869949d617a0ecf22ca7fd5f9bded3f5c981031f1", size = 5262909, upload-time = "2026-05-23T17:39:00.789Z" }, + { url = "https://files.pythonhosted.org/packages/09/8e/4e3d4520e2e751f2de825dbe2cb43f837d33a5528adc44255f9770ea125a/primp-1.3.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:f510e5881e0a4c4b9e7dbc03722c316d58454388b88000a0e7bf18a4b36d601e", size = 4964809, upload-time = "2026-05-23T17:38:59.023Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b5/e7b7495687d07df693325a12c497a9e5185d5001b7b216f32019fa7437a0/primp-1.3.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0504de2901c97903a9c369856a4b186dc90a782d8320652c142b066e697d5a1a", size = 5083654, upload-time = "2026-05-23T17:39:06.598Z" }, + { url = "https://files.pythonhosted.org/packages/f2/f9/e4652e93beb14a16cc4218cfb1ccc18eaca8ee7d93b517d614a135928ec9/primp-1.3.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c3b24e302d95d327e873834b9423823b9c8af2abf5e0bbf57a03f3354cfe528", size = 5594738, upload-time = "2026-05-23T17:39:27.001Z" }, + { url = "https://files.pythonhosted.org/packages/e2/49/e8c8a7bc6b741ab6f15896022eee4f906d04d7ccd15aeeb515dd04bbeb6d/primp-1.3.1-cp314-cp314t-win32.whl", hash = "sha256:4346dcef805279028bf4a54bb87dd43d0920130e25b5790689f5c96c9ba0d9e5", size = 4262615, upload-time = "2026-05-23T17:39:13.88Z" }, + { url = "https://files.pythonhosted.org/packages/a9/84/7ae4a257dec6dff329d4a8d9051d907316095c27ffc8d1ea15c359e6eeb5/primp-1.3.1-cp314-cp314t-win_amd64.whl", hash = "sha256:6c55f152a73b6d6af8ac37bdb648d8bbfd7e656f9ef40d87feb3c0d81cee930a", size = 4660584, upload-time = "2026-05-23T17:39:10.081Z" }, + { url = "https://files.pythonhosted.org/packages/99/20/10e0d96bfaeef1f0cd339ccf9bb8feb4bf798fde93198f7a96c73441080a/primp-1.3.1-cp314-cp314t-win_arm64.whl", hash = "sha256:46a529d74583d6ceba52e15bf4c678fcf24e6d669c1ce935262d5490d1b25801", size = 4623226, upload-time = "2026-05-23T17:38:57.256Z" }, +] + +[[package]] +name = "propcache" +version = "0.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/44/c87281c333769159c50594f22610f77398a47ccbfbbf23074e744e86f87c/propcache-0.5.2.tar.gz", hash = "sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427", size = 50208, upload-time = "2026-05-08T21:02:12.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/ea/23ee535d90ce8bcc465a3028eb3cc0ce3bd1005f4bb27710b30587de798d/propcache-0.5.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:46088abff4cba581dea21ae0467a480526cb25aa5f3c269e909f800328bc3999", size = 94662, upload-time = "2026-05-08T21:01:22.683Z" }, + { url = "https://files.pythonhosted.org/packages/b5/06/c5a52f419b5d8972f8d46a7577476090d8e3263ff589ce40b5ca4968d5be/propcache-0.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fc88b26f08d634f7bc819a7852e5214f5802641ab8d9fd5326892292eee1993e", size = 53928, upload-time = "2026-05-08T21:01:23.986Z" }, + { url = "https://files.pythonhosted.org/packages/63/b1/4260d67d6bd85e58a66b72d54ce15d5de789b6f3870cc6bedf8ff9667401/propcache-0.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97797ebb098e670a2f92dd66f32897e30d7615b14e7f59711de23e30a9072539", size = 54650, upload-time = "2026-05-08T21:01:25.305Z" }, + { url = "https://files.pythonhosted.org/packages/70/06/2f46c318e3307cd7a6a7481def374ce838c0fe20084b39dd54b0879d0e99/propcache-0.5.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba57fffe4ac99c5d30076161b5866336d97600769bad35cc68f7774b15298a4e", size = 59912, upload-time = "2026-05-08T21:01:26.545Z" }, + { url = "https://files.pythonhosted.org/packages/4c/29/fe1aebec2ce57ab985a9c382bded1124431f85078113aa222c5d278430d4/propcache-0.5.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:583c19759d9eec1e5b69e2fbef36a7d9c326041be9746cb822d335c8cedc2979", size = 63300, upload-time = "2026-05-08T21:01:27.937Z" }, + { url = "https://files.pythonhosted.org/packages/b4/18/2334b26768b6c82be8c69e83671b767d5ef426aa09b0cba6c2ea47816774/propcache-0.5.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d0326e2e5e1f3163fa306c834e48e8d490e5fae607a097a40c0648109b47ba80", size = 64208, upload-time = "2026-05-08T21:01:29.484Z" }, + { url = "https://files.pythonhosted.org/packages/2b/76/7f1bfd6afff4c5e38e36a3c6d68eb5f4b7311ea80baf693db78d95b603c4/propcache-0.5.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e00820e192c8dbebcafb383ebbf99030895f09905e7a0eb2e0340a0bcc2bc825", size = 61633, upload-time = "2026-05-08T21:01:31.068Z" }, + { url = "https://files.pythonhosted.org/packages/c4/46/b3ff8aba2b4953a3e50de2cf72f1b5748b8eca93b15f3dc2c84339084c09/propcache-0.5.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c66afea89b1e43725731d2004732a046fe6fe955d51f952c3e95a7314a284a39", size = 61724, upload-time = "2026-05-08T21:01:32.374Z" }, + { url = "https://files.pythonhosted.org/packages/c5/01/814cfcafbcff954f94c01cf30e097ddc88a076b5440fbcf4570753437d40/propcache-0.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4dc37dec6c6cdad0b57881a5658fd14fbf53e333b1a86cf86559f190e1d9ec4", size = 60069, upload-time = "2026-05-08T21:01:33.67Z" }, + { url = "https://files.pythonhosted.org/packages/da/68/5c6f7622d510cc666a300687e06fd060c1a43361c0c9b20d284f06d8096a/propcache-0.5.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5570dbcc97571c15f68068e529c92715a12f8d54030e272d264b377e22bd17a5", size = 57099, upload-time = "2026-05-08T21:01:34.915Z" }, + { url = "https://files.pythonhosted.org/packages/55/27/9cb0b4c679124085327957d42521c99dba04c88c90c3e55a6f0b633ebccc/propcache-0.5.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f814362777a9f841adddb200ecdf8f5cb1e5a3c4b7a86378edbd6ccb26edd702", size = 63391, upload-time = "2026-05-08T21:01:36.231Z" }, + { url = "https://files.pythonhosted.org/packages/f0/9d/7258aaa5bdf60fc6f27591eef6fe52768cb0beda7140be477c8b12c9794a/propcache-0.5.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:196913dea116aeb5a2ba95af4ddcb7ea85559ae07d8eee8751688310d09168c3", size = 61626, upload-time = "2026-05-08T21:01:37.545Z" }, + { url = "https://files.pythonhosted.org/packages/8e/0d/41c602003e8a9b16fe1e7eadf62c7bfba9d5474370b24200bf48b315f45f/propcache-0.5.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6e7b8719005dd1175be4ab1cd25e9b98659a5e0347331506ec6760d2773a7fb5", size = 64781, upload-time = "2026-05-08T21:01:38.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f3/38e66b1856e9bd079deea015bc4a55f7767c0e4db2f7dcf69e7e680ba4ce/propcache-0.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:51f96d685ab16e88cab128cd37a52c5da540809c8b879fa047731bfcb4ad35a4", size = 62570, upload-time = "2026-05-08T21:01:40.415Z" }, + { url = "https://files.pythonhosted.org/packages/95/ca/bbfe9b910ce57dde8bb4876b4520fc02a4e89497c10de26be936758a3aaa/propcache-0.5.2-cp314-cp314-win32.whl", hash = "sha256:cc6fc3cc62e8501d3ed62894425040d2728ecddb1ed072737a5c70bd537aa9f0", size = 39436, upload-time = "2026-05-08T21:01:41.654Z" }, + { url = "https://files.pythonhosted.org/packages/61/d2/45c9defbaa1ea297035d9d4cce9e8f80daafbf19319c6007f157c6256ea9/propcache-0.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:81e3a30b0bb60caa22033dd0f8a3618d1d67356212514f62c57db75cb0ef410c", size = 42373, upload-time = "2026-05-08T21:01:43.041Z" }, + { url = "https://files.pythonhosted.org/packages/44/68/9ea5103f41d5217d7d6ec24db90018e23aebec070c3f9a6e54d12b841fd8/propcache-0.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:0d2c9bf8528f135dbb805ce027567e09164f7efa51a2be07458a2c0420f292d0", size = 38554, upload-time = "2026-05-08T21:01:44.336Z" }, + { url = "https://files.pythonhosted.org/packages/8a/81/fadf555f42d3b762eea8a53950b0489fdc0aa9da5f8ed9e10ce0a4e01b48/propcache-0.5.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4bc8ff1feffc6a61c7002ffe84634c41b822e104990ae009f44a0834430070bb", size = 99395, upload-time = "2026-05-08T21:01:45.883Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c9/c61e134a686949cf7971af3a390148b1156f7be81c73bc0cd12c873e2d48/propcache-0.5.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:79aa3ff0a9b566633b642fa9caf7e21ed1c13d6feca718187873f199e1514078", size = 56653, upload-time = "2026-05-08T21:01:47.307Z" }, + { url = "https://files.pythonhosted.org/packages/cb/73/daf935ea7048ddd7ec8eec5345b4a40b619d2d178b3c0a0900796bc3c794/propcache-0.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1b31822f4474c4036bae62de9402710051d431a606d6a0f907fec79935a071aa", size = 56914, upload-time = "2026-05-08T21:01:48.573Z" }, + { url = "https://files.pythonhosted.org/packages/79/9f/aba959b435ea18617edd7cf0a7ad0b9c574b8fc7e3d2cd55fb59cb255d33/propcache-0.5.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13fef48778b5a2a756523fdb781326b028ca75e32858b04f2cdd19f394564917", size = 62567, upload-time = "2026-05-08T21:01:49.903Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a1/859942de9a791ff42f6141736f5b37749b8f53e65edfa49638c67dd67e6a/propcache-0.5.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8b73ab70f1a3351fbc71f663b3e645af6dd0329100c353081cf69c37433fc6fe", size = 65542, upload-time = "2026-05-08T21:01:51.204Z" }, + { url = "https://files.pythonhosted.org/packages/b5/61/315bc0fd6c0fc7f80a528b8afd209e5fc4a875ea79571b91b8f50f442907/propcache-0.5.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5538d2c13d93e4698af7e092b57bc7298fd35d1d58e656ae18f23ee0d0378e03", size = 66845, upload-time = "2026-05-08T21:01:52.539Z" }, + { url = "https://files.pythonhosted.org/packages/47/f7/9f8122e3132e8e354ac41975ef8f1099be7d5a16bc7ae562734e993665c0/propcache-0.5.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd645f03898405cabe694fb8bc35241e3a9c332ec85627584fe3de201452b335", size = 63985, upload-time = "2026-05-08T21:01:53.847Z" }, + { url = "https://files.pythonhosted.org/packages/c8/54/c317819ec157cbf6f35df9df9657a6f82daf34d5faf15948b2f639c2192e/propcache-0.5.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a473b3440261e0c60706e732b2ed2f517857344fc21bf48fdfe211e2d98eb285", size = 63999, upload-time = "2026-05-08T21:01:55.179Z" }, + { url = "https://files.pythonhosted.org/packages/5a/56/387e3f7dfce0a9233df41fb888aa1c30222cb4bbbf09537c02dd9bd85fe2/propcache-0.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7afa37062e6650640e932e4cc9297d81f9f42d9944029cc386b8247dea4da837", size = 62779, upload-time = "2026-05-08T21:01:57.489Z" }, + { url = "https://files.pythonhosted.org/packages/a1/9c/596784cb5824ed61ee960d3f8655a3f0993e107c6e98ab6c818b7fb92ccb/propcache-0.5.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:8a90efd5777e996e42d568db9ac740b944d691e565cbfd31b2f7832f9184b2b8", size = 59796, upload-time = "2026-05-08T21:01:58.736Z" }, + { url = "https://files.pythonhosted.org/packages/c2/3d/1a6cfa1726a48542c1e8784a0761421476a5b68e09b7f36bf95eb954aaba/propcache-0.5.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f19bb891234d72535764d703bfed1153cc34f4214d5bd7150aee1eec9e8f4366", size = 66023, upload-time = "2026-05-08T21:02:00.228Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0e/05fd6990369477076e4e280bcb970de760fddf0161a46e988bc95f7940ec/propcache-0.5.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:32775082acd2d807ee3db715c7770d38767b817870acfa08c29e057f3c4d5b56", size = 64448, upload-time = "2026-05-08T21:02:01.888Z" }, + { url = "https://files.pythonhosted.org/packages/cd/86/5f8da315a4309c62c10c0b2516b17492d5d3bbe1bb862b96604db67e2a37/propcache-0.5.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9282fb1a3bccd038da9f768b927b24a0c753e466c086b7c4f3c6982851eefb2d", size = 67329, upload-time = "2026-05-08T21:02:03.484Z" }, + { url = "https://files.pythonhosted.org/packages/da/d3/3368efe79ab21f0cdf86ef49895811c9cc933131d4cde1f28a624e22e712/propcache-0.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc49723e2f60d6b32a0f0b08a3fd6d13203c07f1cd9566cfce0f12a917c967a2", size = 65172, upload-time = "2026-05-08T21:02:04.745Z" }, + { url = "https://files.pythonhosted.org/packages/d5/07/127e8b0bacfb325396196f9d976a22453049b89b9b2b08477cc3145faa44/propcache-0.5.2-cp314-cp314t-win32.whl", hash = "sha256:2d7aa89ebca5acc98cba9d1472d976e394782f587bad6661003602a619fd1821", size = 43813, upload-time = "2026-05-08T21:02:06.025Z" }, + { url = "https://files.pythonhosted.org/packages/88/fb/46dad6c0ae49ed230ab1b16c890c2b6314e2403e6c412976f4a72d64a527/propcache-0.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:d447bb0b3054be5818458fbb171208b1d9ff11eba14e18ca18b90cbb45767370", size = 47764, upload-time = "2026-05-08T21:02:07.353Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c4/a47d0a63aa309d10d59ede6e9d4cff03a344a79d1f0f4cd0cd74997b53e0/propcache-0.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:fe67a3d11cd9b4efabfa45c3d00ffba2b26811442a73a581a94b67c2b5faccf6", size = 41140, upload-time = "2026-05-08T21:02:09.065Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ed/1cdcab6ba3d6ab7feca11fc14f0eeea80755bb53ef4e892079f31b10a25f/propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe", size = 14036, upload-time = "2026-05-08T21:02:10.673Z" }, +] + +[[package]] +name = "proto-plus" +version = "1.28.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/56/e647b0c675392d2da368da7b6f158f7368b18542fd6f7d7400a2f39de000/proto_plus-1.28.0.tar.gz", hash = "sha256:38e5696342835b08fc116f30a25665b29531cda9d5d5643e9b81fc312385abd9", size = 57221, upload-time = "2026-05-07T08:04:50.811Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/20/b122d4626976acb81132036d2ad1bb35a1a8775fceb837ec30964622516a/proto_plus-1.28.0-py3-none-any.whl", hash = "sha256:a630604310899e73c59ec302e5765c058d412b2f090b9c79c8822589f14955b8", size = 50410, upload-time = "2026-05-07T08:03:31.962Z" }, +] + +[[package]] +name = "protobuf" +version = "6.33.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/66/70/e908e9c5e52ef7c3a6c7902c9dfbb34c7e29c25d2f81ade3856445fd5c94/protobuf-6.33.6.tar.gz", hash = "sha256:a6768d25248312c297558af96a9f9c929e8c4cee0659cb07e780731095f38135", size = 444531, upload-time = "2026-03-18T19:05:00.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/9f/2f509339e89cfa6f6a4c4ff50438db9ca488dec341f7e454adad60150b00/protobuf-6.33.6-cp310-abi3-win32.whl", hash = "sha256:7d29d9b65f8afef196f8334e80d6bc1d5d4adedb449971fefd3723824e6e77d3", size = 425739, upload-time = "2026-03-18T19:04:48.373Z" }, + { url = "https://files.pythonhosted.org/packages/76/5d/683efcd4798e0030c1bab27374fd13a89f7c2515fb1f3123efdfaa5eab57/protobuf-6.33.6-cp310-abi3-win_amd64.whl", hash = "sha256:0cd27b587afca21b7cfa59a74dcbd48a50f0a6400cfb59391340ad729d91d326", size = 437089, upload-time = "2026-03-18T19:04:50.381Z" }, + { url = "https://files.pythonhosted.org/packages/5c/01/a3c3ed5cd186f39e7880f8303cc51385a198a81469d53d0fdecf1f64d929/protobuf-6.33.6-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:9720e6961b251bde64edfdab7d500725a2af5280f3f4c87e57c0208376aa8c3a", size = 427737, upload-time = "2026-03-18T19:04:51.866Z" }, + { url = "https://files.pythonhosted.org/packages/ee/90/b3c01fdec7d2f627b3a6884243ba328c1217ed2d978def5c12dc50d328a3/protobuf-6.33.6-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:e2afbae9b8e1825e3529f88d514754e094278bb95eadc0e199751cdd9a2e82a2", size = 324610, upload-time = "2026-03-18T19:04:53.096Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ca/25afc144934014700c52e05103c2421997482d561f3101ff352e1292fb81/protobuf-6.33.6-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:c96c37eec15086b79762ed265d59ab204dabc53056e3443e702d2681f4b39ce3", size = 339381, upload-time = "2026-03-18T19:04:54.616Z" }, + { url = "https://files.pythonhosted.org/packages/16/92/d1e32e3e0d894fe00b15ce28ad4944ab692713f2e7f0a99787405e43533a/protobuf-6.33.6-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:e9db7e292e0ab79dd108d7f1a94fe31601ce1ee3f7b79e0692043423020b0593", size = 323436, upload-time = "2026-03-18T19:04:55.768Z" }, + { url = "https://files.pythonhosted.org/packages/c4/72/02445137af02769918a93807b2b7890047c32bfb9f90371cbc12688819eb/protobuf-6.33.6-py3-none-any.whl", hash = "sha256:77179e006c476e69bf8e8ce866640091ec42e1beb80b213c3900006ecfba6901", size = 170656, upload-time = "2026-03-18T19:04:59.826Z" }, +] + +[[package]] +name = "pyasn1" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/5f/6583902b6f79b399c9c40674ac384fd9cd77805f9e6205075f828ef11fb2/pyasn1-0.6.3.tar.gz", hash = "sha256:697a8ecd6d98891189184ca1fa05d1bb00e2f84b5977c481452050549c8a72cf", size = 148685, upload-time = "2026-03-17T01:06:53.382Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/a0/7d793dce3fa811fe047d6ae2431c672364b462850c6235ae306c0efd025f/pyasn1-0.6.3-py3-none-any.whl", hash = "sha256:a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde", size = 83997, upload-time = "2026-03-17T01:06:52.036Z" }, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, +] + +[[package]] +name = "pybase64" +version = "1.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/b8/4ed5c7ad5ec15b08d35cc79ace6145d5c1ae426e46435f4987379439dfea/pybase64-1.4.3.tar.gz", hash = "sha256:c2ed274c9e0ba9c8f9c4083cfe265e66dd679126cd9c2027965d807352f3f053", size = 137272, upload-time = "2025-12-06T13:27:04.013Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/80/4bd3dff423e5a91f667ca41982dc0b79495b90ec0c0f5d59aca513e50f8c/pybase64-1.4.3-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:015bb586a1ea1467f69d57427abe587469392215f59db14f1f5c39b52fdafaf5", size = 33835, upload-time = "2025-12-06T13:24:31.767Z" }, + { url = "https://files.pythonhosted.org/packages/45/60/a94d94cc1e3057f602e0b483c9ebdaef40911d84a232647a2fe593ab77bb/pybase64-1.4.3-cp314-cp314-android_24_x86_64.whl", hash = "sha256:d101e3a516f837c3dcc0e5a0b7db09582ebf99ed670865223123fb2e5839c6c0", size = 40673, upload-time = "2025-12-06T13:24:32.82Z" }, + { url = "https://files.pythonhosted.org/packages/e3/71/cf62b261d431857e8e054537a5c3c24caafa331de30daede7b2c6c558501/pybase64-1.4.3-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8f183ac925a48046abe047360fe3a1b28327afb35309892132fe1915d62fb282", size = 30939, upload-time = "2025-12-06T13:24:34.001Z" }, + { url = "https://files.pythonhosted.org/packages/24/3e/d12f92a3c1f7c6ab5d53c155bff9f1084ba997a37a39a4f781ccba9455f3/pybase64-1.4.3-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30bf3558e24dcce4da5248dcf6d73792adfcf4f504246967e9db155be4c439ad", size = 31401, upload-time = "2025-12-06T13:24:35.11Z" }, + { url = "https://files.pythonhosted.org/packages/9b/3d/9c27440031fea0d05146f8b70a460feb95d8b4e3d9ca8f45c972efb4c3d3/pybase64-1.4.3-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a674b419de318d2ce54387dd62646731efa32b4b590907800f0bd40675c1771d", size = 38075, upload-time = "2025-12-06T13:24:36.53Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d4/6c0e0cf0efd53c254173fbcd84a3d8fcbf5e0f66622473da425becec32a5/pybase64-1.4.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:720104fd7303d07bac302be0ff8f7f9f126f2f45c1edb4f48fdb0ff267e69fe1", size = 38257, upload-time = "2025-12-06T13:24:38.049Z" }, + { url = "https://files.pythonhosted.org/packages/50/eb/27cb0b610d5cd70f5ad0d66c14ad21c04b8db930f7139818e8fbdc14df4d/pybase64-1.4.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:83f1067f73fa5afbc3efc0565cecc6ed53260eccddef2ebe43a8ce2b99ea0e0a", size = 31685, upload-time = "2025-12-06T13:24:40.327Z" }, + { url = "https://files.pythonhosted.org/packages/db/26/b136a4b65e5c94ff06217f7726478df3f31ab1c777c2c02cf698e748183f/pybase64-1.4.3-cp314-cp314-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:b51204d349a4b208287a8aa5b5422be3baa88abf6cc8ff97ccbda34919bbc857", size = 68460, upload-time = "2025-12-06T13:24:41.735Z" }, + { url = "https://files.pythonhosted.org/packages/68/6d/84ce50e7ee1ae79984d689e05a9937b2460d4efa1e5b202b46762fb9036c/pybase64-1.4.3-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:30f2fd53efecbdde4bdca73a872a68dcb0d1bf8a4560c70a3e7746df973e1ef3", size = 71688, upload-time = "2025-12-06T13:24:42.908Z" }, + { url = "https://files.pythonhosted.org/packages/e3/57/6743e420416c3ff1b004041c85eb0ebd9c50e9cf05624664bfa1dc8b5625/pybase64-1.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0932b0c5cfa617091fd74f17d24549ce5de3628791998c94ba57be808078eeaf", size = 60040, upload-time = "2025-12-06T13:24:44.37Z" }, + { url = "https://files.pythonhosted.org/packages/3b/68/733324e28068a89119af2921ce548e1c607cc5c17d354690fc51c302e326/pybase64-1.4.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:acb61f5ab72bec808eb0d4ce8b87ec9f38d7d750cb89b1371c35eb8052a29f11", size = 56478, upload-time = "2025-12-06T13:24:45.815Z" }, + { url = "https://files.pythonhosted.org/packages/b5/9e/f3f4aa8cfe3357a3cdb0535b78eb032b671519d3ecc08c58c4c6b72b5a91/pybase64-1.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:2bc2d5bc15168f5c04c53bdfe5a1e543b2155f456ed1e16d7edce9ce73842021", size = 59463, upload-time = "2025-12-06T13:24:46.938Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d1/53286038e1f0df1cf58abcf4a4a91b0f74ab44539c2547b6c31001ddd054/pybase64-1.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:8a7bc3cd23880bdca59758bcdd6f4ef0674f2393782763910a7466fab35ccb98", size = 60360, upload-time = "2025-12-06T13:24:48.039Z" }, + { url = "https://files.pythonhosted.org/packages/00/9a/5cc6ce95db2383d27ff4d790b8f8b46704d360d701ab77c4f655bcfaa6a7/pybase64-1.4.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:ad15acf618880d99792d71e3905b0e2508e6e331b76a1b34212fa0f11e01ad28", size = 54999, upload-time = "2025-12-06T13:24:49.547Z" }, + { url = "https://files.pythonhosted.org/packages/64/e7/c3c1d09c3d7ae79e3aa1358c6d912d6b85f29281e47aa94fc0122a415a2f/pybase64-1.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:448158d417139cb4851200e5fee62677ae51f56a865d50cda9e0d61bda91b116", size = 58736, upload-time = "2025-12-06T13:24:50.641Z" }, + { url = "https://files.pythonhosted.org/packages/db/d5/0baa08e3d8119b15b588c39f0d39fd10472f0372e3c54ca44649cbefa256/pybase64-1.4.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:9058c49b5a2f3e691b9db21d37eb349e62540f9f5fc4beabf8cbe3c732bead86", size = 52298, upload-time = "2025-12-06T13:24:51.791Z" }, + { url = "https://files.pythonhosted.org/packages/00/87/fc6f11474a1de7e27cd2acbb8d0d7508bda3efa73dfe91c63f968728b2a3/pybase64-1.4.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ce561724f6522907a66303aca27dce252d363fcd85884972d348f4403ba3011a", size = 69049, upload-time = "2025-12-06T13:24:53.253Z" }, + { url = "https://files.pythonhosted.org/packages/69/9d/7fb5566f669ac18b40aa5fc1c438e24df52b843c1bdc5da47d46d4c1c630/pybase64-1.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:63316560a94ac449fe86cb8b9e0a13714c659417e92e26a5cbf085cd0a0c838d", size = 57952, upload-time = "2025-12-06T13:24:54.342Z" }, + { url = "https://files.pythonhosted.org/packages/de/cc/ceb949232dbbd3ec4ee0190d1df4361296beceee9840390a63df8bc31784/pybase64-1.4.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:7ecd796f2ac0be7b73e7e4e232b8c16422014de3295d43e71d2b19fd4a4f5368", size = 54484, upload-time = "2025-12-06T13:24:55.774Z" }, + { url = "https://files.pythonhosted.org/packages/a7/69/659f3c8e6a5d7b753b9c42a4bd9c42892a0f10044e9c7351a4148d413a33/pybase64-1.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d01e102a12fb2e1ed3dc11611c2818448626637857ec3994a9cf4809dfd23477", size = 56542, upload-time = "2025-12-06T13:24:57Z" }, + { url = "https://files.pythonhosted.org/packages/85/2c/29c9e6c9c82b72025f9676f9e82eb1fd2339ad038cbcbf8b9e2ac02798fc/pybase64-1.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ebff797a93c2345f22183f454fd8607a34d75eca5a3a4a969c1c75b304cee39d", size = 71045, upload-time = "2025-12-06T13:24:58.179Z" }, + { url = "https://files.pythonhosted.org/packages/b9/84/5a3dce8d7a0040a5c0c14f0fe1311cd8db872913fa04438071b26b0dac04/pybase64-1.4.3-cp314-cp314-win32.whl", hash = "sha256:28b2a1bb0828c0595dc1ea3336305cd97ff85b01c00d81cfce4f92a95fb88f56", size = 34200, upload-time = "2025-12-06T13:24:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/57/bc/ce7427c12384adee115b347b287f8f3cf65860b824d74fe2c43e37e81c1f/pybase64-1.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:33338d3888700ff68c3dedfcd49f99bfc3b887570206130926791e26b316b029", size = 36323, upload-time = "2025-12-06T13:25:01.708Z" }, + { url = "https://files.pythonhosted.org/packages/9a/1b/2b8ffbe9a96eef7e3f6a5a7be75995eebfb6faaedc85b6da6b233e50c778/pybase64-1.4.3-cp314-cp314-win_arm64.whl", hash = "sha256:62725669feb5acb186458da2f9353e88ae28ef66bb9c4c8d1568b12a790dfa94", size = 31584, upload-time = "2025-12-06T13:25:02.801Z" }, + { url = "https://files.pythonhosted.org/packages/ac/d8/6824c2e6fb45b8fa4e7d92e3c6805432d5edc7b855e3e8e1eedaaf6efb7c/pybase64-1.4.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:153fe29be038948d9372c3e77ae7d1cab44e4ba7d9aaf6f064dbeea36e45b092", size = 38601, upload-time = "2025-12-06T13:25:04.222Z" }, + { url = "https://files.pythonhosted.org/packages/ea/e5/10d2b3a4ad3a4850be2704a2f70cd9c0cf55725c8885679872d3bc846c67/pybase64-1.4.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f7fe3decaa7c4a9e162327ec7bd81ce183d2b16f23c6d53b606649c6e0203e9e", size = 32078, upload-time = "2025-12-06T13:25:05.362Z" }, + { url = "https://files.pythonhosted.org/packages/43/04/8b15c34d3c2282f1c1b0850f1113a249401b618a382646a895170bc9b5e7/pybase64-1.4.3-cp314-cp314t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:a5ae04ea114c86eb1da1f6e18d75f19e3b5ae39cb1d8d3cd87c29751a6a22780", size = 72474, upload-time = "2025-12-06T13:25:06.434Z" }, + { url = "https://files.pythonhosted.org/packages/42/00/f34b4d11278f8fdc68bc38f694a91492aa318f7c6f1bd7396197ac0f8b12/pybase64-1.4.3-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1755b3dce3a2a5c7d17ff6d4115e8bee4a1d5aeae74469db02e47c8f477147da", size = 75706, upload-time = "2025-12-06T13:25:07.636Z" }, + { url = "https://files.pythonhosted.org/packages/bb/5d/71747d4ad7fe16df4c4c852bdbdeb1f2cf35677b48d7c34d3011a7a6ad3a/pybase64-1.4.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fb852f900e27ffc4ec1896817535a0fa19610ef8875a096b59f21d0aa42ff172", size = 65589, upload-time = "2025-12-06T13:25:08.809Z" }, + { url = "https://files.pythonhosted.org/packages/49/b1/d1e82bd58805bb5a3a662864800bab83a83a36ba56e7e3b1706c708002a5/pybase64-1.4.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:9cf21ea8c70c61eddab3421fbfce061fac4f2fb21f7031383005a1efdb13d0b9", size = 60670, upload-time = "2025-12-06T13:25:10.04Z" }, + { url = "https://files.pythonhosted.org/packages/15/67/16c609b7a13d1d9fc87eca12ba2dce5e67f949eeaab61a41bddff843cbb0/pybase64-1.4.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:afff11b331fdc27692fc75e85ae083340a35105cea1a3c4552139e2f0e0d174f", size = 64194, upload-time = "2025-12-06T13:25:11.48Z" }, + { url = "https://files.pythonhosted.org/packages/3c/11/37bc724e42960f0106c2d33dc957dcec8f760c91a908cc6c0df7718bc1a8/pybase64-1.4.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9a5143df542c1ce5c1f423874b948c4d689b3f05ec571f8792286197a39ba02", size = 64984, upload-time = "2025-12-06T13:25:12.645Z" }, + { url = "https://files.pythonhosted.org/packages/6e/66/b2b962a6a480dd5dae3029becf03ea1a650d326e39bf1c44ea3db78bb010/pybase64-1.4.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:d62e9861019ad63624b4a7914dff155af1cc5d6d79df3be14edcaedb5fdad6f9", size = 58750, upload-time = "2025-12-06T13:25:13.848Z" }, + { url = "https://files.pythonhosted.org/packages/2b/15/9b6d711035e29b18b2e1c03d47f41396d803d06ef15b6c97f45b75f73f04/pybase64-1.4.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:84cfd4d92668ef5766cc42a9c9474b88960ac2b860767e6e7be255c6fddbd34a", size = 63816, upload-time = "2025-12-06T13:25:15.356Z" }, + { url = "https://files.pythonhosted.org/packages/b4/21/e2901381ed0df62e2308380f30d9c4d87d6b74e33a84faed3478d33a7197/pybase64-1.4.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:60fc025437f9a7c2cc45e0c19ed68ed08ba672be2c5575fd9d98bdd8f01dd61f", size = 56348, upload-time = "2025-12-06T13:25:16.559Z" }, + { url = "https://files.pythonhosted.org/packages/c4/16/3d788388a178a0407aa814b976fe61bfa4af6760d9aac566e59da6e4a8b4/pybase64-1.4.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:edc8446196f04b71d3af76c0bd1fe0a45066ac5bffecca88adb9626ee28c266f", size = 72842, upload-time = "2025-12-06T13:25:18.055Z" }, + { url = "https://files.pythonhosted.org/packages/a6/63/c15b1f8bd47ea48a5a2d52a4ec61f037062932ea6434ab916107b58e861e/pybase64-1.4.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e99f6fa6509c037794da57f906ade271f52276c956d00f748e5b118462021d48", size = 62651, upload-time = "2025-12-06T13:25:19.191Z" }, + { url = "https://files.pythonhosted.org/packages/bd/b8/f544a2e37c778d59208966d4ef19742a0be37c12fc8149ff34483c176616/pybase64-1.4.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d94020ef09f624d841aa9a3a6029df8cf65d60d7a6d5c8687579fa68bd679b65", size = 58295, upload-time = "2025-12-06T13:25:20.822Z" }, + { url = "https://files.pythonhosted.org/packages/03/99/1fae8a3b7ac181e36f6e7864a62d42d5b1f4fa7edf408c6711e28fba6b4d/pybase64-1.4.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:f64ce70d89942a23602dee910dec9b48e5edf94351e1b378186b74fcc00d7f66", size = 60960, upload-time = "2025-12-06T13:25:22.099Z" }, + { url = "https://files.pythonhosted.org/packages/9d/9e/cd4c727742345ad8384569a4466f1a1428f4e5cc94d9c2ab2f53d30be3fe/pybase64-1.4.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8ea99f56e45c469818b9781903be86ba4153769f007ba0655fa3b46dc332803d", size = 74863, upload-time = "2025-12-06T13:25:23.442Z" }, + { url = "https://files.pythonhosted.org/packages/28/86/a236ecfc5b494e1e922da149689f690abc84248c7c1358f5605b8c9fdd60/pybase64-1.4.3-cp314-cp314t-win32.whl", hash = "sha256:343b1901103cc72362fd1f842524e3bb24978e31aea7ff11e033af7f373f66ab", size = 34513, upload-time = "2025-12-06T13:25:24.592Z" }, + { url = "https://files.pythonhosted.org/packages/56/ce/ca8675f8d1352e245eb012bfc75429ee9cf1f21c3256b98d9a329d44bf0f/pybase64-1.4.3-cp314-cp314t-win_amd64.whl", hash = "sha256:57aff6f7f9dea6705afac9d706432049642de5b01080d3718acc23af87c5af76", size = 36702, upload-time = "2025-12-06T13:25:25.72Z" }, + { url = "https://files.pythonhosted.org/packages/3b/30/4a675864877397179b09b720ee5fcb1cf772cf7bebc831989aff0a5f79c1/pybase64-1.4.3-cp314-cp314t-win_arm64.whl", hash = "sha256:e906aa08d4331e799400829e0f5e4177e76a3281e8a4bc82ba114c6b30e405c9", size = 31904, upload-time = "2025-12-06T13:25:26.826Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[package.optional-dependencies] +email = [ + { name = "email-validator" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, +] + +[[package]] +name = "pydantic-extra-types" +version = "2.11.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/41/d3/3be31542180c0300b6860129ff1e3a428f3ef580727616ce22462626129b/pydantic_extra_types-2.11.2.tar.gz", hash = "sha256:3a2b83b61fe920925688e7838b59caa90a45637d1dbba2b1364b8d1f7ff72a0a", size = 203929, upload-time = "2026-04-05T20:50:51.556Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/a4/7b6ab05c18d6c6e682382a0f0235301684452c4131a869f45961d1d032c9/pydantic_extra_types-2.11.2-py3-none-any.whl", hash = "sha256:683b8943252543e49760f89733b1519bc62f31d1a287ebbdc5a7b7959fb4acfd", size = 82851, upload-time = "2026-04-05T20:50:50.036Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.13.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/6d/fffca34caecc4a3f97bda81b2098da5e8ab7efc9a66e819074a11955d87e/pydantic_settings-2.13.1.tar.gz", hash = "sha256:b4c11847b15237fb0171e1462bf540e294affb9b86db4d9aa5c01730bdbe4025", size = 223826, upload-time = "2026-02-19T13:45:08.055Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/4b/ccc026168948fec4f7555b9164c724cf4125eac006e176541483d2c959be/pydantic_settings-2.13.1-py3-none-any.whl", hash = "sha256:d56fd801823dbeae7f0975e1f8c8e25c258eb75d278ea7abb5d9cebb01b56237", size = 58929, upload-time = "2026-02-19T13:45:06.034Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pymysql" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/bc/1c6a92f385940f727daeecf3bacaf186e03875dff57197801046c583bcf0/pymysql-1.2.0.tar.gz", hash = "sha256:6c7b17ca686988104d7426c27895b455cdeea3e9d3ceb1270f0c3704fead8c33", size = 49021, upload-time = "2026-05-19T08:26:22.302Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/bd/2534e130295c8cfd4f0a2e31623baab7502278f1e97bcfe61db75656a77f/pymysql-1.2.0-py3-none-any.whl", hash = "sha256:62169ce6d5510f08e140c5e7990ee884a9764024e4a9a27b2cc11f1099322ae0", size = 45716, upload-time = "2026-05-19T08:26:20.974Z" }, +] + +[[package]] +name = "pyparsing" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, +] + +[[package]] +name = "pypika" +version = "0.51.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/78/cbaebba88e05e2dcda13ca203131b38d3640219f20ebb49676d26714861b/pypika-0.51.1.tar.gz", hash = "sha256:c30c7c1048fbf056fd3920c5a2b88b0c29dd190a9b2bee971fd17e4abe4d0ebe", size = 80919, upload-time = "2026-02-04T11:27:48.304Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/83/c77dfeed04022e8930b08eedca2b6e5efed256ab3321396fde90066efb65/pypika-0.51.1-py2.py3-none-any.whl", hash = "sha256:77985b4d7ce71b9905255bf12468cf598349e98837c037541cfc240e528aec46", size = 60585, upload-time = "2026-02-04T11:27:46.251Z" }, +] + +[[package]] +name = "pyproject-hooks" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/82/28175b2414effca1cdac8dc99f76d660e7a4fb0ceefa4b4ab8f5f6742925/pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8", size = 19228, upload-time = "2024-09-29T09:24:13.293Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913", size = 10216, upload-time = "2024-09-29T09:24:11.978Z" }, +] + +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.24" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/45/e23b5dc14ddb9918ae4a625379506b17b6f8fc56ca1d82db62462f59aea6/python_multipart-0.0.24.tar.gz", hash = "sha256:9574c97e1c026e00bc30340ef7c7d76739512ab4dfd428fec8c330fa6a5cc3c8", size = 37695, upload-time = "2026-04-05T20:49:13.829Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/73/89930efabd4da63cea44a3f438aeb753d600123570e6d6264e763617a9ce/python_multipart-0.0.24-py3-none-any.whl", hash = "sha256:9b110a98db707df01a53c194f0af075e736a770dc5058089650d70b4a182f950", size = 24420, upload-time = "2026-04-05T20:49:12.555Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "rapidfuzz" +version = "3.14.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/21/ef6157213316e85790041254259907eb722e00b03480256c0545d98acd33/rapidfuzz-3.14.5.tar.gz", hash = "sha256:ba10ac57884ce82112f7ed910b67e7fb6072d8ef2c06e30dc63c0f604a112e0e", size = 57901753, upload-time = "2026-04-07T11:16:31.931Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/41/aa3ffb3355e62e1bf91f6599b3092e866bc88487a07c524004943c7676df/rapidfuzz-3.14.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1a31cc6d7d03e7318a0974c038959c59e19c752b81115f2e9138b3331cd64d45", size = 1943327, upload-time = "2026-04-07T11:15:26.266Z" }, + { url = "https://files.pythonhosted.org/packages/2d/e1/c2141f1840a41e07ad2db6f724945f8f8ff3065463899a22939152dd6e09/rapidfuzz-3.14.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0298d357e2bc59d572da4db0bc631009b6f8f6c9bc8c11e99a12b833f16b6575", size = 1161755, upload-time = "2026-04-07T11:15:28.659Z" }, + { url = "https://files.pythonhosted.org/packages/ca/07/66e753eeaa353161d1d331b7dd517bb349b0bacfebe8496d7b26be26f81f/rapidfuzz-3.14.5-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:59b3dba758661a318995655435c6ab20a04ade79fa51e75bc8dc107cac8df280", size = 1376571, upload-time = "2026-04-07T11:15:31.225Z" }, + { url = "https://files.pythonhosted.org/packages/c8/85/9535df0b78ba51f478c9ce7eb6d1f85535cc31fe356773b48fd9d3e563ca/rapidfuzz-3.14.5-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4900143d82071bdda533b00300c40b14b963ff826b3642cc463b6dd0f036585e", size = 3156468, upload-time = "2026-04-07T11:15:33.428Z" }, + { url = "https://files.pythonhosted.org/packages/81/ee/b667eb93bba6dc4e0de658edd778e1619dc4d6aab68fa5e5c7f075152735/rapidfuzz-3.14.5-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:feedf219672eef83ea6be6f3bb093bba396a8560fc75be85ba225f082903df0a", size = 1458311, upload-time = "2026-04-07T11:15:35.557Z" }, + { url = "https://files.pythonhosted.org/packages/7d/ce/479074f5624364a48df3403c538797ef22d3ac49c19dc76c3f79fcdcc70c/rapidfuzz-3.14.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:419e4397a36e2665ec992d8d64c20ba4b2a42500c76ecadeca78a4f19cb9cc32", size = 2398228, upload-time = "2026-04-07T11:15:37.669Z" }, + { url = "https://files.pythonhosted.org/packages/0b/15/a8982f649150fffbdcd6f17565974501f6ab33b2795267bffbd4a7ba905b/rapidfuzz-3.14.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:97131ab2be39043054ee28d99e09efe316e6d53449b7e962dfcf3c2de8b2b246", size = 2497226, upload-time = "2026-04-07T11:15:39.857Z" }, + { url = "https://files.pythonhosted.org/packages/19/52/5267c03ef6759831b7d4625a0c9c06e87baa2fae084b61ac9c388858317b/rapidfuzz-3.14.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:593c00dac4e30231c35bf3b4f1da8ec0998762e9e94425586a5d636fcd57f9d0", size = 4262283, upload-time = "2026-04-07T11:15:42.279Z" }, + { url = "https://files.pythonhosted.org/packages/71/c0/2579f343a97f5254c43bb5853baccc01488357dcb64a27bcb869b7888a4a/rapidfuzz-3.14.5-cp314-cp314-win32.whl", hash = "sha256:0084b687b02b4e569b46d8d6d4ad25659528e6081cd6d067ca453a69035f07e4", size = 1744614, upload-time = "2026-04-07T11:15:44.498Z" }, + { url = "https://files.pythonhosted.org/packages/17/eb/8edfed1e80119dc9c35b11df4bc701eea85622ad681fff0263b6961d3224/rapidfuzz-3.14.5-cp314-cp314-win_amd64.whl", hash = "sha256:5dfa89d78f22cd773054caff44827b846161a29f2dcf7e78b8f90d086621e502", size = 1588971, upload-time = "2026-04-07T11:15:46.86Z" }, + { url = "https://files.pythonhosted.org/packages/f6/04/5676df93c85cfa57a3045d8047318df9f3cd58c7b8a99340dd95f874795e/rapidfuzz-3.14.5-cp314-cp314-win_arm64.whl", hash = "sha256:67f3f9d2b444268ab53e47d31bab89954888d23c04c6789f2c727e51fe4b1d13", size = 834985, upload-time = "2026-04-07T11:15:49.411Z" }, + { url = "https://files.pythonhosted.org/packages/f7/0d/4a8988cea658fe335048ddef8c876addff1b6daa3c9ca8ad65a5a2196e69/rapidfuzz-3.14.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:77eac0526899b3c3ad1454bb2b03cdb491d67358ec8ef0c9c48bd61b632b431d", size = 1972517, upload-time = "2026-04-07T11:15:51.819Z" }, + { url = "https://files.pythonhosted.org/packages/1c/a3/f5cfd9965a9d9a9e32249159797c47b5d6299ea6d1629f9126b25f1c10a3/rapidfuzz-3.14.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b9c6bd754d11f6e78ac54e3d86b4b11dc1ba2f13e5fc958899574532897f5a99", size = 1196056, upload-time = "2026-04-07T11:15:54.292Z" }, + { url = "https://files.pythonhosted.org/packages/64/07/561c2e40cfd10e6630a7b0ac5a2a813aef50d944bcd1f3d260319d659d5b/rapidfuzz-3.14.5-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:738c96944d076deeaff70e92b65696ab4f7ecb8081d7791c5403a3257dfaf8ff", size = 1374732, upload-time = "2026-04-07T11:15:56.584Z" }, + { url = "https://files.pythonhosted.org/packages/c2/39/123bb94fee40e2fb3b7c49b80827c7ef42d838e18def3fc2fef5a3cf817a/rapidfuzz-3.14.5-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4c1bca487a17fe4226b4ffb2d30e799d2b274d692cffa76bd0746f56235fca3", size = 3166902, upload-time = "2026-04-07T11:15:58.768Z" }, + { url = "https://files.pythonhosted.org/packages/75/0a/45716fafc9fd2e028cf20b5ac5bc704887081cd312f84edb0e325599414b/rapidfuzz-3.14.5-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:af6a90a4ed2a48fa1a2d17e9d824e6c7c950bea5bad0b707c77fd55751e6bfef", size = 1452130, upload-time = "2026-04-07T11:16:01.453Z" }, + { url = "https://files.pythonhosted.org/packages/ca/49/4e96c413114398481c0a5b0086af32c364a18613c9a2ea578d17c4bea4ee/rapidfuzz-3.14.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bf5018938208d4597b2e679a4f8cff9fd252f1df53583130ae56281a21801b64", size = 2396308, upload-time = "2026-04-07T11:16:03.588Z" }, + { url = "https://files.pythonhosted.org/packages/89/b7/49fea9fc6878d59bd259d01dd1972d9b86117992b1c66d9b16f0a65273c3/rapidfuzz-3.14.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c0919d1f89ddf91129906705723118ea09754171e4116f5a5dbc667c7bc9b261", size = 2488210, upload-time = "2026-04-07T11:16:05.871Z" }, + { url = "https://files.pythonhosted.org/packages/0c/44/a1f732b93ffacbdad077b7c801149549b2938e1bece6addb5ad85ed74df8/rapidfuzz-3.14.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:93d8da883a35116d6813432177f35e570db5b0a5e30ecb0cbd7cb39c815735df", size = 4270621, upload-time = "2026-04-07T11:16:08.483Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ce/ff942d19fce5385054650bb71a58495ddda299d94661ccc4e6e7fa44868b/rapidfuzz-3.14.5-cp314-cp314t-win32.whl", hash = "sha256:0f23e37019ec07712d58976b1ab2b889f8649a7f7c2f626a2f34ea9139e79279", size = 1803950, upload-time = "2026-04-07T11:16:10.873Z" }, + { url = "https://files.pythonhosted.org/packages/5c/0f/9aafc63f9661222b819b391c187eed29fc90ad5935f9690e5ecc2d2047a4/rapidfuzz-3.14.5-cp314-cp314t-win_amd64.whl", hash = "sha256:7d5ca9c7832e6879a707296d1463685f7c243a27846227044504741640caec66", size = 1632357, upload-time = "2026-04-07T11:16:13.1Z" }, + { url = "https://files.pythonhosted.org/packages/70/a6/51fc1b0e61e3326e1c68a61cfd0c6b3c34c843681c4b1eefbf0596f59162/rapidfuzz-3.14.5-cp314-cp314t-win_arm64.whl", hash = "sha256:3e91dcd2549b8f8d843f98ba03a17e01f3d8b72ce942adbbb6761bc58ffce813", size = 855409, upload-time = "2026-04-07T11:16:15.787Z" }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + +[[package]] +name = "regex" +version = "2026.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cb/0e/3a246dbf05666918bd3664d9d787f84a9108f6f43cc953a077e4a7dfdb7e/regex-2026.4.4.tar.gz", hash = "sha256:e08270659717f6973523ce3afbafa53515c4dc5dcad637dc215b6fd50f689423", size = 416000, upload-time = "2026-04-03T20:56:28.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/f5/ed97c2dc47b5fbd4b73c0d7d75f9ebc8eca139f2bbef476bba35f28c0a77/regex-2026.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:2da82d643fa698e5e5210e54af90181603d5853cf469f5eedf9bfc8f59b4b8c7", size = 490343, upload-time = "2026-04-03T20:55:15.241Z" }, + { url = "https://files.pythonhosted.org/packages/80/e9/de4828a7385ec166d673a5790ad06ac48cdaa98bc0960108dd4b9cc1aef7/regex-2026.4.4-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:54a1189ad9d9357760557c91103d5e421f0a2dabe68a5cdf9103d0dcf4e00752", size = 291909, upload-time = "2026-04-03T20:55:17.558Z" }, + { url = "https://files.pythonhosted.org/packages/b4/d6/5cfbfc97f3201a4d24b596a77957e092030dcc4205894bc035cedcfce62f/regex-2026.4.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:76d67d5afb1fe402d10a6403bae668d000441e2ab115191a804287d53b772951", size = 289692, upload-time = "2026-04-03T20:55:20.561Z" }, + { url = "https://files.pythonhosted.org/packages/8e/ac/f2212d9fd56fe897e36d0110ba30ba2d247bd6410c5bd98499c7e5a1e1f2/regex-2026.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e7cd3e4ee8d80447a83bbc9ab0c8459781fa77087f856c3e740d7763be0df27f", size = 796979, upload-time = "2026-04-03T20:55:22.56Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e3/a016c12675fbac988a60c7e1c16e67823ff0bc016beb27bd7a001dbdabc6/regex-2026.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e19e18c568d2866d8b6a6dfad823db86193503f90823a8f66689315ba28fbe8", size = 866744, upload-time = "2026-04-03T20:55:24.646Z" }, + { url = "https://files.pythonhosted.org/packages/af/a4/0b90ca4cf17adc3cb43de80ec71018c37c88ad64987e8d0d481a95ca60b5/regex-2026.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7698a6f38730fd1385d390d1ed07bb13dce39aa616aca6a6d89bea178464b9a4", size = 911613, upload-time = "2026-04-03T20:55:27.033Z" }, + { url = "https://files.pythonhosted.org/packages/8e/3b/2b3dac0b82d41ab43aa87c6ecde63d71189d03fe8854b8ca455a315edac3/regex-2026.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:173a66f3651cdb761018078e2d9487f4cf971232c990035ec0eb1cdc6bf929a9", size = 800551, upload-time = "2026-04-03T20:55:29.532Z" }, + { url = "https://files.pythonhosted.org/packages/25/fe/5365eb7aa0e753c4b5957815c321519ecab033c279c60e1b1ae2367fa810/regex-2026.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa7922bbb2cc84fa062d37723f199d4c0cd200245ce269c05db82d904db66b83", size = 776911, upload-time = "2026-04-03T20:55:31.526Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b3/7fb0072156bba065e3b778a7bc7b0a6328212be5dd6a86fd207e0c4f2dab/regex-2026.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:59f67cd0a0acaf0e564c20bbd7f767286f23e91e2572c5703bf3e56ea7557edb", size = 785751, upload-time = "2026-04-03T20:55:33.797Z" }, + { url = "https://files.pythonhosted.org/packages/02/1a/9f83677eb699273e56e858f7bd95acdbee376d42f59e8bfca2fd80d79df3/regex-2026.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:475e50f3f73f73614f7cba5524d6de49dee269df00272a1b85e3d19f6d498465", size = 860484, upload-time = "2026-04-03T20:55:35.745Z" }, + { url = "https://files.pythonhosted.org/packages/3b/7a/93937507b61cfcff8b4c5857f1b452852b09f741daa9acae15c971d8554e/regex-2026.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:a1c0c7d67b64d85ac2e1879923bad2f08a08f3004055f2f406ef73c850114bd4", size = 765939, upload-time = "2026-04-03T20:55:37.972Z" }, + { url = "https://files.pythonhosted.org/packages/86/ea/81a7f968a351c6552b1670ead861e2a385be730ee28402233020c67f9e0f/regex-2026.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:1371c2ccbb744d66ee63631cc9ca12aa233d5749972626b68fe1a649dd98e566", size = 851417, upload-time = "2026-04-03T20:55:39.92Z" }, + { url = "https://files.pythonhosted.org/packages/4c/7e/323c18ce4b5b8f44517a36342961a0306e931e499febbd876bb149d900f0/regex-2026.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:59968142787042db793348a3f5b918cf24ced1f23247328530e063f89c128a95", size = 789056, upload-time = "2026-04-03T20:55:42.303Z" }, + { url = "https://files.pythonhosted.org/packages/c0/af/e7510f9b11b1913b0cd44eddb784b2d650b2af6515bfce4cffcc5bfd1d38/regex-2026.4.4-cp314-cp314-win32.whl", hash = "sha256:59efe72d37fd5a91e373e5146f187f921f365f4abc1249a5ab446a60f30dd5f8", size = 272130, upload-time = "2026-04-03T20:55:44.995Z" }, + { url = "https://files.pythonhosted.org/packages/9a/51/57dae534c915e2d3a21490e88836fa2ae79dde3b66255ecc0c0a155d2c10/regex-2026.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:e0aab3ff447845049d676827d2ff714aab4f73f340e155b7de7458cf53baa5a4", size = 280992, upload-time = "2026-04-03T20:55:47.316Z" }, + { url = "https://files.pythonhosted.org/packages/0a/5e/abaf9f4c3792e34edb1434f06717fae2b07888d85cb5cec29f9204931bf8/regex-2026.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:a7a5bb6aa0cf62208bb4fa079b0c756734f8ad0e333b425732e8609bd51ee22f", size = 273563, upload-time = "2026-04-03T20:55:49.273Z" }, + { url = "https://files.pythonhosted.org/packages/ff/06/35da85f9f217b9538b99cbb170738993bcc3b23784322decb77619f11502/regex-2026.4.4-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:97850d0638391bdc7d35dc1c1039974dcb921eaafa8cc935ae4d7f272b1d60b3", size = 494191, upload-time = "2026-04-03T20:55:51.258Z" }, + { url = "https://files.pythonhosted.org/packages/54/5b/1bc35f479eef8285c4baf88d8c002023efdeebb7b44a8735b36195486ae7/regex-2026.4.4-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ee7337f88f2a580679f7bbfe69dc86c043954f9f9c541012f49abc554a962f2e", size = 293877, upload-time = "2026-04-03T20:55:53.214Z" }, + { url = "https://files.pythonhosted.org/packages/39/5b/f53b9ad17480b3ddd14c90da04bfb55ac6894b129e5dea87bcaf7d00e336/regex-2026.4.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7429f4e6192c11d659900c0648ba8776243bf396ab95558b8c51a345afeddde6", size = 292410, upload-time = "2026-04-03T20:55:55.736Z" }, + { url = "https://files.pythonhosted.org/packages/bb/56/52377f59f60a7c51aa4161eecf0b6032c20b461805aca051250da435ffc9/regex-2026.4.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4f10fbd5dd13dcf4265b4cc07d69ca70280742870c97ae10093e3d66000359", size = 811831, upload-time = "2026-04-03T20:55:57.802Z" }, + { url = "https://files.pythonhosted.org/packages/dd/63/8026310bf066f702a9c361f83a8c9658f3fe4edb349f9c1e5d5273b7c40c/regex-2026.4.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a152560af4f9742b96f3827090f866eeec5becd4765c8e0d3473d9d280e76a5a", size = 871199, upload-time = "2026-04-03T20:56:00.333Z" }, + { url = "https://files.pythonhosted.org/packages/20/9f/a514bbb00a466dbb506d43f187a04047f7be1505f10a9a15615ead5080ee/regex-2026.4.4-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54170b3e95339f415d54651f97df3bff7434a663912f9358237941bbf9143f55", size = 917649, upload-time = "2026-04-03T20:56:02.445Z" }, + { url = "https://files.pythonhosted.org/packages/cb/6b/8399f68dd41a2030218839b9b18360d79b86d22b9fab5ef477c7f23ca67c/regex-2026.4.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:07f190d65f5a72dcb9cf7106bfc3d21e7a49dd2879eda2207b683f32165e4d99", size = 816388, upload-time = "2026-04-03T20:56:04.595Z" }, + { url = "https://files.pythonhosted.org/packages/1e/9c/103963f47c24339a483b05edd568594c2be486188f688c0170fd504b2948/regex-2026.4.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9a2741ce5a29d3c84b0b94261ba630ab459a1b847a0d6beca7d62d188175c790", size = 785746, upload-time = "2026-04-03T20:56:07.13Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ee/7f6054c0dec0cee3463c304405e4ff42e27cff05bf36fcb34be549ab17bd/regex-2026.4.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b26c30df3a28fd9793113dac7385a4deb7294a06c0f760dd2b008bd49a9139bc", size = 801483, upload-time = "2026-04-03T20:56:09.365Z" }, + { url = "https://files.pythonhosted.org/packages/30/c2/51d3d941cf6070dc00c3338ecf138615fc3cce0421c3df6abe97a08af61a/regex-2026.4.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:421439d1bee44b19f4583ccf42670ca464ffb90e9fdc38d37f39d1ddd1e44f1f", size = 866331, upload-time = "2026-04-03T20:56:12.039Z" }, + { url = "https://files.pythonhosted.org/packages/16/e8/76d50dcc122ac33927d939f350eebcfe3dbcbda96913e03433fc36de5e63/regex-2026.4.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:b40379b53ecbc747fd9bdf4a0ea14eb8188ca1bd0f54f78893a39024b28f4863", size = 772673, upload-time = "2026-04-03T20:56:14.558Z" }, + { url = "https://files.pythonhosted.org/packages/a5/6e/5f6bf75e20ea6873d05ba4ec78378c375cbe08cdec571c83fbb01606e563/regex-2026.4.4-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:08c55c13d2eef54f73eeadc33146fb0baaa49e7335eb1aff6ae1324bf0ddbe4a", size = 857146, upload-time = "2026-04-03T20:56:16.663Z" }, + { url = "https://files.pythonhosted.org/packages/0b/33/3c76d9962949e487ebba353a18e89399f292287204ac8f2f4cfc3a51c233/regex-2026.4.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9776b85f510062f5a75ef112afe5f494ef1635607bf1cc220c1391e9ac2f5e81", size = 803463, upload-time = "2026-04-03T20:56:18.923Z" }, + { url = "https://files.pythonhosted.org/packages/19/eb/ef32dcd2cb69b69bc0c3e55205bce94a7def48d495358946bc42186dcccc/regex-2026.4.4-cp314-cp314t-win32.whl", hash = "sha256:385edaebde5db5be103577afc8699fea73a0e36a734ba24870be7ffa61119d74", size = 275709, upload-time = "2026-04-03T20:56:20.996Z" }, + { url = "https://files.pythonhosted.org/packages/a0/86/c291bf740945acbf35ed7dbebf8e2eea2f3f78041f6bd7cdab80cb274dc0/regex-2026.4.4-cp314-cp314t-win_amd64.whl", hash = "sha256:5d354b18839328927832e2fa5f7c95b7a3ccc39e7a681529e1685898e6436d45", size = 285622, upload-time = "2026-04-03T20:56:23.641Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e7/ec846d560ae6a597115153c02ca6138a7877a1748b2072d9521c10a93e58/regex-2026.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:af0384cb01a33600c49505c27c6c57ab0b27bf84a74e28524c92ca897ebdac9d", size = 275773, upload-time = "2026-04-03T20:56:26.07Z" }, +] + +[[package]] +name = "requests" +version = "2.33.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" }, +] + +[[package]] +name = "requests-file" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3c/f8/5dc70102e4d337063452c82e1f0d95e39abfe67aa222ed8a5ddeb9df8de8/requests_file-3.0.1.tar.gz", hash = "sha256:f14243d7796c588f3521bd423c5dea2ee4cc730e54a3cac9574d78aca1272576", size = 6967, upload-time = "2025-10-20T18:56:42.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/d5/de8f089119205a09da657ed4784c584ede8381a0ce6821212a6d4ca47054/requests_file-3.0.1-py2.py3-none-any.whl", hash = "sha256:d0f5eb94353986d998f80ac63c7f146a307728be051d4d1cd390dbdb59c10fa2", size = 4514, upload-time = "2025-10-20T18:56:41.184Z" }, +] + +[[package]] +name = "requests-oauthlib" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "oauthlib" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/f2/05f29bc3913aea15eb670be136045bf5c5bbf4b99ecb839da9b422bb2c85/requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9", size = 55650, upload-time = "2024-03-22T20:32:29.939Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179, upload-time = "2024-03-22T20:32:28.055Z" }, +] + +[[package]] +name = "requests-toolbelt" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" }, +] + +[[package]] +name = "rich" +version = "14.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" }, +] + +[[package]] +name = "rich-toolkit" +version = "0.19.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/ba/dae9e3096651042754da419a4042bc1c75e07d615f9b15066d738838e4df/rich_toolkit-0.19.7.tar.gz", hash = "sha256:133c0915872da91d4c25d85342d5ec1dfacc69b63448af1a08a0d4b4f23ef46e", size = 195877, upload-time = "2026-02-24T16:06:20.555Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/3c/c923619f6d2f5fafcc96fec0aaf9550a46cd5b6481f06e0c6b66a2a4fed0/rich_toolkit-0.19.7-py3-none-any.whl", hash = "sha256:0288e9203728c47c5a4eb60fd2f0692d9df7455a65901ab6f898437a2ba5989d", size = 32963, upload-time = "2026-02-24T16:06:22.066Z" }, +] + +[[package]] +name = "rignore" +version = "0.7.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/f5/8bed2310abe4ae04b67a38374a4d311dd85220f5d8da56f47ae9361be0b0/rignore-0.7.6.tar.gz", hash = "sha256:00d3546cd793c30cb17921ce674d2c8f3a4b00501cb0e3dd0e82217dbeba2671", size = 57140, upload-time = "2025-11-05T21:41:21.968Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/b9/1f5bd82b87e5550cd843ceb3768b4a8ef274eb63f29333cf2f29644b3d75/rignore-0.7.6-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:8e41be9fa8f2f47239ded8920cc283699a052ac4c371f77f5ac017ebeed75732", size = 882632, upload-time = "2025-11-05T20:42:44.063Z" }, + { url = "https://files.pythonhosted.org/packages/e9/6b/07714a3efe4a8048864e8a5b7db311ba51b921e15268b17defaebf56d3db/rignore-0.7.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6dc1e171e52cefa6c20e60c05394a71165663b48bca6c7666dee4f778f2a7d90", size = 820760, upload-time = "2025-11-05T20:42:27.885Z" }, + { url = "https://files.pythonhosted.org/packages/ac/0f/348c829ea2d8d596e856371b14b9092f8a5dfbb62674ec9b3f67e4939a9d/rignore-0.7.6-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ce2268837c3600f82ab8db58f5834009dc638ee17103582960da668963bebc5", size = 899044, upload-time = "2025-11-05T20:40:55.336Z" }, + { url = "https://files.pythonhosted.org/packages/f0/30/2e1841a19b4dd23878d73edd5d82e998a83d5ed9570a89675f140ca8b2ad/rignore-0.7.6-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:690a3e1b54bfe77e89c4bacb13f046e642f8baadafc61d68f5a726f324a76ab6", size = 874144, upload-time = "2025-11-05T20:41:10.195Z" }, + { url = "https://files.pythonhosted.org/packages/c2/bf/0ce9beb2e5f64c30e3580bef09f5829236889f01511a125f98b83169b993/rignore-0.7.6-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09d12ac7a0b6210c07bcd145007117ebd8abe99c8eeb383e9e4673910c2754b2", size = 1168062, upload-time = "2025-11-05T20:41:26.511Z" }, + { url = "https://files.pythonhosted.org/packages/b9/8b/571c178414eb4014969865317da8a02ce4cf5241a41676ef91a59aab24de/rignore-0.7.6-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2a2b2b74a8c60203b08452479b90e5ce3dbe96a916214bc9eb2e5af0b6a9beb0", size = 942542, upload-time = "2025-11-05T20:41:41.838Z" }, + { url = "https://files.pythonhosted.org/packages/19/62/7a3cf601d5a45137a7e2b89d10c05b5b86499190c4b7ca5c3c47d79ee519/rignore-0.7.6-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fc5a531ef02131e44359419a366bfac57f773ea58f5278c2cdd915f7d10ea94", size = 958739, upload-time = "2025-11-05T20:42:12.463Z" }, + { url = "https://files.pythonhosted.org/packages/5f/1f/4261f6a0d7caf2058a5cde2f5045f565ab91aa7badc972b57d19ce58b14e/rignore-0.7.6-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b7a1f77d9c4cd7e76229e252614d963442686bfe12c787a49f4fe481df49e7a9", size = 984138, upload-time = "2025-11-05T20:41:56.775Z" }, + { url = "https://files.pythonhosted.org/packages/2b/bf/628dfe19c75e8ce1f45f7c248f5148b17dfa89a817f8e3552ab74c3ae812/rignore-0.7.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ead81f728682ba72b5b1c3d5846b011d3e0174da978de87c61645f2ed36659a7", size = 1079299, upload-time = "2025-11-05T21:40:16.639Z" }, + { url = "https://files.pythonhosted.org/packages/af/a5/be29c50f5c0c25c637ed32db8758fdf5b901a99e08b608971cda8afb293b/rignore-0.7.6-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:12ffd50f520c22ffdabed8cd8bfb567d9ac165b2b854d3e679f4bcaef11a9441", size = 1139618, upload-time = "2025-11-05T21:40:34.507Z" }, + { url = "https://files.pythonhosted.org/packages/2a/40/3c46cd7ce4fa05c20b525fd60f599165e820af66e66f2c371cd50644558f/rignore-0.7.6-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:e5a16890fbe3c894f8ca34b0fcacc2c200398d4d46ae654e03bc9b3dbf2a0a72", size = 1117626, upload-time = "2025-11-05T21:40:51.494Z" }, + { url = "https://files.pythonhosted.org/packages/8c/b9/aea926f263b8a29a23c75c2e0d8447965eb1879d3feb53cfcf84db67ed58/rignore-0.7.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3abab3bf99e8a77488ef6c7c9a799fac22224c28fe9f25cc21aa7cc2b72bfc0b", size = 1128144, upload-time = "2025-11-05T21:41:09.169Z" }, + { url = "https://files.pythonhosted.org/packages/a4/f6/0d6242f8d0df7f2ecbe91679fefc1f75e7cd2072cb4f497abaab3f0f8523/rignore-0.7.6-cp314-cp314-win32.whl", hash = "sha256:eeef421c1782953c4375aa32f06ecae470c1285c6381eee2a30d2e02a5633001", size = 646385, upload-time = "2025-11-05T21:41:55.105Z" }, + { url = "https://files.pythonhosted.org/packages/d5/38/c0dcd7b10064f084343d6af26fe9414e46e9619c5f3224b5272e8e5d9956/rignore-0.7.6-cp314-cp314-win_amd64.whl", hash = "sha256:6aeed503b3b3d5af939b21d72a82521701a4bd3b89cd761da1e7dc78621af304", size = 725738, upload-time = "2025-11-05T21:41:39.736Z" }, + { url = "https://files.pythonhosted.org/packages/d9/7a/290f868296c1ece914d565757ab363b04730a728b544beb567ceb3b2d96f/rignore-0.7.6-cp314-cp314-win_arm64.whl", hash = "sha256:104f215b60b3c984c386c3e747d6ab4376d5656478694e22c7bd2f788ddd8304", size = 656008, upload-time = "2025-11-05T21:41:29.028Z" }, + { url = "https://files.pythonhosted.org/packages/ca/d2/3c74e3cd81fe8ea08a8dcd2d755c09ac2e8ad8fe409508904557b58383d3/rignore-0.7.6-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:bb24a5b947656dd94cb9e41c4bc8b23cec0c435b58be0d74a874f63c259549e8", size = 882835, upload-time = "2025-11-05T20:42:45.443Z" }, + { url = "https://files.pythonhosted.org/packages/77/61/a772a34b6b63154877433ac2d048364815b24c2dd308f76b212c408101a2/rignore-0.7.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5b1e33c9501cefe24b70a1eafd9821acfd0ebf0b35c3a379430a14df089993e3", size = 820301, upload-time = "2025-11-05T20:42:29.226Z" }, + { url = "https://files.pythonhosted.org/packages/71/30/054880b09c0b1b61d17eeb15279d8bf729c0ba52b36c3ada52fb827cbb3c/rignore-0.7.6-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bec3994665a44454df86deb762061e05cd4b61e3772f5b07d1882a8a0d2748d5", size = 897611, upload-time = "2025-11-05T20:40:56.475Z" }, + { url = "https://files.pythonhosted.org/packages/1e/40/b2d1c169f833d69931bf232600eaa3c7998ba4f9a402e43a822dad2ea9f2/rignore-0.7.6-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:26cba2edfe3cff1dfa72bddf65d316ddebf182f011f2f61538705d6dbaf54986", size = 873875, upload-time = "2025-11-05T20:41:11.561Z" }, + { url = "https://files.pythonhosted.org/packages/55/59/ca5ae93d83a1a60e44b21d87deb48b177a8db1b85e82fc8a9abb24a8986d/rignore-0.7.6-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ffa86694fec604c613696cb91e43892aa22e1fec5f9870e48f111c603e5ec4e9", size = 1167245, upload-time = "2025-11-05T20:41:28.29Z" }, + { url = "https://files.pythonhosted.org/packages/a5/52/cf3dce392ba2af806cba265aad6bcd9c48bb2a6cb5eee448d3319f6e505b/rignore-0.7.6-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48efe2ed95aa8104145004afb15cdfa02bea5cdde8b0344afeb0434f0d989aa2", size = 941750, upload-time = "2025-11-05T20:41:43.111Z" }, + { url = "https://files.pythonhosted.org/packages/ec/be/3f344c6218d779395e785091d05396dfd8b625f6aafbe502746fcd880af2/rignore-0.7.6-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dcae43eb44b7f2457fef7cc87f103f9a0013017a6f4e62182c565e924948f21", size = 958896, upload-time = "2025-11-05T20:42:13.784Z" }, + { url = "https://files.pythonhosted.org/packages/c9/34/d3fa71938aed7d00dcad87f0f9bcb02ad66c85d6ffc83ba31078ce53646a/rignore-0.7.6-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2cd649a7091c0dad2f11ef65630d30c698d505cbe8660dd395268e7c099cc99f", size = 983992, upload-time = "2025-11-05T20:41:58.022Z" }, + { url = "https://files.pythonhosted.org/packages/24/a4/52a697158e9920705bdbd0748d59fa63e0f3233fb92e9df9a71afbead6ca/rignore-0.7.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42de84b0289d478d30ceb7ae59023f7b0527786a9a5b490830e080f0e4ea5aeb", size = 1078181, upload-time = "2025-11-05T21:40:18.151Z" }, + { url = "https://files.pythonhosted.org/packages/ac/65/aa76dbcdabf3787a6f0fd61b5cc8ed1e88580590556d6c0207960d2384bb/rignore-0.7.6-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:875a617e57b53b4acbc5a91de418233849711c02e29cc1f4f9febb2f928af013", size = 1139232, upload-time = "2025-11-05T21:40:35.966Z" }, + { url = "https://files.pythonhosted.org/packages/08/44/31b31a49b3233c6842acc1c0731aa1e7fb322a7170612acf30327f700b44/rignore-0.7.6-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8703998902771e96e49968105207719f22926e4431b108450f3f430b4e268b7c", size = 1117349, upload-time = "2025-11-05T21:40:53.013Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ae/1b199a2302c19c658cf74e5ee1427605234e8c91787cfba0015f2ace145b/rignore-0.7.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:602ef33f3e1b04c1e9a10a3c03f8bc3cef2d2383dcc250d309be42b49923cabc", size = 1127702, upload-time = "2025-11-05T21:41:10.881Z" }, + { url = "https://files.pythonhosted.org/packages/fc/d3/18210222b37e87e36357f7b300b7d98c6dd62b133771e71ae27acba83a4f/rignore-0.7.6-cp314-cp314t-win32.whl", hash = "sha256:c1d8f117f7da0a4a96a8daef3da75bc090e3792d30b8b12cfadc240c631353f9", size = 647033, upload-time = "2025-11-05T21:42:00.095Z" }, + { url = "https://files.pythonhosted.org/packages/3e/87/033eebfbee3ec7d92b3bb1717d8f68c88e6fc7de54537040f3b3a405726f/rignore-0.7.6-cp314-cp314t-win_amd64.whl", hash = "sha256:ca36e59408bec81de75d307c568c2d0d410fb880b1769be43611472c61e85c96", size = 725647, upload-time = "2025-11-05T21:41:44.449Z" }, + { url = "https://files.pythonhosted.org/packages/79/62/b88e5879512c55b8ee979c666ee6902adc4ed05007226de266410ae27965/rignore-0.7.6-cp314-cp314t-win_arm64.whl", hash = "sha256:b83adabeb3e8cf662cabe1931b83e165b88c526fa6af6b3aa90429686e474896", size = 656035, upload-time = "2025-11-05T21:41:31.13Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, + { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, + { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, + { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, + { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, + { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, + { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, + { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, + { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, + { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, + { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, + { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, + { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, + { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, + { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, + { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, + { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, +] + +[[package]] +name = "safetensors" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/29/9c/6e74567782559a63bd040a236edca26fd71bc7ba88de2ef35d75df3bca5e/safetensors-0.7.0.tar.gz", hash = "sha256:07663963b67e8bd9f0b8ad15bb9163606cd27cc5a1b96235a50d8369803b96b0", size = 200878, upload-time = "2025-11-19T15:18:43.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/47/aef6c06649039accf914afef490268e1067ed82be62bcfa5b7e886ad15e8/safetensors-0.7.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c82f4d474cf725255d9e6acf17252991c3c8aac038d6ef363a4bf8be2f6db517", size = 467781, upload-time = "2025-11-19T15:18:35.84Z" }, + { url = "https://files.pythonhosted.org/packages/e8/00/374c0c068e30cd31f1e1b46b4b5738168ec79e7689ca82ee93ddfea05109/safetensors-0.7.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:94fd4858284736bb67a897a41608b5b0c2496c9bdb3bf2af1fa3409127f20d57", size = 447058, upload-time = "2025-11-19T15:18:34.416Z" }, + { url = "https://files.pythonhosted.org/packages/f1/06/578ffed52c2296f93d7fd2d844cabfa92be51a587c38c8afbb8ae449ca89/safetensors-0.7.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e07d91d0c92a31200f25351f4acb2bc6aff7f48094e13ebb1d0fb995b54b6542", size = 491748, upload-time = "2025-11-19T15:18:09.79Z" }, + { url = "https://files.pythonhosted.org/packages/ae/33/1debbbb70e4791dde185edb9413d1fe01619255abb64b300157d7f15dddd/safetensors-0.7.0-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8469155f4cb518bafb4acf4865e8bb9d6804110d2d9bdcaa78564b9fd841e104", size = 503881, upload-time = "2025-11-19T15:18:16.145Z" }, + { url = "https://files.pythonhosted.org/packages/8e/1c/40c2ca924d60792c3be509833df711b553c60effbd91da6f5284a83f7122/safetensors-0.7.0-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:54bef08bf00a2bff599982f6b08e8770e09cc012d7bba00783fc7ea38f1fb37d", size = 623463, upload-time = "2025-11-19T15:18:21.11Z" }, + { url = "https://files.pythonhosted.org/packages/9b/3a/13784a9364bd43b0d61eef4bea2845039bc2030458b16594a1bd787ae26e/safetensors-0.7.0-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42cb091236206bb2016d245c377ed383aa7f78691748f3bb6ee1bfa51ae2ce6a", size = 532855, upload-time = "2025-11-19T15:18:25.719Z" }, + { url = "https://files.pythonhosted.org/packages/a0/60/429e9b1cb3fc651937727befe258ea24122d9663e4d5709a48c9cbfceecb/safetensors-0.7.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac7252938f0696ddea46f5e855dd3138444e82236e3be475f54929f0c510d48", size = 507152, upload-time = "2025-11-19T15:18:33.023Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a8/4b45e4e059270d17af60359713ffd83f97900d45a6afa73aaa0d737d48b6/safetensors-0.7.0-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1d060c70284127fa805085d8f10fbd0962792aed71879d00864acda69dbab981", size = 541856, upload-time = "2025-11-19T15:18:31.075Z" }, + { url = "https://files.pythonhosted.org/packages/06/87/d26d8407c44175d8ae164a95b5a62707fcc445f3c0c56108e37d98070a3d/safetensors-0.7.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:cdab83a366799fa730f90a4ebb563e494f28e9e92c4819e556152ad55e43591b", size = 674060, upload-time = "2025-11-19T15:18:37.211Z" }, + { url = "https://files.pythonhosted.org/packages/11/f5/57644a2ff08dc6325816ba7217e5095f17269dada2554b658442c66aed51/safetensors-0.7.0-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:672132907fcad9f2aedcb705b2d7b3b93354a2aec1b2f706c4db852abe338f85", size = 771715, upload-time = "2025-11-19T15:18:38.689Z" }, + { url = "https://files.pythonhosted.org/packages/86/31/17883e13a814bd278ae6e266b13282a01049b0c81341da7fd0e3e71a80a3/safetensors-0.7.0-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:5d72abdb8a4d56d4020713724ba81dac065fedb7f3667151c4a637f1d3fb26c0", size = 714377, upload-time = "2025-11-19T15:18:40.162Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d8/0c8a7dc9b41dcac53c4cbf9df2b9c83e0e0097203de8b37a712b345c0be5/safetensors-0.7.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b0f6d66c1c538d5a94a73aa9ddca8ccc4227e6c9ff555322ea40bdd142391dd4", size = 677368, upload-time = "2025-11-19T15:18:41.627Z" }, + { url = "https://files.pythonhosted.org/packages/05/e5/cb4b713c8a93469e3c5be7c3f8d77d307e65fe89673e731f5c2bfd0a9237/safetensors-0.7.0-cp38-abi3-win32.whl", hash = "sha256:c74af94bf3ac15ac4d0f2a7c7b4663a15f8c2ab15ed0fc7531ca61d0835eccba", size = 326423, upload-time = "2025-11-19T15:18:45.74Z" }, + { url = "https://files.pythonhosted.org/packages/5d/e6/ec8471c8072382cb91233ba7267fd931219753bb43814cbc71757bfd4dab/safetensors-0.7.0-cp38-abi3-win_amd64.whl", hash = "sha256:d1239932053f56f3456f32eb9625590cc7582e905021f94636202a864d470755", size = 341380, upload-time = "2025-11-19T15:18:44.427Z" }, +] + +[[package]] +name = "scam-detection-project" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "ddgs" }, + { name = "fastapi", extra = ["standard"] }, + { name = "google-api-python-client" }, + { name = "google-cloud-vision" }, + { name = "httpx" }, + { name = "huggingface-hub" }, + { name = "langchain-chroma" }, + { name = "langchain-community" }, + { name = "langchain-core" }, + { name = "langchain-groq" }, + { name = "langchain-huggingface" }, + { name = "langchain-text-splitters" }, + { name = "networkx" }, + { name = "optimum", extra = ["onnxruntime"] }, + { name = "pandas" }, + { name = "pydantic-settings" }, + { name = "pymysql" }, + { name = "rapidfuzz" }, + { name = "scikit-learn" }, + { name = "sentence-transformers" }, + { name = "serpapi" }, + { name = "sqlalchemy" }, + { name = "tavily-python" }, + { name = "tldextract" }, + { name = "torch" }, + { name = "transformers" }, + { name = "uvicorn" }, + { name = "xgboost" }, +] + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, +] + +[package.metadata] +requires-dist = [ + { name = "ddgs" }, + { name = "fastapi", extras = ["standard"] }, + { name = "google-api-python-client" }, + { name = "google-cloud-vision" }, + { name = "httpx", specifier = ">=0.28.1" }, + { name = "huggingface-hub", specifier = ">=0.36.2" }, + { name = "langchain-chroma", specifier = ">=0.2.6" }, + { name = "langchain-community", specifier = ">=0.4.1" }, + { name = "langchain-core", specifier = ">=0.3.79" }, + { name = "langchain-groq", specifier = ">=1.1.2" }, + { name = "langchain-huggingface", specifier = ">=1.2.2" }, + { name = "langchain-text-splitters", specifier = ">=0.3.11" }, + { name = "networkx", specifier = ">=3.0" }, + { name = "optimum", extras = ["onnxruntime"], specifier = ">=1.16.0" }, + { name = "pandas", specifier = ">=3.0.3" }, + { name = "pydantic-settings", specifier = ">=2.13.1" }, + { name = "pymysql" }, + { name = "rapidfuzz", specifier = ">=3.14.5" }, + { name = "scikit-learn", specifier = ">=1.6.1" }, + { name = "sentence-transformers", specifier = ">=5.5.0" }, + { name = "serpapi", specifier = ">=1.0.2" }, + { name = "sqlalchemy" }, + { name = "tavily-python", specifier = ">=0.7.26" }, + { name = "tldextract" }, + { name = "torch" }, + { name = "transformers", specifier = ">=4.40.0" }, + { name = "uvicorn" }, + { name = "xgboost" }, +] + +[package.metadata.requires-dev] +dev = [{ name = "pytest" }] + +[[package]] +name = "scikit-learn" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "joblib" }, + { name = "numpy" }, + { name = "scipy" }, + { name = "threadpoolctl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/d4/40988bf3b8e34feec1d0e6a051446b1f66225f8529b9309becaeef62b6c4/scikit_learn-1.8.0.tar.gz", hash = "sha256:9bccbb3b40e3de10351f8f5068e105d0f4083b1a65fa07b6634fbc401a6287fd", size = 7335585, upload-time = "2025-12-10T07:08:53.618Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/05/1af2c186174cc92dcab2233f327336058c077d38f6fe2aceb08e6ab4d509/scikit_learn-1.8.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c22a2da7a198c28dd1a6e1136f19c830beab7fdca5b3e5c8bba8394f8a5c45b3", size = 8528667, upload-time = "2025-12-10T07:08:27.541Z" }, + { url = "https://files.pythonhosted.org/packages/a8/25/01c0af38fe969473fb292bba9dc2b8f9b451f3112ff242c647fee3d0dfe7/scikit_learn-1.8.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:6b595b07a03069a2b1740dc08c2299993850ea81cce4fe19b2421e0c970de6b7", size = 8066524, upload-time = "2025-12-10T07:08:29.822Z" }, + { url = "https://files.pythonhosted.org/packages/be/ce/a0623350aa0b68647333940ee46fe45086c6060ec604874e38e9ab7d8e6c/scikit_learn-1.8.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:29ffc74089f3d5e87dfca4c2c8450f88bdc61b0fc6ed5d267f3988f19a1309f6", size = 8657133, upload-time = "2025-12-10T07:08:31.865Z" }, + { url = "https://files.pythonhosted.org/packages/b8/cb/861b41341d6f1245e6ca80b1c1a8c4dfce43255b03df034429089ca2a2c5/scikit_learn-1.8.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb65db5d7531bccf3a4f6bec3462223bea71384e2cda41da0f10b7c292b9e7c4", size = 8923223, upload-time = "2025-12-10T07:08:34.166Z" }, + { url = "https://files.pythonhosted.org/packages/76/18/a8def8f91b18cd1ba6e05dbe02540168cb24d47e8dcf69e8d00b7da42a08/scikit_learn-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:56079a99c20d230e873ea40753102102734c5953366972a71d5cb39a32bc40c6", size = 8096518, upload-time = "2025-12-10T07:08:36.339Z" }, + { url = "https://files.pythonhosted.org/packages/d1/77/482076a678458307f0deb44e29891d6022617b2a64c840c725495bee343f/scikit_learn-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:3bad7565bc9cf37ce19a7c0d107742b320c1285df7aab1a6e2d28780df167242", size = 7754546, upload-time = "2025-12-10T07:08:38.128Z" }, + { url = "https://files.pythonhosted.org/packages/2d/d1/ef294ca754826daa043b2a104e59960abfab4cf653891037d19dd5b6f3cf/scikit_learn-1.8.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:4511be56637e46c25721e83d1a9cea9614e7badc7040c4d573d75fbe257d6fd7", size = 8848305, upload-time = "2025-12-10T07:08:41.013Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e2/b1f8b05138ee813b8e1a4149f2f0d289547e60851fd1bb268886915adbda/scikit_learn-1.8.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:a69525355a641bf8ef136a7fa447672fb54fe8d60cab5538d9eb7c6438543fb9", size = 8432257, upload-time = "2025-12-10T07:08:42.873Z" }, + { url = "https://files.pythonhosted.org/packages/26/11/c32b2138a85dcb0c99f6afd13a70a951bfdff8a6ab42d8160522542fb647/scikit_learn-1.8.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c2656924ec73e5939c76ac4c8b026fc203b83d8900362eb2599d8aee80e4880f", size = 8678673, upload-time = "2025-12-10T07:08:45.362Z" }, + { url = "https://files.pythonhosted.org/packages/c7/57/51f2384575bdec454f4fe4e7a919d696c9ebce914590abf3e52d47607ab8/scikit_learn-1.8.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15fc3b5d19cc2be65404786857f2e13c70c83dd4782676dd6814e3b89dc8f5b9", size = 8922467, upload-time = "2025-12-10T07:08:47.408Z" }, + { url = "https://files.pythonhosted.org/packages/35/4d/748c9e2872637a57981a04adc038dacaa16ba8ca887b23e34953f0b3f742/scikit_learn-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:00d6f1d66fbcf4eba6e356e1420d33cc06c70a45bb1363cd6f6a8e4ebbbdece2", size = 8774395, upload-time = "2025-12-10T07:08:49.337Z" }, + { url = "https://files.pythonhosted.org/packages/60/22/d7b2ebe4704a5e50790ba089d5c2ae308ab6bb852719e6c3bd4f04c3a363/scikit_learn-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f28dd15c6bb0b66ba09728cf09fd8736c304be29409bd8445a080c1280619e8c", size = 8002647, upload-time = "2025-12-10T07:08:51.601Z" }, +] + +[[package]] +name = "scipy" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/83/333afb452af6f0fd70414dc04f898647ee1423979ce02efa75c3b0f2c28e/scipy-1.17.1-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:a48a72c77a310327f6a3a920092fa2b8fd03d7deaa60f093038f22d98e096717", size = 31584510, upload-time = "2026-02-23T00:21:01.015Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a6/d05a85fd51daeb2e4ea71d102f15b34fedca8e931af02594193ae4fd25f7/scipy-1.17.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:45abad819184f07240d8a696117a7aacd39787af9e0b719d00285549ed19a1e9", size = 28170131, upload-time = "2026-02-23T00:21:05.888Z" }, + { url = "https://files.pythonhosted.org/packages/db/7b/8624a203326675d7746a254083a187398090a179335b2e4a20e2ddc46e83/scipy-1.17.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3fd1fcdab3ea951b610dc4cef356d416d5802991e7e32b5254828d342f7b7e0b", size = 20342032, upload-time = "2026-02-23T00:21:09.904Z" }, + { url = "https://files.pythonhosted.org/packages/c9/35/2c342897c00775d688d8ff3987aced3426858fd89d5a0e26e020b660b301/scipy-1.17.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7bdf2da170b67fdf10bca777614b1c7d96ae3ca5794fd9587dce41eb2966e866", size = 22678766, upload-time = "2026-02-23T00:21:14.313Z" }, + { url = "https://files.pythonhosted.org/packages/ef/f2/7cdb8eb308a1a6ae1e19f945913c82c23c0c442a462a46480ce487fdc0ac/scipy-1.17.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:adb2642e060a6549c343603a3851ba76ef0b74cc8c079a9a58121c7ec9fe2350", size = 32957007, upload-time = "2026-02-23T00:21:19.663Z" }, + { url = "https://files.pythonhosted.org/packages/0b/2e/7eea398450457ecb54e18e9d10110993fa65561c4f3add5e8eccd2b9cd41/scipy-1.17.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eee2cfda04c00a857206a4330f0c5e3e56535494e30ca445eb19ec624ae75118", size = 35221333, upload-time = "2026-02-23T00:21:25.278Z" }, + { url = "https://files.pythonhosted.org/packages/d9/77/5b8509d03b77f093a0d52e606d3c4f79e8b06d1d38c441dacb1e26cacf46/scipy-1.17.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d2650c1fb97e184d12d8ba010493ee7b322864f7d3d00d3f9bb97d9c21de4068", size = 35042066, upload-time = "2026-02-23T00:21:31.358Z" }, + { url = "https://files.pythonhosted.org/packages/f9/df/18f80fb99df40b4070328d5ae5c596f2f00fffb50167e31439e932f29e7d/scipy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:08b900519463543aa604a06bec02461558a6e1cef8fdbb8098f77a48a83c8118", size = 37612763, upload-time = "2026-02-23T00:21:37.247Z" }, + { url = "https://files.pythonhosted.org/packages/4b/39/f0e8ea762a764a9dc52aa7dabcfad51a354819de1f0d4652b6a1122424d6/scipy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:3877ac408e14da24a6196de0ddcace62092bfc12a83823e92e49e40747e52c19", size = 37290984, upload-time = "2026-02-23T00:22:35.023Z" }, + { url = "https://files.pythonhosted.org/packages/7c/56/fe201e3b0f93d1a8bcf75d3379affd228a63d7e2d80ab45467a74b494947/scipy-1.17.1-cp314-cp314-win_arm64.whl", hash = "sha256:f8885db0bc2bffa59d5c1b72fad7a6a92d3e80e7257f967dd81abb553a90d293", size = 25192877, upload-time = "2026-02-23T00:22:39.798Z" }, + { url = "https://files.pythonhosted.org/packages/96/ad/f8c414e121f82e02d76f310f16db9899c4fcde36710329502a6b2a3c0392/scipy-1.17.1-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:1cc682cea2ae55524432f3cdff9e9a3be743d52a7443d0cba9017c23c87ae2f6", size = 31949750, upload-time = "2026-02-23T00:21:42.289Z" }, + { url = "https://files.pythonhosted.org/packages/7c/b0/c741e8865d61b67c81e255f4f0a832846c064e426636cd7de84e74d209be/scipy-1.17.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:2040ad4d1795a0ae89bfc7e8429677f365d45aa9fd5e4587cf1ea737f927b4a1", size = 28585858, upload-time = "2026-02-23T00:21:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1b/3985219c6177866628fa7c2595bfd23f193ceebbe472c98a08824b9466ff/scipy-1.17.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:131f5aaea57602008f9822e2115029b55d4b5f7c070287699fe45c661d051e39", size = 20757723, upload-time = "2026-02-23T00:21:52.039Z" }, + { url = "https://files.pythonhosted.org/packages/c0/19/2a04aa25050d656d6f7b9e7b685cc83d6957fb101665bfd9369ca6534563/scipy-1.17.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9cdc1a2fcfd5c52cfb3045feb399f7b3ce822abdde3a193a6b9a60b3cb5854ca", size = 23043098, upload-time = "2026-02-23T00:21:56.185Z" }, + { url = "https://files.pythonhosted.org/packages/86/f1/3383beb9b5d0dbddd030335bf8a8b32d4317185efe495374f134d8be6cce/scipy-1.17.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e3dcd57ab780c741fde8dc68619de988b966db759a3c3152e8e9142c26295ad", size = 33030397, upload-time = "2026-02-23T00:22:01.404Z" }, + { url = "https://files.pythonhosted.org/packages/41/68/8f21e8a65a5a03f25a79165ec9d2b28c00e66dc80546cf5eb803aeeff35b/scipy-1.17.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9956e4d4f4a301ebf6cde39850333a6b6110799d470dbbb1e25326ac447f52a", size = 35281163, upload-time = "2026-02-23T00:22:07.024Z" }, + { url = "https://files.pythonhosted.org/packages/84/8d/c8a5e19479554007a5632ed7529e665c315ae7492b4f946b0deb39870e39/scipy-1.17.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a4328d245944d09fd639771de275701ccadf5f781ba0ff092ad141e017eccda4", size = 35116291, upload-time = "2026-02-23T00:22:12.585Z" }, + { url = "https://files.pythonhosted.org/packages/52/52/e57eceff0e342a1f50e274264ed47497b59e6a4e3118808ee58ddda7b74a/scipy-1.17.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a77cbd07b940d326d39a1d1b37817e2ee4d79cb30e7338f3d0cddffae70fcaa2", size = 37682317, upload-time = "2026-02-23T00:22:18.513Z" }, + { url = "https://files.pythonhosted.org/packages/11/2f/b29eafe4a3fbc3d6de9662b36e028d5f039e72d345e05c250e121a230dd4/scipy-1.17.1-cp314-cp314t-win_amd64.whl", hash = "sha256:eb092099205ef62cd1782b006658db09e2fed75bffcae7cc0d44052d8aa0f484", size = 37345327, upload-time = "2026-02-23T00:22:24.442Z" }, + { url = "https://files.pythonhosted.org/packages/07/39/338d9219c4e87f3e708f18857ecd24d22a0c3094752393319553096b98af/scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21", size = 25489165, upload-time = "2026-02-23T00:22:29.563Z" }, +] + +[[package]] +name = "sentence-transformers" +version = "5.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, + { name = "numpy" }, + { name = "scikit-learn" }, + { name = "scipy" }, + { name = "torch" }, + { name = "tqdm" }, + { name = "transformers" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2c/27/16d127a61303e05847d878b23687f3371868c76e738557fa80b4373a8c2b/sentence_transformers-5.5.0.tar.gz", hash = "sha256:9cec675e68bfe09d07466d1f13ab06d1d79d60a0f45b154baf433bde6ae159cb", size = 444908, upload-time = "2026-05-12T14:05:42.383Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/20/18416624bcbae866ec0b111979766cebabe8e5ff7563ab953ecbaf3ff9e7/sentence_transformers-5.5.0-py3-none-any.whl", hash = "sha256:75313fdcc2397ec4b58297c25d6187fcca5a6b2aeb09570a72eff5a3223d8d58", size = 588665, upload-time = "2026-05-12T14:05:40.899Z" }, +] + +[[package]] +name = "sentry-sdk" +version = "2.57.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4f/87/46c0406d8b5ddd026f73adaf5ab75ce144219c41a4830b52df4b9ab55f7f/sentry_sdk-2.57.0.tar.gz", hash = "sha256:4be8d1e71c32fb27f79c577a337ac8912137bba4bcbc64a4ec1da4d6d8dc5199", size = 435288, upload-time = "2026-03-31T09:39:29.264Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/64/982e07b93219cb52e1cca5d272cb579e2f3eb001956c9e7a9a6d106c9473/sentry_sdk-2.57.0-py2.py3-none-any.whl", hash = "sha256:812c8bf5ff3d2f0e89c82f5ce80ab3a6423e102729c4706af7413fd1eb480585", size = 456489, upload-time = "2026-03-31T09:39:27.524Z" }, +] + +[[package]] +name = "serpapi" +version = "1.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8d/19/6af9f42d372d2d0012493155f5decf0a889f434e824a6b281ab2c8f88822/serpapi-1.0.2.tar.gz", hash = "sha256:06ff981129a1cb7c3706469a67f8d43e77ab295bcbdbfcb7c118d39e8efb0783", size = 16893, upload-time = "2026-03-18T14:29:16.448Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/21/6b33cea480c69992813fbd36bfdb622ead6e91c6ff259ee4b1143803769d/serpapi-1.0.2-py3-none-any.whl", hash = "sha256:4edb67318918c0ff460aae118d66f76ad83ab75fbf901a77a9722b0cfe6c70aa", size = 11768, upload-time = "2026-03-18T14:29:15.515Z" }, +] + +[[package]] +name = "setuptools" +version = "81.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/1c/73e719955c59b8e424d015ab450f51c0af856ae46ea2da83eba51cc88de1/setuptools-81.0.0.tar.gz", hash = "sha256:487b53915f52501f0a79ccfd0c02c165ffe06631443a886740b91af4b7a5845a", size = 1198299, upload-time = "2026-02-06T21:10:39.601Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/e3/c164c88b2e5ce7b24d667b9bd83589cf4f3520d97cad01534cd3c4f55fdb/setuptools-81.0.0-py3-none-any.whl", hash = "sha256:fdd925d5c5d9f62e4b74b30d6dd7828ce236fd6ed998a08d81de62ce5a6310d6", size = 1062021, upload-time = "2026-02-06T21:10:37.175Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "socksio" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/5c/48a7d9495be3d1c651198fd99dbb6ce190e2274d0f28b9051307bdec6b85/socksio-1.0.0.tar.gz", hash = "sha256:f88beb3da5b5c38b9890469de67d0cb0f9d494b78b106ca1845f96c10b91c4ac", size = 19055, upload-time = "2020-04-17T15:50:34.664Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/c3/6eeb6034408dac0fa653d126c9204ade96b819c936e136c5e8a6897eee9c/socksio-1.0.0-py3-none-any.whl", hash = "sha256:95dc1f15f9b34e8d7b16f06d74b8ccf48f609af32ab33c608d08761c5dcbb1f3", size = 12763, upload-time = "2020-04-17T15:50:31.878Z" }, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.49" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/45/461788f35e0364a8da7bda51a1fe1b09762d0c32f12f63727998d85a873b/sqlalchemy-2.0.49.tar.gz", hash = "sha256:d15950a57a210e36dd4cec1aac22787e2a4d57ba9318233e2ef8b2daf9ff2d5f", size = 9898221, upload-time = "2026-04-03T16:38:11.704Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/33/bf28f618c0a9597d14e0b9ee7d1e0622faff738d44fe986ee287cdf1b8d0/sqlalchemy-2.0.49-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:233088b4b99ebcbc5258c755a097aa52fbf90727a03a5a80781c4b9c54347a2e", size = 2156356, upload-time = "2026-04-03T16:53:09.914Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a7/5f476227576cb8644650eff68cc35fa837d3802b997465c96b8340ced1e2/sqlalchemy-2.0.49-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57ca426a48eb2c682dae8204cd89ea8ab7031e2675120a47924fabc7caacbc2a", size = 3276486, upload-time = "2026-04-03T17:07:46.9Z" }, + { url = "https://files.pythonhosted.org/packages/2e/84/efc7c0bf3a1c5eef81d397f6fddac855becdbb11cb38ff957888603014a7/sqlalchemy-2.0.49-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:685e93e9c8f399b0c96a624799820176312f5ceef958c0f88215af4013d29066", size = 3281479, upload-time = "2026-04-03T17:12:32.226Z" }, + { url = "https://files.pythonhosted.org/packages/91/68/bb406fa4257099c67bd75f3f2261b129c63204b9155de0d450b37f004698/sqlalchemy-2.0.49-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9e0400fa22f79acc334d9a6b185dc00a44a8e6578aa7e12d0ddcd8434152b187", size = 3226269, upload-time = "2026-04-03T17:07:48.678Z" }, + { url = "https://files.pythonhosted.org/packages/67/84/acb56c00cca9f251f437cb49e718e14f7687505749ea9255d7bd8158a6df/sqlalchemy-2.0.49-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a05977bffe9bffd2229f477fa75eabe3192b1b05f408961d1bebff8d1cd4d401", size = 3248260, upload-time = "2026-04-03T17:12:34.381Z" }, + { url = "https://files.pythonhosted.org/packages/56/19/6a20ea25606d1efd7bd1862149bb2a22d1451c3f851d23d887969201633f/sqlalchemy-2.0.49-cp314-cp314-win32.whl", hash = "sha256:0f2fa354ba106eafff2c14b0cc51f22801d1e8b2e4149342023bd6f0955de5f5", size = 2118463, upload-time = "2026-04-03T17:05:47.093Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4f/8297e4ed88e80baa1f5aa3c484a0ee29ef3c69c7582f206c916973b75057/sqlalchemy-2.0.49-cp314-cp314-win_amd64.whl", hash = "sha256:77641d299179c37b89cf2343ca9972c88bb6eef0d5fc504a2f86afd15cd5adf5", size = 2144204, upload-time = "2026-04-03T17:05:48.694Z" }, + { url = "https://files.pythonhosted.org/packages/1f/33/95e7216df810c706e0cd3655a778604bbd319ed4f43333127d465a46862d/sqlalchemy-2.0.49-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c1dc3368794d522f43914e03312202523cc89692f5389c32bea0233924f8d977", size = 3565474, upload-time = "2026-04-03T16:58:35.128Z" }, + { url = "https://files.pythonhosted.org/packages/0c/a4/ed7b18d8ccf7f954a83af6bb73866f5bc6f5636f44c7731fbb741f72cc4f/sqlalchemy-2.0.49-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c821c47ecfe05cc32140dcf8dc6fd5d21971c86dbd56eabfe5ba07a64910c01", size = 3530567, upload-time = "2026-04-03T17:06:04.587Z" }, + { url = "https://files.pythonhosted.org/packages/73/a3/20faa869c7e21a827c4a2a42b41353a54b0f9f5e96df5087629c306df71e/sqlalchemy-2.0.49-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9c04bff9a5335eb95c6ecf1c117576a0aa560def274876fd156cfe5510fccc61", size = 3474282, upload-time = "2026-04-03T16:58:37.131Z" }, + { url = "https://files.pythonhosted.org/packages/b7/50/276b9a007aa0764304ad467eceb70b04822dc32092492ee5f322d559a4dc/sqlalchemy-2.0.49-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7f605a456948c35260e7b2a39f8952a26f077fd25653c37740ed186b90aaa68a", size = 3480406, upload-time = "2026-04-03T17:06:07.176Z" }, + { url = "https://files.pythonhosted.org/packages/e5/c3/c80fcdb41905a2df650c2a3e0337198b6848876e63d66fe9188ef9003d24/sqlalchemy-2.0.49-cp314-cp314t-win32.whl", hash = "sha256:6270d717b11c5476b0cbb21eedc8d4dbb7d1a956fd6c15a23e96f197a6193158", size = 2149151, upload-time = "2026-04-03T17:02:07.281Z" }, + { url = "https://files.pythonhosted.org/packages/05/52/9f1a62feab6ed368aff068524ff414f26a6daebc7361861035ae00b05530/sqlalchemy-2.0.49-cp314-cp314t-win_amd64.whl", hash = "sha256:275424295f4256fd301744b8f335cff367825d270f155d522b30c7bf49903ee7", size = 2184178, upload-time = "2026-04-03T17:02:08.623Z" }, + { url = "https://files.pythonhosted.org/packages/e5/30/8519fdde58a7bdf155b714359791ad1dc018b47d60269d5d160d311fdc36/sqlalchemy-2.0.49-py3-none-any.whl", hash = "sha256:ec44cfa7ef1a728e88ad41674de50f6db8cfdb3e2af84af86e0041aaf02d43d0", size = 1942158, upload-time = "2026-04-03T16:53:44.135Z" }, +] + +[[package]] +name = "starlette" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/81/69/17425771797c36cded50b7fe44e850315d039f28b15901ab44839e70b593/starlette-1.0.0.tar.gz", hash = "sha256:6a4beaf1f81bb472fd19ea9b918b50dc3a77a6f2e190a12954b25e6ed5eea149", size = 2655289, upload-time = "2026-03-22T18:29:46.779Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/c9/584bc9651441b4ba60cc4d557d8a547b5aff901af35bda3a4ee30c819b82/starlette-1.0.0-py3-none-any.whl", hash = "sha256:d3ec55e0bb321692d275455ddfd3df75fff145d009685eb40dc91fc66b03d38b", size = 72651, upload-time = "2026-03-22T18:29:45.111Z" }, +] + +[[package]] +name = "sympy" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mpmath" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, +] + +[[package]] +name = "tavily-python" +version = "0.7.26" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "requests" }, + { name = "tiktoken" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bd/29/c2d5056dbf45a80484df5a1bf0d0ae59c611aa9680372c400a278fbf0c50/tavily_python-0.7.26.tar.gz", hash = "sha256:36202728144fce1ef0cece33800f98e3cdeb5f600e6678c50b1232b866a2f509", size = 29647, upload-time = "2026-06-10T18:38:45.231Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/c2/616ebcd49561d74c93099efa45fd5a4af2e528415f01351575980be0ba9e/tavily_python-0.7.26-py3-none-any.whl", hash = "sha256:9b9e7f2d10b3572444e13e9a3bf4f5341975d591cbc1306be04ef89a363e42a7", size = 21940, upload-time = "2026-06-10T18:38:44.127Z" }, +] + +[[package]] +name = "tenacity" +version = "9.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/c6/ee486fd809e357697ee8a44d3d69222b344920433d3b6666ccd9b374630c/tenacity-9.1.4.tar.gz", hash = "sha256:adb31d4c263f2bd041081ab33b498309a57c77f9acf2db65aadf0898179cf93a", size = 49413, upload-time = "2026-02-07T10:45:33.841Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/c1/eb8f9debc45d3b7918a32ab756658a0904732f75e555402972246b0b8e71/tenacity-9.1.4-py3-none-any.whl", hash = "sha256:6095a360c919085f28c6527de529e76a06ad89b23659fa881ae0649b867a9d55", size = 28926, upload-time = "2026-02-07T10:45:32.24Z" }, +] + +[[package]] +name = "threadpoolctl" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274, upload-time = "2025-03-13T13:49:23.031Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" }, +] + +[[package]] +name = "tiktoken" +version = "0.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "regex" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/e5/5f3cb2159769d0f4324c0e9e87f9de3c4b1cd45848a96b2eb3566ad5ca77/tiktoken-0.13.0.tar.gz", hash = "sha256:c9435714c3a84c2319499de9a300c0e604449dd0799ff246458b3bb6a7f433c1", size = 38986, upload-time = "2026-05-15T04:51:27.153Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/93/0dd6adca026a616c3a92974566b43381eea4b475ce1f36c062b8271a9ac5/tiktoken-0.13.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaaaef47c2406277181d2086484c317bf7fc433e2d5d03ff94f56b0dcec87471", size = 1034977, upload-time = "2026-05-15T04:51:00.957Z" }, + { url = "https://files.pythonhosted.org/packages/d9/77/5ec6e6bc5b30bed6d93f7f2162d8f6b32437b3ba27cb527cfe004f6109c9/tiktoken-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ca8b310bd93b3772cb1b7922d915446864860f562bdfe4825c63a0aed3fb28cd", size = 983635, upload-time = "2026-05-15T04:51:02.629Z" }, + { url = "https://files.pythonhosted.org/packages/94/b0/c8ae9aff00d625c50659b4513e707a0462c4bf5d4d6cc1b802103225c02e/tiktoken-0.13.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:32e0c12305105002c047b3bb1070b0dd9a73b0cb3b2856a8972b810e7a4f5881", size = 1116036, upload-time = "2026-05-15T04:51:04.082Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ac/6a5dddd1d0a6018ecb389bd0353e6b4a515eb4d2286611bd0ace1937b9e1/tiktoken-0.13.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:5ba5fd62507a932d1241346179e3b39bc7bf7408f03c272652d93b3bedf5db24", size = 1135544, upload-time = "2026-05-15T04:51:05.229Z" }, + { url = "https://files.pythonhosted.org/packages/f4/b8/585032b4384b2f7dcdaddcb52865c83a701a420d09e3c2b4a2be1c450c57/tiktoken-0.13.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d108bc2d470fc53c8ecd24f2c0fd2b5f98c33e87cdb6aa2e9b8c5dced703d273", size = 1182217, upload-time = "2026-05-15T04:51:06.517Z" }, + { url = "https://files.pythonhosted.org/packages/cd/b6/993ff1ded3958215fd341a847b8e5ffeb5de473f435296870d314fc91ac4/tiktoken-0.13.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cb99cb5127449f58d0a2d5f5ccfb390d8dbdfd919c221246caaee29d8725ed51", size = 1239404, upload-time = "2026-05-15T04:51:07.843Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3d/fef7e06e3b33e7538db0ced734cf9fe23b6832d2ac4990c119c377aec55e/tiktoken-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:115c4f26ffa11caac8b54eea35c2ad38c612c20a48d35dd15d70a02ac6f51f58", size = 918686, upload-time = "2026-05-15T04:51:08.925Z" }, + { url = "https://files.pythonhosted.org/packages/c1/82/a7fc44582bc32ab00de988a2299bf77c077f59068b233109e34b7d6ca7e6/tiktoken-0.13.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:472527e9132952f2fbf77cd290658bacf003d4d5a3fabc18e5fbd407cbae4d9b", size = 1034454, upload-time = "2026-05-15T04:51:10.035Z" }, + { url = "https://files.pythonhosted.org/packages/37/d0/24d8a890c14f432a05cea669c17bebeaa99f96a7c79523b590f564246411/tiktoken-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4e2f67d27c9626cdd25fe33d9313c5cdb3d8d82da646b68d6eb8e7e9c20e6448", size = 982976, upload-time = "2026-05-15T04:51:11.23Z" }, + { url = "https://files.pythonhosted.org/packages/49/b7/2ab43f62788a9266187a9bfc1d3af99ad83e5eaa25fbef168a69cd5ad14f/tiktoken-0.13.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:2b920b35805cd64585a37c3dc7ce65fba4d2d36016be01e1d7942482ca29093a", size = 1115526, upload-time = "2026-05-15T04:51:12.608Z" }, + { url = "https://files.pythonhosted.org/packages/64/39/1494321ed323ce7a14d88e3cd6cb9058625977df1c6961ddc492bd10a9f3/tiktoken-0.13.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:493af3aa28a4aaf2e3d2600a2ee717252c9bf5ab38fff94eb5a02db5ab77e5ad", size = 1136466, upload-time = "2026-05-15T04:51:13.926Z" }, + { url = "https://files.pythonhosted.org/packages/96/d9/dfd086aa2d918c563a140720e0ce296cada1634efd2783d5cf51e05f984e/tiktoken-0.13.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6644c9c2b5cf3916f5a3641d7d12fdb3f006a7b3d9ff6acdaec44e29ab1ff91e", size = 1181863, upload-time = "2026-05-15T04:51:15.025Z" }, + { url = "https://files.pythonhosted.org/packages/2f/68/a18b4f307086954fdae32714cb4f85562e34f9d34ab206e61f1816aa6018/tiktoken-0.13.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5cb65b60b9408563676d874a3a4ee573370066f0dc4e29d84e82e989c6517424", size = 1239218, upload-time = "2026-05-15T04:51:16.103Z" }, + { url = "https://files.pythonhosted.org/packages/16/5b/f2aa703a4fc5d2dff73460a7d46cc2f3f44aa0f3dd8eeb20d2a0ecf68862/tiktoken-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:85b78cc3a2c3d48723ca751fa981f1fedccd54194ca0471b957364353a898b07", size = 918110, upload-time = "2026-05-15T04:51:17.237Z" }, +] + +[[package]] +name = "tldextract" +version = "5.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "idna" }, + { name = "requests" }, + { name = "requests-file" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/65/7b/644fbbb49564a6cb124a8582013315a41148dba2f72209bba14a84242bf0/tldextract-5.3.1.tar.gz", hash = "sha256:a72756ca170b2510315076383ea2993478f7da6f897eef1f4a5400735d5057fb", size = 126105, upload-time = "2025-12-28T23:58:05.532Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/42/0e49d6d0aac449ca71952ec5bae764af009754fcb2e76a5cc097543747b3/tldextract-5.3.1-py3-none-any.whl", hash = "sha256:6bfe36d518de569c572062b788e16a659ccaceffc486d243af0484e8ecf432d9", size = 105886, upload-time = "2025-12-28T23:58:04.071Z" }, +] + +[[package]] +name = "tokenizers" +version = "0.22.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/73/6f/f80cfef4a312e1fb34baf7d85c72d4411afde10978d4657f8cdd811d3ccc/tokenizers-0.22.2.tar.gz", hash = "sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917", size = 372115, upload-time = "2026-01-05T10:45:15.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/97/5dbfabf04c7e348e655e907ed27913e03db0923abb5dfdd120d7b25630e1/tokenizers-0.22.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:544dd704ae7238755d790de45ba8da072e9af3eea688f698b137915ae959281c", size = 3100275, upload-time = "2026-01-05T10:41:02.158Z" }, + { url = "https://files.pythonhosted.org/packages/2e/47/174dca0502ef88b28f1c9e06b73ce33500eedfac7a7692108aec220464e7/tokenizers-0.22.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1e418a55456beedca4621dbab65a318981467a2b188e982a23e117f115ce5001", size = 2981472, upload-time = "2026-01-05T10:41:00.276Z" }, + { url = "https://files.pythonhosted.org/packages/d6/84/7990e799f1309a8b87af6b948f31edaa12a3ed22d11b352eaf4f4b2e5753/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2249487018adec45d6e3554c71d46eb39fa8ea67156c640f7513eb26f318cec7", size = 3290736, upload-time = "2026-01-05T10:40:32.165Z" }, + { url = "https://files.pythonhosted.org/packages/78/59/09d0d9ba94dcd5f4f1368d4858d24546b4bdc0231c2354aa31d6199f0399/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25b85325d0815e86e0bac263506dd114578953b7b53d7de09a6485e4a160a7dd", size = 3168835, upload-time = "2026-01-05T10:40:38.847Z" }, + { url = "https://files.pythonhosted.org/packages/47/50/b3ebb4243e7160bda8d34b731e54dd8ab8b133e50775872e7a434e524c28/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfb88f22a209ff7b40a576d5324bf8286b519d7358663db21d6246fb17eea2d5", size = 3521673, upload-time = "2026-01-05T10:40:56.614Z" }, + { url = "https://files.pythonhosted.org/packages/e0/fa/89f4cb9e08df770b57adb96f8cbb7e22695a4cb6c2bd5f0c4f0ebcf33b66/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c774b1276f71e1ef716e5486f21e76333464f47bece56bbd554485982a9e03e", size = 3724818, upload-time = "2026-01-05T10:40:44.507Z" }, + { url = "https://files.pythonhosted.org/packages/64/04/ca2363f0bfbe3b3d36e95bf67e56a4c88c8e3362b658e616d1ac185d47f2/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df6c4265b289083bf710dff49bc51ef252f9d5be33a45ee2bed151114a56207b", size = 3379195, upload-time = "2026-01-05T10:40:51.139Z" }, + { url = "https://files.pythonhosted.org/packages/2e/76/932be4b50ef6ccedf9d3c6639b056a967a86258c6d9200643f01269211ca/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:369cc9fc8cc10cb24143873a0d95438bb8ee257bb80c71989e3ee290e8d72c67", size = 3274982, upload-time = "2026-01-05T10:40:58.331Z" }, + { url = "https://files.pythonhosted.org/packages/1d/28/5f9f5a4cc211b69e89420980e483831bcc29dade307955cc9dc858a40f01/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:29c30b83d8dcd061078b05ae0cb94d3c710555fbb44861139f9f83dcca3dc3e4", size = 9478245, upload-time = "2026-01-05T10:41:04.053Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fb/66e2da4704d6aadebf8cb39f1d6d1957df667ab24cff2326b77cda0dcb85/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:37ae80a28c1d3265bb1f22464c856bd23c02a05bb211e56d0c5301a435be6c1a", size = 9560069, upload-time = "2026-01-05T10:45:10.673Z" }, + { url = "https://files.pythonhosted.org/packages/16/04/fed398b05caa87ce9b1a1bb5166645e38196081b225059a6edaff6440fac/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:791135ee325f2336f498590eb2f11dc5c295232f288e75c99a36c5dbce63088a", size = 9899263, upload-time = "2026-01-05T10:45:12.559Z" }, + { url = "https://files.pythonhosted.org/packages/05/a1/d62dfe7376beaaf1394917e0f8e93ee5f67fea8fcf4107501db35996586b/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38337540fbbddff8e999d59970f3c6f35a82de10053206a7562f1ea02d046fa5", size = 10033429, upload-time = "2026-01-05T10:45:14.333Z" }, + { url = "https://files.pythonhosted.org/packages/fd/18/a545c4ea42af3df6effd7d13d250ba77a0a86fb20393143bbb9a92e434d4/tokenizers-0.22.2-cp39-abi3-win32.whl", hash = "sha256:a6bf3f88c554a2b653af81f3204491c818ae2ac6fbc09e76ef4773351292bc92", size = 2502363, upload-time = "2026-01-05T10:45:20.593Z" }, + { url = "https://files.pythonhosted.org/packages/65/71/0670843133a43d43070abeb1949abfdef12a86d490bea9cd9e18e37c5ff7/tokenizers-0.22.2-cp39-abi3-win_amd64.whl", hash = "sha256:c9ea31edff2968b44a88f97d784c2f16dc0729b8b143ed004699ebca91f05c48", size = 2747786, upload-time = "2026-01-05T10:45:18.411Z" }, + { url = "https://files.pythonhosted.org/packages/72/f4/0de46cfa12cdcbcd464cc59fde36912af405696f687e53a091fb432f694c/tokenizers-0.22.2-cp39-abi3-win_arm64.whl", hash = "sha256:9ce725d22864a1e965217204946f830c37876eee3b2ba6fc6255e8e903d5fcbc", size = 2612133, upload-time = "2026-01-05T10:45:17.232Z" }, +] + +[[package]] +name = "torch" +version = "2.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-bindings", marker = "sys_platform == 'linux'" }, + { name = "cuda-toolkit", extra = ["cublas", "cudart", "cufft", "cufile", "cupti", "curand", "cusolver", "cusparse", "nvjitlink", "nvrtc", "nvtx"], marker = "sys_platform == 'linux'" }, + { name = "filelock" }, + { name = "fsspec" }, + { name = "jinja2" }, + { name = "networkx" }, + { name = "nvidia-cudnn-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cusparselt-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nccl-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nvshmem-cu13", marker = "sys_platform == 'linux'" }, + { name = "setuptools" }, + { name = "sympy" }, + { name = "triton", marker = "sys_platform == 'linux'" }, + { name = "typing-extensions" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/0d/8603382f61abd0db35841148ddc1ffd607bf3100b11c6e1dab6d2fc44e72/torch-2.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:01018087326984a33b64e04c8cb5c2795f9120e0d775ada1f6638840227b04d7", size = 80573442, upload-time = "2026-03-23T18:09:10.117Z" }, + { url = "https://files.pythonhosted.org/packages/c7/86/7cd7c66cb9cec6be330fff36db5bd0eef386d80c031b581ec81be1d4b26c/torch-2.11.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:2bb3cc54bd0dea126b0060bb1ec9de0f9c7f7342d93d436646516b0330cd5be7", size = 419749385, upload-time = "2026-03-23T18:07:33.77Z" }, + { url = "https://files.pythonhosted.org/packages/47/e8/b98ca2d39b2e0e4730c0ee52537e488e7008025bc77ca89552ff91021f7c/torch-2.11.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:4dc8b3809469b6c30b411bb8c4cad3828efd26236153d9beb6a3ec500f211a60", size = 530716756, upload-time = "2026-03-23T18:07:50.02Z" }, + { url = "https://files.pythonhosted.org/packages/78/88/d4a4cda8362f8a30d1ed428564878c3cafb0d87971fbd3947d4c84552095/torch-2.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:2b4e811728bd0cc58fb2b0948fe939a1ee2bf1422f6025be2fca4c7bd9d79718", size = 114552300, upload-time = "2026-03-23T18:09:05.617Z" }, + { url = "https://files.pythonhosted.org/packages/bf/46/4419098ed6d801750f26567b478fc185c3432e11e2cad712bc6b4c2ab0d0/torch-2.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8245477871c3700d4370352ffec94b103cfcb737229445cf9946cddb7b2ca7cd", size = 80959460, upload-time = "2026-03-23T18:09:00.818Z" }, + { url = "https://files.pythonhosted.org/packages/fd/66/54a56a4a6ceaffb567231994a9745821d3af922a854ed33b0b3a278e0a99/torch-2.11.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:ab9a8482f475f9ba20e12db84b0e55e2f58784bdca43a854a6ccd3fd4b9f75e6", size = 419735835, upload-time = "2026-03-23T18:07:18.974Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e7/0b6665f533aa9e337662dc190425abc0af1fe3234088f4454c52393ded61/torch-2.11.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:563ed3d25542d7e7bbc5b235ccfacfeb97fb470c7fee257eae599adb8005c8a2", size = 530613405, upload-time = "2026-03-23T18:08:07.014Z" }, + { url = "https://files.pythonhosted.org/packages/cf/bf/c8d12a2c86dbfd7f40fb2f56fbf5a505ccf2d9ce131eb559dfc7c51e1a04/torch-2.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b2a43985ff5ef6ddd923bbcf99943e5f58059805787c5c9a2622bf05ca2965b0", size = 114792991, upload-time = "2026-03-23T18:08:19.216Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, +] + +[[package]] +name = "transformers" +version = "4.57.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "huggingface-hub" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "regex" }, + { name = "requests" }, + { name = "safetensors" }, + { name = "tokenizers" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c4/35/67252acc1b929dc88b6602e8c4a982e64f31e733b804c14bc24b47da35e6/transformers-4.57.6.tar.gz", hash = "sha256:55e44126ece9dc0a291521b7e5492b572e6ef2766338a610b9ab5afbb70689d3", size = 10134912, upload-time = "2026-01-16T10:38:39.284Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/b8/e484ef633af3887baeeb4b6ad12743363af7cce68ae51e938e00aaa0529d/transformers-4.57.6-py3-none-any.whl", hash = "sha256:4c9e9de11333ddfe5114bc872c9f370509198acf0b87a832a0ab9458e2bd0550", size = 11993498, upload-time = "2026-01-16T10:38:31.289Z" }, +] + +[[package]] +name = "triton" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/55/5ecf0dcaa0f2fbbd4420f7ef227ee3cb172e91e5fede9d0ecaddc43363b4/triton-3.6.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef5523241e7d1abca00f1d240949eebdd7c673b005edbbce0aca95b8191f1d43", size = 176138577, upload-time = "2026-01-20T16:16:25.426Z" }, + { url = "https://files.pythonhosted.org/packages/df/3d/9e7eee57b37c80cec63322c0231bb6da3cfe535a91d7a4d64896fcb89357/triton-3.6.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a17a5d5985f0ac494ed8a8e54568f092f7057ef60e1b0fa09d3fd1512064e803", size = 188273063, upload-time = "2026-01-20T16:01:07.278Z" }, + { url = "https://files.pythonhosted.org/packages/48/db/56ee649cab5eaff4757541325aca81f52d02d4a7cd3506776cad2451e060/triton-3.6.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b3a97e8ed304dfa9bd23bb41ca04cdf6b2e617d5e782a8653d616037a5d537d", size = 176274804, upload-time = "2026-01-20T16:16:31.528Z" }, + { url = "https://files.pythonhosted.org/packages/f6/56/6113c23ff46c00aae423333eb58b3e60bdfe9179d542781955a5e1514cb3/triton-3.6.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:46bd1c1af4b6704e554cad2eeb3b0a6513a980d470ccfa63189737340c7746a7", size = 188397994, upload-time = "2026-01-20T16:01:14.236Z" }, +] + +[[package]] +name = "typer" +version = "0.24.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/24/cb09efec5cc954f7f9b930bf8279447d24618bb6758d4f6adf2574c41780/typer-0.24.1.tar.gz", hash = "sha256:e39b4732d65fbdcde189ae76cf7cd48aeae72919dea1fdfc16593be016256b45", size = 118613, upload-time = "2026-02-21T16:54:40.609Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/91/48db081e7a63bb37284f9fbcefda7c44c277b18b0e13fbc36ea2335b71e6/typer-0.24.1-py3-none-any.whl", hash = "sha256:112c1f0ce578bfb4cab9ffdabc68f031416ebcc216536611ba21f04e9aa84c9e", size = 56085, upload-time = "2026-02-21T16:54:41.616Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspect" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/74/1789779d91f1961fa9438e9a8710cdae6bd138c80d7303996933d117264a/typing_inspect-0.9.0.tar.gz", hash = "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78", size = 13825, upload-time = "2023-05-24T20:25:47.612Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/65/f3/107a22063bf27bdccf2024833d3445f4eea42b2e598abfbd46f6a63b6cb0/typing_inspect-0.9.0-py3-none-any.whl", hash = "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f", size = 8827, upload-time = "2023-05-24T20:25:45.287Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "tzdata" +version = "2026.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/19/1b9b0e29f30c6d35cb345486df41110984ea67ae69dddbc0e8a100999493/tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", size = 198254, upload-time = "2026-04-24T15:22:08.651Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321, upload-time = "2026-04-24T15:22:05.876Z" }, +] + +[[package]] +name = "uritemplate" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/60/f174043244c5306c9988380d2cb10009f91563fc4b31293d27e17201af56/uritemplate-4.2.0.tar.gz", hash = "sha256:480c2ed180878955863323eea31b0ede668795de182617fef9c6ca09e6ec9d0e", size = 33267, upload-time = "2025-06-02T15:12:06.318Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/99/3ae339466c9183ea5b8ae87b34c0b897eda475d2aec2307cae60e5cd4f29/uritemplate-4.2.0-py3-none-any.whl", hash = "sha256:962201ba1c4edcab02e60f9a0d3821e82dfc5d2d6662a21abd533879bdb8a686", size = 11488, upload-time = "2025-06-02T15:12:03.405Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] + +[[package]] +name = "uuid-utils" +version = "0.14.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/d1/38a573f0c631c062cf42fa1f5d021d4dd3c31fb23e4376e4b56b0c9fbbed/uuid_utils-0.14.1.tar.gz", hash = "sha256:9bfc95f64af80ccf129c604fb6b8ca66c6f256451e32bc4570f760e4309c9b69", size = 22195, upload-time = "2026-02-20T22:50:38.833Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/b7/add4363039a34506a58457d96d4aa2126061df3a143eb4d042aedd6a2e76/uuid_utils-0.14.1-cp39-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:93a3b5dc798a54a1feb693f2d1cb4cf08258c32ff05ae4929b5f0a2ca624a4f0", size = 604679, upload-time = "2026-02-20T22:50:27.469Z" }, + { url = "https://files.pythonhosted.org/packages/dd/84/d1d0bef50d9e66d31b2019997c741b42274d53dde2e001b7a83e9511c339/uuid_utils-0.14.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:ccd65a4b8e83af23eae5e56d88034b2fe7264f465d3e830845f10d1591b81741", size = 309346, upload-time = "2026-02-20T22:50:31.857Z" }, + { url = "https://files.pythonhosted.org/packages/ef/ed/b6d6fd52a6636d7c3eddf97d68da50910bf17cd5ac221992506fb56cf12e/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b56b0cacd81583834820588378e432b0696186683b813058b707aedc1e16c4b1", size = 344714, upload-time = "2026-02-20T22:50:42.642Z" }, + { url = "https://files.pythonhosted.org/packages/a8/a7/a19a1719fb626fe0b31882db36056d44fe904dc0cf15b06fdf56b2679cf7/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb3cf14de789097320a3c56bfdfdd51b1225d11d67298afbedee7e84e3837c96", size = 350914, upload-time = "2026-02-20T22:50:36.487Z" }, + { url = "https://files.pythonhosted.org/packages/1d/fc/f6690e667fdc3bb1a73f57951f97497771c56fe23e3d302d7404be394d4f/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60e0854a90d67f4b0cc6e54773deb8be618f4c9bad98d3326f081423b5d14fae", size = 482609, upload-time = "2026-02-20T22:50:37.511Z" }, + { url = "https://files.pythonhosted.org/packages/54/6e/dcd3fa031320921a12ec7b4672dea3bd1dd90ddffa363a91831ba834d559/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce6743ba194de3910b5feb1a62590cd2587e33a73ab6af8a01b642ceb5055862", size = 345699, upload-time = "2026-02-20T22:50:46.87Z" }, + { url = "https://files.pythonhosted.org/packages/04/28/e5220204b58b44ac0047226a9d016a113fde039280cc8732d9e6da43b39f/uuid_utils-0.14.1-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:043fb58fde6cf1620a6c066382f04f87a8e74feb0f95a585e4ed46f5d44af57b", size = 372205, upload-time = "2026-02-20T22:50:28.438Z" }, + { url = "https://files.pythonhosted.org/packages/c7/d9/3d2eb98af94b8dfffc82b6a33b4dfc87b0a5de2c68a28f6dde0db1f8681b/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c915d53f22945e55fe0d3d3b0b87fd965a57f5fd15666fd92d6593a73b1dd297", size = 521836, upload-time = "2026-02-20T22:50:23.057Z" }, + { url = "https://files.pythonhosted.org/packages/a8/15/0eb106cc6fe182f7577bc0ab6e2f0a40be247f35c5e297dbf7bbc460bd02/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:0972488e3f9b449e83f006ead5a0e0a33ad4a13e4462e865b7c286ab7d7566a3", size = 625260, upload-time = "2026-02-20T22:50:25.949Z" }, + { url = "https://files.pythonhosted.org/packages/3c/17/f539507091334b109e7496830af2f093d9fc8082411eafd3ece58af1f8ba/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:1c238812ae0c8ffe77d8d447a32c6dfd058ea4631246b08b5a71df586ff08531", size = 587824, upload-time = "2026-02-20T22:50:35.225Z" }, + { url = "https://files.pythonhosted.org/packages/2e/c2/d37a7b2e41f153519367d4db01f0526e0d4b06f1a4a87f1c5dfca5d70a8b/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:bec8f8ef627af86abf8298e7ec50926627e29b34fa907fcfbedb45aaa72bca43", size = 551407, upload-time = "2026-02-20T22:50:44.915Z" }, + { url = "https://files.pythonhosted.org/packages/65/36/2d24b2cbe78547c6532da33fb8613debd3126eccc33a6374ab788f5e46e9/uuid_utils-0.14.1-cp39-abi3-win32.whl", hash = "sha256:b54d6aa6252d96bac1fdbc80d26ba71bad9f220b2724d692ad2f2310c22ef523", size = 183476, upload-time = "2026-02-20T22:50:32.745Z" }, + { url = "https://files.pythonhosted.org/packages/83/92/2d7e90df8b1a69ec4cff33243ce02b7a62f926ef9e2f0eca5a026889cd73/uuid_utils-0.14.1-cp39-abi3-win_amd64.whl", hash = "sha256:fc27638c2ce267a0ce3e06828aff786f91367f093c80625ee21dad0208e0f5ba", size = 187147, upload-time = "2026-02-20T22:50:45.807Z" }, + { url = "https://files.pythonhosted.org/packages/d9/26/529f4beee17e5248e37e0bc17a2761d34c0fa3b1e5729c88adb2065bae6e/uuid_utils-0.14.1-cp39-abi3-win_arm64.whl", hash = "sha256:b04cb49b42afbc4ff8dbc60cf054930afc479d6f4dd7f1ec3bbe5dbfdde06b7a", size = 188132, upload-time = "2026-02-20T22:50:41.718Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.44.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/da/6eee1ff8b6cbeed47eeb5229749168e81eb4b7b999a1a15a7176e51410c9/uvicorn-0.44.0.tar.gz", hash = "sha256:6c942071b68f07e178264b9152f1f16dfac5da85880c4ce06366a96d70d4f31e", size = 86947, upload-time = "2026-04-06T09:23:22.826Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/23/a5bbd9600dd607411fa644c06ff4951bec3a4d82c4b852374024359c19c0/uvicorn-0.44.0-py3-none-any.whl", hash = "sha256:ce937c99a2cc70279556967274414c087888e8cec9f9c94644dfca11bd3ced89", size = 69425, upload-time = "2026-04-06T09:23:21.524Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "httptools" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, + { name = "watchfiles" }, + { name = "websockets" }, +] + +[[package]] +name = "uvloop" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067, upload-time = "2025-10-16T22:16:44.503Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423, upload-time = "2025-10-16T22:16:45.968Z" }, + { url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437, upload-time = "2025-10-16T22:16:47.451Z" }, + { url = "https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101, upload-time = "2025-10-16T22:16:49.318Z" }, + { url = "https://files.pythonhosted.org/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158, upload-time = "2025-10-16T22:16:50.517Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360, upload-time = "2025-10-16T22:16:52.646Z" }, + { url = "https://files.pythonhosted.org/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790, upload-time = "2025-10-16T22:16:54.355Z" }, + { url = "https://files.pythonhosted.org/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783, upload-time = "2025-10-16T22:16:55.906Z" }, + { url = "https://files.pythonhosted.org/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548, upload-time = "2025-10-16T22:16:57.008Z" }, + { url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065, upload-time = "2025-10-16T22:16:58.206Z" }, + { url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384, upload-time = "2025-10-16T22:16:59.36Z" }, + { url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" }, +] + +[[package]] +name = "watchfiles" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315, upload-time = "2025-10-14T15:05:26.501Z" }, + { url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869, upload-time = "2025-10-14T15:05:27.649Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919, upload-time = "2025-10-14T15:05:28.701Z" }, + { url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845, upload-time = "2025-10-14T15:05:30.064Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027, upload-time = "2025-10-14T15:05:31.064Z" }, + { url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615, upload-time = "2025-10-14T15:05:32.074Z" }, + { url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836, upload-time = "2025-10-14T15:05:33.209Z" }, + { url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099, upload-time = "2025-10-14T15:05:34.189Z" }, + { url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626, upload-time = "2025-10-14T15:05:35.216Z" }, + { url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519, upload-time = "2025-10-14T15:05:36.259Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ce/d8acdc8de545de995c339be67711e474c77d643555a9bb74a9334252bd55/watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", size = 272078, upload-time = "2025-10-14T15:05:37.63Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c9/a74487f72d0451524be827e8edec251da0cc1fcf111646a511ae752e1a3d/watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", size = 287664, upload-time = "2025-10-14T15:05:38.95Z" }, + { url = "https://files.pythonhosted.org/packages/df/b8/8ac000702cdd496cdce998c6f4ee0ca1f15977bba51bdf07d872ebdfc34c/watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", size = 277154, upload-time = "2025-10-14T15:05:39.954Z" }, + { url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820, upload-time = "2025-10-14T15:05:40.932Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510, upload-time = "2025-10-14T15:05:41.945Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408, upload-time = "2025-10-14T15:05:43.385Z" }, + { url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968, upload-time = "2025-10-14T15:05:44.404Z" }, + { url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096, upload-time = "2025-10-14T15:05:45.398Z" }, + { url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040, upload-time = "2025-10-14T15:05:46.502Z" }, + { url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847, upload-time = "2025-10-14T15:05:47.484Z" }, + { url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072, upload-time = "2025-10-14T15:05:48.928Z" }, + { url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104, upload-time = "2025-10-14T15:05:49.908Z" }, + { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" }, +] + +[[package]] +name = "websocket-client" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/41/aa4bf9664e4cda14c3b39865b12251e8e7d239f4cd0e3cc1b6c2ccde25c1/websocket_client-1.9.0.tar.gz", hash = "sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98", size = 70576, upload-time = "2025-10-07T21:16:36.495Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-py3-none-any.whl", hash = "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef", size = 82616, upload-time = "2025-10-07T21:16:34.951Z" }, +] + +[[package]] +name = "websockets" +version = "15.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, +] + +[[package]] +name = "xgboost" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "nvidia-nccl-cu12", marker = "sys_platform == 'linux'" }, + { name = "scipy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/91/bb/1eb0242409d22db725d7a88088e6cfd6556829fb0736f9ff69aa9f1e9455/xgboost-3.2.0.tar.gz", hash = "sha256:99b0e9a2a64896cdaf509c5e46372d336c692406646d20f2af505003c0c5d70d", size = 1263936, upload-time = "2026-02-10T11:03:05.542Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/49/6e4cdd877c24adf56cb3586bc96d93d4dcd780b5ea1efb32e1ee0de08bae/xgboost-3.2.0-py3-none-macosx_10_15_x86_64.whl", hash = "sha256:2f661966d3e322536d9c448090a870fcba1e32ee5760c10b7c46bac7a342079a", size = 2507014, upload-time = "2026-02-10T10:50:57.44Z" }, + { url = "https://files.pythonhosted.org/packages/93/f1/c09ef1add609453aa3ba5bafcd0d1c1a805c1263c0b60138ec968f8ec296/xgboost-3.2.0-py3-none-macosx_12_0_arm64.whl", hash = "sha256:eabbd40d474b8dbf6cb3536325f9150b9e6f0db32d18de9914fb3227d0bef5b7", size = 2328527, upload-time = "2026-02-10T10:51:17.502Z" }, + { url = "https://files.pythonhosted.org/packages/96/9f/d9914a7b8df842832850b1a18e5f47aaa071c217cdd1da2ae9deb291018b/xgboost-3.2.0-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:852eabc6d3b3702a59bf78dbfdcd1cb9c4d3a3b6e5ed1f8781d8b9512354fdd2", size = 131100954, upload-time = "2026-02-10T11:02:42.704Z" }, + { url = "https://files.pythonhosted.org/packages/79/98/679de17c2caa4fd3b0b4386ecf7377301702cb0afb22930a07c142fcb1d8/xgboost-3.2.0-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:99b4a6bbcb47212fec5cf5fbe12347215f073c08967431b0122cfbd1ee70312c", size = 131748579, upload-time = "2026-02-10T10:54:40.424Z" }, + { url = "https://files.pythonhosted.org/packages/1f/3d/1661dd114a914a67e3f7ab66fa1382e7599c2a8c340f314ad30a3e2b4d08/xgboost-3.2.0-py3-none-win_amd64.whl", hash = "sha256:0d169736fd836fc13646c7ab787167b3a8110351c2c6bc770c755ee1618f0442", size = 101681668, upload-time = "2026-02-10T10:59:31.202Z" }, +] + +[[package]] +name = "xxhash" +version = "3.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/2f/e183a1b407002f5af81822bee18b61cdb94b8670208ef34734d8d2b8ebe9/xxhash-3.7.0.tar.gz", hash = "sha256:6cc4eefbb542a5d6ffd6d70ea9c502957c925e800f998c5630ecc809d6702bae", size = 82022, upload-time = "2026-04-25T11:10:32.553Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/cc/431db584f6fbb9312e40a173af027644e5580d39df1f73603cbb9dca4d6b/xxhash-3.7.0-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:8c5fcfd806c335bfa2adf1cd0b3110a44fc7b6995c3a648c27489bae85801465", size = 36644, upload-time = "2026-04-25T11:08:00.658Z" }, + { url = "https://files.pythonhosted.org/packages/bc/01/255ec513e0a705d1f9a61413e78dfce4e3235203f0ed525a24c2b4b56345/xxhash-3.7.0-cp314-cp314-android_24_x86_64.whl", hash = "sha256:506a0b488f190f0a06769575e30caf71615c898ed93ab18b0dbcb6dec5c3713c", size = 35003, upload-time = "2026-04-25T11:08:02.338Z" }, + { url = "https://files.pythonhosted.org/packages/68/70/c55fc33c93445b44d8fc5a17b41ed99e3cebe92bcf8396809e63fc9a1165/xxhash-3.7.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:ec68dbba21532c0173a9872298e65c89749f7c9d21538c3a78b5bb6105871568", size = 29655, upload-time = "2026-04-25T11:08:03.701Z" }, + { url = "https://files.pythonhosted.org/packages/c2/72/ff8de73df000d74467d12a59ce6d6e2b2a368b978d41ab7b1fba5ed442be/xxhash-3.7.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:fa77e7ec1450d415d20129961814787c9abd9a07f98872f070b1fe96c5084611", size = 30664, upload-time = "2026-04-25T11:08:05.011Z" }, + { url = "https://files.pythonhosted.org/packages/b6/91/08416d9bd9bc3bf39d831abe8a5631ac2db5141dfd6fe81c3fe59a1f9264/xxhash-3.7.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:fe32736295ea38e43e7d9424053c8c47c9f64fecfc7c895fb3da9b30b131c9ee", size = 33317, upload-time = "2026-04-25T11:08:06.413Z" }, + { url = "https://files.pythonhosted.org/packages/0e/3b/86b1caa4dee10a99f4bf9521e623359341c5e50d05158fa10c275b2bd079/xxhash-3.7.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ab9dd2c83c4bbd63e422181a76f13502d049d3ddcac9a1bdc29196263d692bb8", size = 33457, upload-time = "2026-04-25T11:08:08.099Z" }, + { url = "https://files.pythonhosted.org/packages/ed/38/98ea14ad1517e1461292a65906951458d520689782bfbae111050145bdba/xxhash-3.7.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3afec3a336a2286601a437cb07562ab0227685e6fbb9ec17e8c18457ff348ecf", size = 30894, upload-time = "2026-04-25T11:08:09.429Z" }, + { url = "https://files.pythonhosted.org/packages/61/a2/074654d0b893606541199993c7db70067d9fc63b748e0d60020a52a1bd36/xxhash-3.7.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:565df64437a9390f84465dcca33e7377114c7ede8d05cd2cf20081f831ea788e", size = 194409, upload-time = "2026-04-25T11:08:10.91Z" }, + { url = "https://files.pythonhosted.org/packages/e2/26/6d2a1afc468189f77ca28c32e1c83e1b9da1178231e05641dbc1b350e332/xxhash-3.7.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:12eca820a5d558633d423bf8bb78ce72a55394823f64089247f788a7e0ae691e", size = 213135, upload-time = "2026-04-25T11:08:12.575Z" }, + { url = "https://files.pythonhosted.org/packages/8e/0e/d8aecf95e09c42547453137be74d2f7b8b14e08f5177fa2fab6144a19061/xxhash-3.7.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f262b8f7599516567e070abf607b9af649052b2c4bd6f9be02b0cb41b7024805", size = 236379, upload-time = "2026-04-25T11:08:14.206Z" }, + { url = "https://files.pythonhosted.org/packages/f2/74/8140e8210536b3dd0cc816c4faaeb5ba6e63e8125ab25af4bcddd6a037b3/xxhash-3.7.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1598916cb197681e03e601901e4ab96a9a963de398c59d0964f8a6f44a2b361", size = 212447, upload-time = "2026-04-25T11:08:15.79Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d2/462001d2903b4bee5a5689598a0a55e5e7cd1ac7f4247a5545cff10d3ebb/xxhash-3.7.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:322b2f0622230f526aeb1738149948a7ae357a9e2ceb1383c6fd1fdaecdafa16", size = 445660, upload-time = "2026-04-25T11:08:17.441Z" }, + { url = "https://files.pythonhosted.org/packages/23/09/2bd1ed7f8689b20e51727952cac8329d50c694dc32b2eba06ba5bc742b37/xxhash-3.7.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24cc22070880cc57b830a65cde4e65fa884c6d9b28ae4803b5ee05911e7bafba", size = 194076, upload-time = "2026-04-25T11:08:19.134Z" }, + { url = "https://files.pythonhosted.org/packages/c9/6e/692302cd0a5f4ac4e6289f37fa888dc2e1e07750b68fe3e4bfe939b8cea3/xxhash-3.7.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb5a888a968b2434abf9ecda357b5d43f10d7b5a6da6fdbbe036208473aff0e2", size = 284990, upload-time = "2026-04-25T11:08:20.618Z" }, + { url = "https://files.pythonhosted.org/packages/05/d9/e54b159b3d9df7999d2a7c676ce7b323d1b5588a64f8f51ed8172567bd87/xxhash-3.7.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a999771ff97bec27d18341be4f3a36b163bb1ac41ec17bef6d2dabd84acd33c7", size = 210590, upload-time = "2026-04-25T11:08:22.24Z" }, + { url = "https://files.pythonhosted.org/packages/50/93/0e0df1a3a196ced4ca71de76d65ead25d8e87bbfb87b64306ea47a40c00d/xxhash-3.7.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:ed4a6efe2dee1655adb73e7ad40c6aa955a6892422b1e3b95de6a34de56e3cbb", size = 241442, upload-time = "2026-04-25T11:08:23.844Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a9/d917a7a814e90b218f8a0d37967105eea91bf752c3303683c99a1f7bfc1f/xxhash-3.7.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9fd17f14ac0faa12126c2f9ca774a8cf342957265ec3c8669c144e5e6cdb478c", size = 198356, upload-time = "2026-04-25T11:08:25.99Z" }, + { url = "https://files.pythonhosted.org/packages/89/5e/f2ba1877c39469abbefc72991d6ebdcbd4c0880db01ae8cb1f553b0c537d/xxhash-3.7.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:05fd1254268c59b5cb2a029dfc204275e9fc52de2913f1e53aa8d01442c96b4d", size = 210898, upload-time = "2026-04-25T11:08:27.608Z" }, + { url = "https://files.pythonhosted.org/packages/90/c6/be56b58e73de531f39a10de1355bb77ceb663900dc4bf2d6d3002a9c3f9e/xxhash-3.7.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:a2eae53197c6276d5b317f75a1be226bbf440c20b58bf525f36b5d0e1f657ca6", size = 275519, upload-time = "2026-04-25T11:08:29.301Z" }, + { url = "https://files.pythonhosted.org/packages/92/e2/17ddc85d5765b9c709f192009ed8f5a1fc876f4eb35bba7c307b5b1169f9/xxhash-3.7.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:bfe6f92e3522dcbe8c4281efd74fa7542a336cb00b0e3272c4ec0edabeaeaf67", size = 414191, upload-time = "2026-04-25T11:08:31.16Z" }, + { url = "https://files.pythonhosted.org/packages/9c/42/85f5b79f4bf1ec7ba052491164adfd4f4e9519f5dc7246de4fbd64a1bd56/xxhash-3.7.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7ab9a49c410d8c6c786ab99e79c529938d894c01433130353dd0fe999111077a", size = 191604, upload-time = "2026-04-25T11:08:32.862Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d0/6127b623aa4cca18d8b7743592b048d689fd6c6e37ff26a22cddf6cd9d7f/xxhash-3.7.0-cp314-cp314-win32.whl", hash = "sha256:040ea63668f9185b92bc74942df09c7e65703deed71431333678fc6e739a9955", size = 31271, upload-time = "2026-04-25T11:08:34.651Z" }, + { url = "https://files.pythonhosted.org/packages/64/4f/44fc4788568004c43921701cbc127f48218a1eede2c9aea231115323564d/xxhash-3.7.0-cp314-cp314-win_amd64.whl", hash = "sha256:2a61e2a3fb23c892496d587b470dee7fa1b58b248a187719c65ea8e94ec13257", size = 32284, upload-time = "2026-04-25T11:08:35.987Z" }, + { url = "https://files.pythonhosted.org/packages/6d/77/18bb895eb60a49453d16e17d67990e5caff557c78eafc90ad4e2eabf4570/xxhash-3.7.0-cp314-cp314-win_arm64.whl", hash = "sha256:c7741c7524961d8c0cb4d4c21b28957ff731a3fd5b5cd8b856dc80a40e9e5acc", size = 28701, upload-time = "2026-04-25T11:08:37.767Z" }, + { url = "https://files.pythonhosted.org/packages/45/a0/46f72244570c550fbbb7db1ef554183dd5ebe9136385f30e032b781ae8f6/xxhash-3.7.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:fc84bf7aa7592f31ec63a3e7b11d624f468a3f19f5238cec7282a42e838ab1d7", size = 33646, upload-time = "2026-04-25T11:08:39.109Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3a/453846a7eceea11e75def361eed01ec6a0205b9822c19927ed364ccae7cc/xxhash-3.7.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9f1563fdc8abfc389748e6932c7e4e99c89a53e4ec37d4563c24fc06f5e5644b", size = 31125, upload-time = "2026-04-25T11:08:40.467Z" }, + { url = "https://files.pythonhosted.org/packages/bd/3e/49434aba738885d512f9e486db1bdd19db28dfa40372b56da26ef7a4e738/xxhash-3.7.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2d415f18becf6f153046ab6adc97da77e3643a0ee205dae61c4012604113a020", size = 196633, upload-time = "2026-04-25T11:08:41.943Z" }, + { url = "https://files.pythonhosted.org/packages/a4/e9/006cb6127baeb9f8abe6d15e62faa01349f09b34e2bfd65175b2422d026b/xxhash-3.7.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bb16aa13ed175bc9be5c2491ba031b85a9b51c4ed90e0b3d4ebe63cf3fb54f8e", size = 215899, upload-time = "2026-04-25T11:08:43.645Z" }, + { url = "https://files.pythonhosted.org/packages/27/e4/cc57d72e66df0ae29b914335f1c6dcf61e8f3746ddf0ae3c471aa4f15e00/xxhash-3.7.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f9fd595f1e5941b3d7863e4774e4b30caa6731fc34b9277da032295aa5656ee5", size = 238116, upload-time = "2026-04-25T11:08:45.698Z" }, + { url = "https://files.pythonhosted.org/packages/af/78/3531d4a3fd8a0038cc6be1f265a69c1b3587f557a10b677dd736de2202c1/xxhash-3.7.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1295325c5a98d552333fa53dc2b026b0ef0ec9c8e73ca3a952990b4c7d65d459", size = 215012, upload-time = "2026-04-25T11:08:47.355Z" }, + { url = "https://files.pythonhosted.org/packages/b4/f6/259fb1eaaec921f59b17203b0daee69829761226d3b980d5191d7723dd83/xxhash-3.7.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3573a651d146912da9daa9e29e5fbc45994420daaa9ef1e2fa5823e1dc485513", size = 448534, upload-time = "2026-04-25T11:08:49.149Z" }, + { url = "https://files.pythonhosted.org/packages/7b/16/a66d0eaf6a7e68532c07714361ddc904c663ec940f3b028c1ae4a21a7b9d/xxhash-3.7.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ec1e080a3d02d94ea9335bfab0e3374b877e25411422c18f51a943fa4b46381", size = 196217, upload-time = "2026-04-25T11:08:50.805Z" }, + { url = "https://files.pythonhosted.org/packages/8d/ef/d2efc7fc51756dc52509109d1a25cefc859d74bc4b19a167b12dbd8c2786/xxhash-3.7.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:84415265192072d8638a3afc3c1bc5995e310570cd9acb54dc46d3939e364fe0", size = 286906, upload-time = "2026-04-25T11:08:52.418Z" }, + { url = "https://files.pythonhosted.org/packages/fc/67/25decd1d4a4018582ec4db2a868a2b7e40640f4adb20dfeb19ac923aa825/xxhash-3.7.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d4dea659b57443989ef32f4295104fd6912c73d0bf26d1d148bb88a9f159b02", size = 213057, upload-time = "2026-04-25T11:08:54.105Z" }, + { url = "https://files.pythonhosted.org/packages/0d/5d/17651eb29d06786cdc40c60ae3d27d645aa5d61d2eca6237a7ba0b94789b/xxhash-3.7.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:05ece0fe4d9c9c2728912d1981ae1566cfc83a011571b24732cbf76e1fb70dca", size = 243886, upload-time = "2026-04-25T11:08:56.109Z" }, + { url = "https://files.pythonhosted.org/packages/8a/d4/174d9cf7502243d586e6a9ae842b1ae23026620995114f85f1380e588bc9/xxhash-3.7.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:fd880353cf1ffaf321bc18dd663e111976dbd0d3bbd8a66d58d2b470dfa7f396", size = 201015, upload-time = "2026-04-25T11:08:57.777Z" }, + { url = "https://files.pythonhosted.org/packages/91/8c/2254e2d06c3ac5e6fe22eaf3da791b87ea823ae9f2c17b4af66755c5752d/xxhash-3.7.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:4e15cc9e2817f6481160f930c62842b3ff419e20e13072bcbab12230943092bc", size = 213457, upload-time = "2026-04-25T11:08:59.826Z" }, + { url = "https://files.pythonhosted.org/packages/79/a2/e3daa762545921173e3360f3b4ff7fc63c2d27359f7230ec1a7a74e117f6/xxhash-3.7.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:90b9d1a8bd37d768ffc92a1f651ec69afc532a96fa1ac2ea7abbed5d630b3237", size = 277738, upload-time = "2026-04-25T11:09:01.423Z" }, + { url = "https://files.pythonhosted.org/packages/e1/4c/e186da2c46b87f5204640e008d42730bf3c1ee9f0efb71ae1ebcdfeac681/xxhash-3.7.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:157c49475b34ecea8809e51123d9769a534e139d1247942f7a4bc67710bb2533", size = 417127, upload-time = "2026-04-25T11:09:03.592Z" }, + { url = "https://files.pythonhosted.org/packages/17/28/3798e15007a3712d0da3d3fe70f8e11916569858b5cc371053bc26270832/xxhash-3.7.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5a6ddec83325685e729ca119d1f5c518ec39294212ecd770e60693cdc5f7eb79", size = 193962, upload-time = "2026-04-25T11:09:06.228Z" }, + { url = "https://files.pythonhosted.org/packages/ad/95/a26baa93b5241fd7630998816a4ec47a5a0bad193b3f8fc8f3593e1a4a67/xxhash-3.7.0-cp314-cp314t-win32.whl", hash = "sha256:a04a6cab47e2166435aaf5b9e5ee41d1532cc8300efdef87f2a4d0acb7db19ed", size = 31643, upload-time = "2026-04-25T11:09:08.153Z" }, + { url = "https://files.pythonhosted.org/packages/44/36/5454f13c447e395f9b06a3e91274c59f503d31fad84e1836efe3bdb71f6a/xxhash-3.7.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8653dd7c2eda020545bb2c71c7f7039b53fe7434d0fc1a0a9deb79ab3f1a4fc1", size = 32522, upload-time = "2026-04-25T11:09:09.534Z" }, + { url = "https://files.pythonhosted.org/packages/74/35/698e7e3ff38e22992ea24870a511d8762474fb6783627a2910ff22a185c2/xxhash-3.7.0-cp314-cp314t-win_arm64.whl", hash = "sha256:468f0fc114faaa4b36699f8e328bbc3bb11dc418ba94ac52c26dd736d4b6c637", size = 28807, upload-time = "2026-04-25T11:09:11.234Z" }, +] + +[[package]] +name = "yarl" +version = "1.23.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/23/6e/beb1beec874a72f23815c1434518bfc4ed2175065173fb138c3705f658d4/yarl-1.23.0.tar.gz", hash = "sha256:53b1ea6ca88ebd4420379c330aea57e258408dd0df9af0992e5de2078dc9f5d5", size = 194676, upload-time = "2026-03-01T22:07:53.373Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/98/b85a038d65d1b92c3903ab89444f48d3cee490a883477b716d7a24b1a78c/yarl-1.23.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:21d1b7305a71a15b4794b5ff22e8eef96ff4a6d7f9657155e5aa419444b28912", size = 124455, upload-time = "2026-03-01T22:06:43.615Z" }, + { url = "https://files.pythonhosted.org/packages/39/54/bc2b45559f86543d163b6e294417a107bb87557609007c007ad889afec18/yarl-1.23.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:85610b4f27f69984932a7abbe52703688de3724d9f72bceb1cca667deff27474", size = 86752, upload-time = "2026-03-01T22:06:45.425Z" }, + { url = "https://files.pythonhosted.org/packages/24/f9/e8242b68362bffe6fb536c8db5076861466fc780f0f1b479fc4ffbebb128/yarl-1.23.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23f371bd662cf44a7630d4d113101eafc0cfa7518a2760d20760b26021454719", size = 86291, upload-time = "2026-03-01T22:06:46.974Z" }, + { url = "https://files.pythonhosted.org/packages/ea/d8/d1cb2378c81dd729e98c716582b1ccb08357e8488e4c24714658cc6630e8/yarl-1.23.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a80f77dc1acaaa61f0934176fccca7096d9b1ff08c8ba9cddf5ae034a24319", size = 99026, upload-time = "2026-03-01T22:06:48.459Z" }, + { url = "https://files.pythonhosted.org/packages/0a/ff/7196790538f31debe3341283b5b0707e7feb947620fc5e8236ef28d44f72/yarl-1.23.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:bd654fad46d8d9e823afbb4f87c79160b5a374ed1ff5bde24e542e6ba8f41434", size = 92355, upload-time = "2026-03-01T22:06:50.306Z" }, + { url = "https://files.pythonhosted.org/packages/c1/56/25d58c3eddde825890a5fe6aa1866228377354a3c39262235234ab5f616b/yarl-1.23.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:682bae25f0a0dd23a056739f23a134db9f52a63e2afd6bfb37ddc76292bbd723", size = 106417, upload-time = "2026-03-01T22:06:52.1Z" }, + { url = "https://files.pythonhosted.org/packages/51/8a/882c0e7bc8277eb895b31bce0138f51a1ba551fc2e1ec6753ffc1e7c1377/yarl-1.23.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a82836cab5f197a0514235aaf7ffccdc886ccdaa2324bc0aafdd4ae898103039", size = 106422, upload-time = "2026-03-01T22:06:54.424Z" }, + { url = "https://files.pythonhosted.org/packages/42/2b/fef67d616931055bf3d6764885990a3ac647d68734a2d6a9e1d13de437a2/yarl-1.23.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c57676bdedc94cd3bc37724cf6f8cd2779f02f6aba48de45feca073e714fe52", size = 101915, upload-time = "2026-03-01T22:06:55.895Z" }, + { url = "https://files.pythonhosted.org/packages/18/6a/530e16aebce27c5937920f3431c628a29a4b6b430fab3fd1c117b26ff3f6/yarl-1.23.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c7f8dc16c498ff06497c015642333219871effba93e4a2e8604a06264aca5c5c", size = 100690, upload-time = "2026-03-01T22:06:58.21Z" }, + { url = "https://files.pythonhosted.org/packages/88/08/93749219179a45e27b036e03260fda05190b911de8e18225c294ac95bbc9/yarl-1.23.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5ee586fb17ff8f90c91cf73c6108a434b02d69925f44f5f8e0d7f2f260607eae", size = 98750, upload-time = "2026-03-01T22:06:59.794Z" }, + { url = "https://files.pythonhosted.org/packages/d9/cf/ea424a004969f5d81a362110a6ac1496d79efdc6d50c2c4b2e3ea0fc2519/yarl-1.23.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:17235362f580149742739cc3828b80e24029d08cbb9c4bda0242c7b5bc610a8e", size = 94685, upload-time = "2026-03-01T22:07:01.375Z" }, + { url = "https://files.pythonhosted.org/packages/e2/b7/14341481fe568e2b0408bcf1484c652accafe06a0ade9387b5d3fd9df446/yarl-1.23.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0793e2bd0cf14234983bbb371591e6bea9e876ddf6896cdcc93450996b0b5c85", size = 106009, upload-time = "2026-03-01T22:07:03.151Z" }, + { url = "https://files.pythonhosted.org/packages/0a/e6/5c744a9b54f4e8007ad35bce96fbc9218338e84812d36f3390cea616881a/yarl-1.23.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3650dc2480f94f7116c364096bc84b1d602f44224ef7d5c7208425915c0475dd", size = 100033, upload-time = "2026-03-01T22:07:04.701Z" }, + { url = "https://files.pythonhosted.org/packages/0c/23/e3bfc188d0b400f025bc49d99793d02c9abe15752138dcc27e4eaf0c4a9e/yarl-1.23.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f40e782d49630ad384db66d4d8b73ff4f1b8955dc12e26b09a3e3af064b3b9d6", size = 106483, upload-time = "2026-03-01T22:07:06.231Z" }, + { url = "https://files.pythonhosted.org/packages/72/42/f0505f949a90b3f8b7a363d6cbdf398f6e6c58946d85c6d3a3bc70595b26/yarl-1.23.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94f8575fbdf81749008d980c17796097e645574a3b8c28ee313931068dad14fe", size = 102175, upload-time = "2026-03-01T22:07:08.4Z" }, + { url = "https://files.pythonhosted.org/packages/aa/65/b39290f1d892a9dd671d1c722014ca062a9c35d60885d57e5375db0404b5/yarl-1.23.0-cp314-cp314-win32.whl", hash = "sha256:c8aa34a5c864db1087d911a0b902d60d203ea3607d91f615acd3f3108ac32169", size = 83871, upload-time = "2026-03-01T22:07:09.968Z" }, + { url = "https://files.pythonhosted.org/packages/a9/5b/9b92f54c784c26e2a422e55a8d2607ab15b7ea3349e28359282f84f01d43/yarl-1.23.0-cp314-cp314-win_amd64.whl", hash = "sha256:63e92247f383c85ab00dd0091e8c3fa331a96e865459f5ee80353c70a4a42d70", size = 89093, upload-time = "2026-03-01T22:07:11.501Z" }, + { url = "https://files.pythonhosted.org/packages/e0/7d/8a84dc9381fd4412d5e7ff04926f9865f6372b4c2fd91e10092e65d29eb8/yarl-1.23.0-cp314-cp314-win_arm64.whl", hash = "sha256:70efd20be968c76ece7baa8dafe04c5be06abc57f754d6f36f3741f7aa7a208e", size = 83384, upload-time = "2026-03-01T22:07:13.069Z" }, + { url = "https://files.pythonhosted.org/packages/dd/8d/d2fad34b1c08aa161b74394183daa7d800141aaaee207317e82c790b418d/yarl-1.23.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:9a18d6f9359e45722c064c97464ec883eb0e0366d33eda61cb19a244bf222679", size = 131019, upload-time = "2026-03-01T22:07:14.903Z" }, + { url = "https://files.pythonhosted.org/packages/19/ff/33009a39d3ccf4b94d7d7880dfe17fb5816c5a4fe0096d9b56abceea9ac7/yarl-1.23.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2803ed8b21ca47a43da80a6fd1ed3019d30061f7061daa35ac54f63933409412", size = 89894, upload-time = "2026-03-01T22:07:17.372Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f1/dab7ac5e7306fb79c0190766a3c00b4cb8d09a1f390ded68c85a5934faf5/yarl-1.23.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:394906945aa8b19fc14a61cf69743a868bb8c465efe85eee687109cc540b98f4", size = 89979, upload-time = "2026-03-01T22:07:19.361Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b1/08e95f3caee1fad6e65017b9f26c1d79877b502622d60e517de01e72f95d/yarl-1.23.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71d006bee8397a4a89f469b8deb22469fe7508132d3c17fa6ed871e79832691c", size = 95943, upload-time = "2026-03-01T22:07:21.266Z" }, + { url = "https://files.pythonhosted.org/packages/c0/cc/6409f9018864a6aa186c61175b977131f373f1988e198e031236916e87e4/yarl-1.23.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:62694e275c93d54f7ccedcfef57d42761b2aad5234b6be1f3e3026cae4001cd4", size = 88786, upload-time = "2026-03-01T22:07:23.129Z" }, + { url = "https://files.pythonhosted.org/packages/76/40/cc22d1d7714b717fde2006fad2ced5efe5580606cb059ae42117542122f3/yarl-1.23.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31de1613658308efdb21ada98cbc86a97c181aa050ba22a808120bb5be3ab94", size = 101307, upload-time = "2026-03-01T22:07:24.689Z" }, + { url = "https://files.pythonhosted.org/packages/8f/0d/476c38e85ddb4c6ec6b20b815bdd779aa386a013f3d8b85516feee55c8dc/yarl-1.23.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb1e8b8d66c278b21d13b0a7ca22c41dd757a7c209c6b12c313e445c31dd3b28", size = 100904, upload-time = "2026-03-01T22:07:26.287Z" }, + { url = "https://files.pythonhosted.org/packages/72/32/0abe4a76d59adf2081dcb0397168553ece4616ada1c54d1c49d8936c74f8/yarl-1.23.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50f9d8d531dfb767c565f348f33dd5139a6c43f5cbdf3f67da40d54241df93f6", size = 97728, upload-time = "2026-03-01T22:07:27.906Z" }, + { url = "https://files.pythonhosted.org/packages/b7/35/7b30f4810fba112f60f5a43237545867504e15b1c7647a785fbaf588fac2/yarl-1.23.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575aa4405a656e61a540f4a80eaa5260f2a38fff7bfdc4b5f611840d76e9e277", size = 95964, upload-time = "2026-03-01T22:07:30.198Z" }, + { url = "https://files.pythonhosted.org/packages/2d/86/ed7a73ab85ef00e8bb70b0cb5421d8a2a625b81a333941a469a6f4022828/yarl-1.23.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:041b1a4cefacf65840b4e295c6985f334ba83c30607441ae3cf206a0eed1a2e4", size = 95882, upload-time = "2026-03-01T22:07:32.132Z" }, + { url = "https://files.pythonhosted.org/packages/19/90/d56967f61a29d8498efb7afb651e0b2b422a1e9b47b0ab5f4e40a19b699b/yarl-1.23.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:d38c1e8231722c4ce40d7593f28d92b5fc72f3e9774fe73d7e800ec32299f63a", size = 90797, upload-time = "2026-03-01T22:07:34.404Z" }, + { url = "https://files.pythonhosted.org/packages/72/00/8b8f76909259f56647adb1011d7ed8b321bcf97e464515c65016a47ecdf0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d53834e23c015ee83a99377db6e5e37d8484f333edb03bd15b4bc312cc7254fb", size = 101023, upload-time = "2026-03-01T22:07:35.953Z" }, + { url = "https://files.pythonhosted.org/packages/ac/e2/cab11b126fb7d440281b7df8e9ddbe4851e70a4dde47a202b6642586b8d9/yarl-1.23.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2e27c8841126e017dd2a054a95771569e6070b9ee1b133366d8b31beb5018a41", size = 96227, upload-time = "2026-03-01T22:07:37.594Z" }, + { url = "https://files.pythonhosted.org/packages/c2/9b/2c893e16bfc50e6b2edf76c1a9eb6cb0c744346197e74c65e99ad8d634d0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:76855800ac56f878847a09ce6dba727c93ca2d89c9e9d63002d26b916810b0a2", size = 100302, upload-time = "2026-03-01T22:07:39.334Z" }, + { url = "https://files.pythonhosted.org/packages/28/ec/5498c4e3a6d5f1003beb23405671c2eb9cdbf3067d1c80f15eeafe301010/yarl-1.23.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e09fd068c2e169a7070d83d3bde728a4d48de0549f975290be3c108c02e499b4", size = 98202, upload-time = "2026-03-01T22:07:41.717Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c3/cd737e2d45e70717907f83e146f6949f20cc23cd4bf7b2688727763aa458/yarl-1.23.0-cp314-cp314t-win32.whl", hash = "sha256:73309162a6a571d4cbd3b6a1dcc703c7311843ae0d1578df6f09be4e98df38d4", size = 90558, upload-time = "2026-03-01T22:07:43.433Z" }, + { url = "https://files.pythonhosted.org/packages/e1/19/3774d162f6732d1cfb0b47b4140a942a35ca82bb19b6db1f80e9e7bdc8f8/yarl-1.23.0-cp314-cp314t-win_amd64.whl", hash = "sha256:4503053d296bc6e4cbd1fad61cf3b6e33b939886c4f249ba7c78b602214fabe2", size = 97610, upload-time = "2026-03-01T22:07:45.773Z" }, + { url = "https://files.pythonhosted.org/packages/51/47/3fa2286c3cb162c71cdb34c4224d5745a1ceceb391b2bd9b19b668a8d724/yarl-1.23.0-cp314-cp314t-win_arm64.whl", hash = "sha256:44bb7bef4ea409384e3f8bc36c063d77ea1b8d4a5b2706956c0d6695f07dcc25", size = 86041, upload-time = "2026-03-01T22:07:49.026Z" }, + { url = "https://files.pythonhosted.org/packages/69/68/c8739671f5699c7dc470580a4f821ef37c32c4cb0b047ce223a7f115757f/yarl-1.23.0-py3-none-any.whl", hash = "sha256:a2df6afe50dea8ae15fa34c9f824a3ee958d785fd5d089063d960bae1daa0a3f", size = 48288, upload-time = "2026-03-01T22:07:51.388Z" }, +] + +[[package]] +name = "zipp" +version = "3.23.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/21/093488dfc7cc8964ded15ab726fad40f25fd3d788fd741cc1c5a17d78ee8/zipp-3.23.1.tar.gz", hash = "sha256:32120e378d32cd9714ad503c1d024619063ec28aad2248dc6672ad13edfa5110", size = 25965, upload-time = "2026-04-13T23:21:46.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/8a/0861bec20485572fbddf3dfba2910e38fe249796cb73ecdeb74e07eeb8d3/zipp-3.23.1-py3-none-any.whl", hash = "sha256:0b3596c50a5c700c9cb40ba8d86d9f2cc4807e9bedb06bcdf7fac85633e444dc", size = 10378, upload-time = "2026-04-13T23:21:45.386Z" }, +] + +[[package]] +name = "zstandard" +version = "0.25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/aa/3e0508d5a5dd96529cdc5a97011299056e14c6505b678fd58938792794b1/zstandard-0.25.0.tar.gz", hash = "sha256:7713e1179d162cf5c7906da876ec2ccb9c3a9dcbdffef0cc7f70c3667a205f0b", size = 711513, upload-time = "2025-09-14T22:15:54.002Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/5c/f8923b595b55fe49e30612987ad8bf053aef555c14f05bb659dd5dbe3e8a/zstandard-0.25.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e29f0cf06974c899b2c188ef7f783607dbef36da4c242eb6c82dcd8b512855e3", size = 795887, upload-time = "2025-09-14T22:17:54.198Z" }, + { url = "https://files.pythonhosted.org/packages/8d/09/d0a2a14fc3439c5f874042dca72a79c70a532090b7ba0003be73fee37ae2/zstandard-0.25.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:05df5136bc5a011f33cd25bc9f506e7426c0c9b3f9954f056831ce68f3b6689f", size = 640658, upload-time = "2025-09-14T22:17:55.423Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7c/8b6b71b1ddd517f68ffb55e10834388d4f793c49c6b83effaaa05785b0b4/zstandard-0.25.0-cp314-cp314-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:f604efd28f239cc21b3adb53eb061e2a205dc164be408e553b41ba2ffe0ca15c", size = 5379849, upload-time = "2025-09-14T22:17:57.372Z" }, + { url = "https://files.pythonhosted.org/packages/a4/86/a48e56320d0a17189ab7a42645387334fba2200e904ee47fc5a26c1fd8ca/zstandard-0.25.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223415140608d0f0da010499eaa8ccdb9af210a543fac54bce15babbcfc78439", size = 5058095, upload-time = "2025-09-14T22:17:59.498Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ad/eb659984ee2c0a779f9d06dbfe45e2dc39d99ff40a319895df2d3d9a48e5/zstandard-0.25.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e54296a283f3ab5a26fc9b8b5d4978ea0532f37b231644f367aa588930aa043", size = 5551751, upload-time = "2025-09-14T22:18:01.618Z" }, + { url = "https://files.pythonhosted.org/packages/61/b3/b637faea43677eb7bd42ab204dfb7053bd5c4582bfe6b1baefa80ac0c47b/zstandard-0.25.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ca54090275939dc8ec5dea2d2afb400e0f83444b2fc24e07df7fdef677110859", size = 6364818, upload-time = "2025-09-14T22:18:03.769Z" }, + { url = "https://files.pythonhosted.org/packages/31/dc/cc50210e11e465c975462439a492516a73300ab8caa8f5e0902544fd748b/zstandard-0.25.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e09bb6252b6476d8d56100e8147b803befa9a12cea144bbe629dd508800d1ad0", size = 5560402, upload-time = "2025-09-14T22:18:05.954Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ae/56523ae9c142f0c08efd5e868a6da613ae76614eca1305259c3bf6a0ed43/zstandard-0.25.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a9ec8c642d1ec73287ae3e726792dd86c96f5681eb8df274a757bf62b750eae7", size = 4955108, upload-time = "2025-09-14T22:18:07.68Z" }, + { url = "https://files.pythonhosted.org/packages/98/cf/c899f2d6df0840d5e384cf4c4121458c72802e8bda19691f3b16619f51e9/zstandard-0.25.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a4089a10e598eae6393756b036e0f419e8c1d60f44a831520f9af41c14216cf2", size = 5269248, upload-time = "2025-09-14T22:18:09.753Z" }, + { url = "https://files.pythonhosted.org/packages/1b/c0/59e912a531d91e1c192d3085fc0f6fb2852753c301a812d856d857ea03c6/zstandard-0.25.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f67e8f1a324a900e75b5e28ffb152bcac9fbed1cc7b43f99cd90f395c4375344", size = 5430330, upload-time = "2025-09-14T22:18:11.966Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/7e31db1240de2df22a58e2ea9a93fc6e38cc29353e660c0272b6735d6669/zstandard-0.25.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9654dbc012d8b06fc3d19cc825af3f7bf8ae242226df5f83936cb39f5fdc846c", size = 5811123, upload-time = "2025-09-14T22:18:13.907Z" }, + { url = "https://files.pythonhosted.org/packages/f6/49/fac46df5ad353d50535e118d6983069df68ca5908d4d65b8c466150a4ff1/zstandard-0.25.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4203ce3b31aec23012d3a4cf4a2ed64d12fea5269c49aed5e4c3611b938e4088", size = 5359591, upload-time = "2025-09-14T22:18:16.465Z" }, + { url = "https://files.pythonhosted.org/packages/c2/38/f249a2050ad1eea0bb364046153942e34abba95dd5520af199aed86fbb49/zstandard-0.25.0-cp314-cp314-win32.whl", hash = "sha256:da469dc041701583e34de852d8634703550348d5822e66a0c827d39b05365b12", size = 444513, upload-time = "2025-09-14T22:18:20.61Z" }, + { url = "https://files.pythonhosted.org/packages/3a/43/241f9615bcf8ba8903b3f0432da069e857fc4fd1783bd26183db53c4804b/zstandard-0.25.0-cp314-cp314-win_amd64.whl", hash = "sha256:c19bcdd826e95671065f8692b5a4aa95c52dc7a02a4c5a0cac46deb879a017a2", size = 516118, upload-time = "2025-09-14T22:18:17.849Z" }, + { url = "https://files.pythonhosted.org/packages/f0/ef/da163ce2450ed4febf6467d77ccb4cd52c4c30ab45624bad26ca0a27260c/zstandard-0.25.0-cp314-cp314-win_arm64.whl", hash = "sha256:d7541afd73985c630bafcd6338d2518ae96060075f9463d7dc14cfb33514383d", size = 476940, upload-time = "2025-09-14T22:18:19.088Z" }, +]