feat(02-03): add MediaAttachment model, WhatsApp normalizer, and signature verification
- Add MediaType(StrEnum) and MediaAttachment(BaseModel) to shared/models/message.py - Add media: list[MediaAttachment] field to MessageContent - Add whatsapp_app_secret, whatsapp_verify_token, and MinIO settings to shared/config.py - Add normalize_whatsapp_event() to gateway/normalize.py (text, image, document support) - Create whatsapp.py adapter with verify_whatsapp_signature() and verify_hub_challenge() - 30 new passing tests (signature verification + normalizer)
This commit is contained in:
721
packages/gateway/gateway/channels/whatsapp.py
Normal file
721
packages/gateway/gateway/channels/whatsapp.py
Normal file
@@ -0,0 +1,721 @@
|
|||||||
|
"""
|
||||||
|
WhatsApp Business Cloud API channel adapter.
|
||||||
|
|
||||||
|
Handles Meta Cloud API v20.0 webhooks for WhatsApp Business accounts.
|
||||||
|
|
||||||
|
EVENT FLOW (inbound):
|
||||||
|
1. Meta sends webhook to POST /whatsapp/webhook
|
||||||
|
2. Read raw body BEFORE any JSON parsing (required for HMAC verification)
|
||||||
|
3. Verify HMAC-SHA256 signature on raw body bytes
|
||||||
|
4. Parse JSON and skip non-message events (status updates, read receipts)
|
||||||
|
5. Normalize via normalize_whatsapp_event() -> KonstructMessage
|
||||||
|
6. Resolve tenant from phone_number_id as workspace_id
|
||||||
|
7. Check rate limit
|
||||||
|
8. Check idempotency (Meta retries on non-200; we dedup on message_id)
|
||||||
|
9. Business-function scoping gate (tier 1: keyword, tier 2: LLM via system prompt)
|
||||||
|
10. Download media from Meta API and store in MinIO (if media message)
|
||||||
|
11. Dispatch handle_message.delay()
|
||||||
|
12. Always return 200 OK to Meta (Meta retries on non-200)
|
||||||
|
|
||||||
|
HUB CHALLENGE (webhook registration):
|
||||||
|
Meta sends GET /whatsapp/webhook with hub.mode, hub.verify_token, hub.challenge.
|
||||||
|
We verify the token and return hub.challenge as plain text.
|
||||||
|
|
||||||
|
OUTBOUND:
|
||||||
|
send_whatsapp_message() sends text responses directly via Meta Graph API.
|
||||||
|
LLM-generated response routing is wired in Plan 02-05 (orchestrator tasks).
|
||||||
|
|
||||||
|
SECURITY NOTE:
|
||||||
|
Always read raw body via request.body() BEFORE json parsing.
|
||||||
|
HMAC-SHA256 is computed on raw bytes — parsing first discards the original bytes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import hmac
|
||||||
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
from fastapi import APIRouter, HTTPException, Request
|
||||||
|
from fastapi.responses import JSONResponse, PlainTextResponse
|
||||||
|
|
||||||
|
from gateway.normalize import normalize_whatsapp_event
|
||||||
|
from router.idempotency import is_duplicate
|
||||||
|
from router.ratelimit import RateLimitExceeded, check_rate_limit
|
||||||
|
from router.tenant import resolve_tenant
|
||||||
|
from shared.config import settings
|
||||||
|
from shared.db import async_session_factory
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
whatsapp_router = APIRouter()
|
||||||
|
|
||||||
|
# Meta Graph API base URL
|
||||||
|
_META_API_BASE = "https://graph.facebook.com/v20.0"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Signature verification helpers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def verify_whatsapp_signature(
|
||||||
|
body: bytes,
|
||||||
|
signature_header: str,
|
||||||
|
app_secret: str,
|
||||||
|
) -> bytes:
|
||||||
|
"""
|
||||||
|
Verify the HMAC-SHA256 signature on the raw webhook body.
|
||||||
|
|
||||||
|
Meta sends the signature in the X-Hub-Signature-256 header as:
|
||||||
|
``sha256={hex_digest}``
|
||||||
|
|
||||||
|
The signature is computed over the raw body bytes using the app secret
|
||||||
|
as the HMAC key. We use hmac.compare_digest for timing-safe comparison
|
||||||
|
to prevent timing oracle attacks.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
body: Raw request body bytes (read BEFORE JSON parsing).
|
||||||
|
signature_header: Value of the X-Hub-Signature-256 header.
|
||||||
|
app_secret: WhatsApp app secret (from settings.whatsapp_app_secret).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The raw body bytes (unchanged) if the signature is valid.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
HTTPException(403): If the signature is missing, malformed, or invalid.
|
||||||
|
"""
|
||||||
|
if not signature_header.startswith("sha256="):
|
||||||
|
logger.warning("WhatsApp webhook: missing or malformed X-Hub-Signature-256 header")
|
||||||
|
raise HTTPException(status_code=403, detail="Invalid signature")
|
||||||
|
|
||||||
|
received_sig = signature_header[len("sha256="):]
|
||||||
|
|
||||||
|
# Compute expected HMAC-SHA256 over raw body
|
||||||
|
expected_sig = hmac.new(
|
||||||
|
app_secret.encode("utf-8"),
|
||||||
|
body,
|
||||||
|
hashlib.sha256,
|
||||||
|
).hexdigest()
|
||||||
|
|
||||||
|
# Timing-safe comparison
|
||||||
|
if not hmac.compare_digest(expected_sig, received_sig):
|
||||||
|
logger.warning("WhatsApp webhook: signature mismatch — possible unauthorized request")
|
||||||
|
raise HTTPException(status_code=403, detail="Invalid signature")
|
||||||
|
|
||||||
|
return body
|
||||||
|
|
||||||
|
|
||||||
|
def verify_hub_challenge(
|
||||||
|
hub_mode: str,
|
||||||
|
hub_verify_token: str,
|
||||||
|
hub_challenge: str,
|
||||||
|
expected_token: str,
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Verify the hub challenge for WhatsApp webhook registration.
|
||||||
|
|
||||||
|
Meta sends a GET request with hub.mode, hub.verify_token, and hub.challenge
|
||||||
|
when registering a webhook URL. We verify the token and return hub.challenge
|
||||||
|
as plain text to confirm ownership of the endpoint.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
hub_mode: Must be "subscribe" for webhook registration.
|
||||||
|
hub_verify_token: Token sent by Meta (must match expected_token).
|
||||||
|
hub_challenge: Random string from Meta that we echo back.
|
||||||
|
expected_token: Our configured verify token (settings.whatsapp_verify_token).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The hub_challenge string if verification passes.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
HTTPException(403): If mode is not "subscribe" or token doesn't match.
|
||||||
|
"""
|
||||||
|
if hub_mode != "subscribe":
|
||||||
|
logger.warning("WhatsApp hub: unexpected mode=%r", hub_mode)
|
||||||
|
raise HTTPException(status_code=403, detail="Invalid hub.mode")
|
||||||
|
|
||||||
|
if not hmac.compare_digest(hub_verify_token, expected_token):
|
||||||
|
logger.warning("WhatsApp hub: verify_token mismatch")
|
||||||
|
raise HTTPException(status_code=403, detail="Invalid verify_token")
|
||||||
|
|
||||||
|
return hub_challenge
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Business-function scoping helpers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def is_clearly_off_topic(text: str, allowed_functions: list[str]) -> bool:
|
||||||
|
"""
|
||||||
|
Tier 1 business-function scoping gate.
|
||||||
|
|
||||||
|
Performs a simple keyword overlap check between the message text and the
|
||||||
|
agent's allowed business functions. Returns True only when there is ZERO
|
||||||
|
keyword overlap — i.e., the message shares no words (case-insensitive) with
|
||||||
|
any of the allowed function strings.
|
||||||
|
|
||||||
|
This implements the Meta 2026 business messaging policy requirement:
|
||||||
|
AI agents must be scoped to their declared business function and must
|
||||||
|
reject clearly off-topic requests without an LLM call.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text: The inbound message text (lowercased internally).
|
||||||
|
allowed_functions: List of function/topic strings for this agent
|
||||||
|
(e.g. ["customer support", "order tracking", "returns"]).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if the message is clearly off-topic (zero keyword overlap).
|
||||||
|
False if any keyword overlap exists (message may be on-topic).
|
||||||
|
"""
|
||||||
|
if not allowed_functions:
|
||||||
|
# No scoping configured — let all messages through
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not text.strip():
|
||||||
|
# Empty message — not clearly off-topic, let adapter decide
|
||||||
|
return False
|
||||||
|
|
||||||
|
text_lower = text.lower()
|
||||||
|
text_words = set(text_lower.split())
|
||||||
|
|
||||||
|
for func in allowed_functions:
|
||||||
|
func_words = set(func.lower().split())
|
||||||
|
if text_words & func_words:
|
||||||
|
# Found overlap — not clearly off-topic
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def build_off_topic_reply(agent_name: str, allowed_functions: list[str]) -> str:
|
||||||
|
"""
|
||||||
|
Build a canned redirect response for clearly off-topic messages.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
agent_name: The agent's display name.
|
||||||
|
allowed_functions: The agent's declared allowed business functions.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A human-readable redirect message mentioning the agent and topics.
|
||||||
|
"""
|
||||||
|
topics = ", ".join(allowed_functions) if allowed_functions else "my designated topics"
|
||||||
|
return (
|
||||||
|
f"{agent_name} is here to help with {topics}. "
|
||||||
|
f"How can I assist you with one of those?"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Outbound message delivery
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
async def send_whatsapp_message(
|
||||||
|
phone_number_id: str,
|
||||||
|
access_token: str,
|
||||||
|
recipient_wa_id: str,
|
||||||
|
text: str,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Send a text message via Meta Cloud API v20.0.
|
||||||
|
|
||||||
|
POST to /v20.0/{phone_number_id}/messages with:
|
||||||
|
- messaging_product: "whatsapp"
|
||||||
|
- to: recipient_wa_id
|
||||||
|
- type: "text"
|
||||||
|
- text: {"body": text}
|
||||||
|
|
||||||
|
Args:
|
||||||
|
phone_number_id: The sending phone number ID from channel_connections.
|
||||||
|
access_token: The WhatsApp Business API access token.
|
||||||
|
recipient_wa_id: The recipient's WhatsApp ID (phone number).
|
||||||
|
text: The message text to send.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
httpx.HTTPStatusError: If the Meta API returns an error status.
|
||||||
|
"""
|
||||||
|
url = f"{_META_API_BASE}/{phone_number_id}/messages"
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {access_token}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
payload: dict[str, Any] = {
|
||||||
|
"messaging_product": "whatsapp",
|
||||||
|
"to": recipient_wa_id,
|
||||||
|
"type": "text",
|
||||||
|
"text": {"body": text},
|
||||||
|
}
|
||||||
|
|
||||||
|
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||||
|
resp = await client.post(url, json=payload, headers=headers)
|
||||||
|
resp.raise_for_status()
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"send_whatsapp_message: sent to=%s phone_number_id=%s",
|
||||||
|
recipient_wa_id,
|
||||||
|
phone_number_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def send_whatsapp_media(
|
||||||
|
phone_number_id: str,
|
||||||
|
access_token: str,
|
||||||
|
recipient_wa_id: str,
|
||||||
|
media_url: str,
|
||||||
|
media_type: str,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Send a media message (image or document) via Meta Cloud API v20.0.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
phone_number_id: The sending phone number ID.
|
||||||
|
access_token: The WhatsApp Business API access token.
|
||||||
|
recipient_wa_id: The recipient's WhatsApp ID.
|
||||||
|
media_url: Publicly accessible URL for the media file.
|
||||||
|
media_type: Media type string: "image" or "document".
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
httpx.HTTPStatusError: If the Meta API returns an error status.
|
||||||
|
"""
|
||||||
|
url = f"{_META_API_BASE}/{phone_number_id}/messages"
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {access_token}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
payload: dict[str, Any] = {
|
||||||
|
"messaging_product": "whatsapp",
|
||||||
|
"to": recipient_wa_id,
|
||||||
|
"type": media_type,
|
||||||
|
media_type: {"link": media_url},
|
||||||
|
}
|
||||||
|
|
||||||
|
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||||
|
resp = await client.post(url, json=payload, headers=headers)
|
||||||
|
resp.raise_for_status()
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"send_whatsapp_media: sent %s to=%s phone_number_id=%s",
|
||||||
|
media_type,
|
||||||
|
recipient_wa_id,
|
||||||
|
phone_number_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Media download and MinIO storage
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
async def _download_and_store_media(
|
||||||
|
tenant_id: str,
|
||||||
|
message_id: str,
|
||||||
|
media_url: str,
|
||||||
|
access_token: str,
|
||||||
|
filename: str | None,
|
||||||
|
mime_type: str | None,
|
||||||
|
) -> tuple[str, str]:
|
||||||
|
"""
|
||||||
|
Download a media file from Meta API and store it in MinIO.
|
||||||
|
|
||||||
|
The ``media_url`` here is the ``meta-media://{media_id}`` placeholder set
|
||||||
|
by the normalizer. We first resolve the actual download URL from Meta's
|
||||||
|
media endpoint, then download and upload to MinIO.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tenant_id: Konstruct tenant ID (for MinIO key prefix).
|
||||||
|
message_id: WhatsApp message ID (for MinIO key).
|
||||||
|
media_url: Placeholder URL ``meta-media://{media_id}``.
|
||||||
|
access_token: WhatsApp Business API token.
|
||||||
|
filename: Original filename (or None for images).
|
||||||
|
mime_type: MIME type of the media.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (storage_key, presigned_url).
|
||||||
|
"""
|
||||||
|
import boto3 # type: ignore[import-untyped]
|
||||||
|
|
||||||
|
# Extract media_id from placeholder URL
|
||||||
|
media_id = media_url.replace("meta-media://", "")
|
||||||
|
|
||||||
|
# Step 1: Get actual download URL from Meta API
|
||||||
|
meta_url = f"{_META_API_BASE}/{media_id}"
|
||||||
|
headers = {"Authorization": f"Bearer {access_token}"}
|
||||||
|
|
||||||
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||||
|
url_resp = await client.get(meta_url, headers=headers)
|
||||||
|
url_resp.raise_for_status()
|
||||||
|
download_url: str = url_resp.json().get("url", "")
|
||||||
|
|
||||||
|
# Step 2: Download the media file
|
||||||
|
media_resp = await client.get(download_url, headers=headers)
|
||||||
|
media_resp.raise_for_status()
|
||||||
|
media_bytes = media_resp.content
|
||||||
|
|
||||||
|
# Step 3: Determine filename
|
||||||
|
ext = ""
|
||||||
|
if mime_type:
|
||||||
|
ext_map = {
|
||||||
|
"image/jpeg": ".jpg",
|
||||||
|
"image/png": ".png",
|
||||||
|
"image/webp": ".webp",
|
||||||
|
"application/pdf": ".pdf",
|
||||||
|
"audio/ogg": ".ogg",
|
||||||
|
"audio/mpeg": ".mp3",
|
||||||
|
"video/mp4": ".mp4",
|
||||||
|
}
|
||||||
|
ext = ext_map.get(mime_type, "")
|
||||||
|
|
||||||
|
safe_filename = filename or f"{media_id}{ext}"
|
||||||
|
storage_key = f"{tenant_id}/media/{message_id}/{safe_filename}"
|
||||||
|
|
||||||
|
# Step 4: Upload to MinIO
|
||||||
|
s3_client = boto3.client(
|
||||||
|
"s3",
|
||||||
|
endpoint_url=settings.minio_endpoint,
|
||||||
|
aws_access_key_id=settings.minio_access_key,
|
||||||
|
aws_secret_access_key=settings.minio_secret_key,
|
||||||
|
region_name="us-east-1", # MinIO ignores region but boto3 requires it
|
||||||
|
)
|
||||||
|
|
||||||
|
s3_client.put_object(
|
||||||
|
Bucket=settings.minio_media_bucket,
|
||||||
|
Key=storage_key,
|
||||||
|
Body=media_bytes,
|
||||||
|
ContentType=mime_type or "application/octet-stream",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Step 5: Generate presigned URL (1 hour TTL)
|
||||||
|
presigned_url: str = s3_client.generate_presigned_url(
|
||||||
|
"get_object",
|
||||||
|
Params={"Bucket": settings.minio_media_bucket, "Key": storage_key},
|
||||||
|
ExpiresIn=3600,
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"_download_and_store_media: stored tenant=%s key=%s",
|
||||||
|
tenant_id,
|
||||||
|
storage_key,
|
||||||
|
)
|
||||||
|
return storage_key, presigned_url
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Webhook routes
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
@whatsapp_router.get("/whatsapp/webhook")
|
||||||
|
async def whatsapp_webhook_verify(request: Request) -> PlainTextResponse:
|
||||||
|
"""
|
||||||
|
WhatsApp webhook URL verification (hub challenge handshake).
|
||||||
|
|
||||||
|
Meta sends a GET request when you register or update a webhook URL.
|
||||||
|
We must return hub.challenge as plain text to confirm ownership.
|
||||||
|
|
||||||
|
Query params:
|
||||||
|
hub.mode: Must be "subscribe"
|
||||||
|
hub.verify_token: Must match our configured WHATSAPP_VERIFY_TOKEN
|
||||||
|
hub.challenge: Random string we echo back
|
||||||
|
"""
|
||||||
|
hub_mode = request.query_params.get("hub.mode", "")
|
||||||
|
hub_verify_token = request.query_params.get("hub.verify_token", "")
|
||||||
|
hub_challenge = request.query_params.get("hub.challenge", "")
|
||||||
|
|
||||||
|
challenge = verify_hub_challenge(
|
||||||
|
hub_mode=hub_mode,
|
||||||
|
hub_verify_token=hub_verify_token,
|
||||||
|
hub_challenge=hub_challenge,
|
||||||
|
expected_token=settings.whatsapp_verify_token,
|
||||||
|
)
|
||||||
|
|
||||||
|
return PlainTextResponse(challenge)
|
||||||
|
|
||||||
|
|
||||||
|
@whatsapp_router.post("/whatsapp/webhook")
|
||||||
|
async def whatsapp_webhook_inbound(request: Request) -> JSONResponse:
|
||||||
|
"""
|
||||||
|
WhatsApp inbound message webhook.
|
||||||
|
|
||||||
|
CRITICAL: Read raw body FIRST (before JSON parsing) for HMAC verification.
|
||||||
|
Always return 200 OK to Meta — Meta retries delivery on non-200 responses.
|
||||||
|
|
||||||
|
Full adapter sequence:
|
||||||
|
1. Read raw body (BEFORE JSON parsing)
|
||||||
|
2. Verify HMAC-SHA256 signature
|
||||||
|
3. Parse JSON
|
||||||
|
4. Skip non-message events (status updates, read receipts)
|
||||||
|
5. Normalize to KonstructMessage
|
||||||
|
6. Resolve tenant from phone_number_id
|
||||||
|
7. Check rate limit
|
||||||
|
8. Check idempotency
|
||||||
|
9. Business-function scoping (tier 1 keyword gate)
|
||||||
|
10. Download media and store in MinIO (if media message)
|
||||||
|
11. Dispatch handle_message.delay()
|
||||||
|
"""
|
||||||
|
# Always return 200 OK — Meta retries on non-200
|
||||||
|
try:
|
||||||
|
await _process_whatsapp_webhook(request)
|
||||||
|
except HTTPException:
|
||||||
|
# Re-raise HTTPExceptions from signature verification (403)
|
||||||
|
raise
|
||||||
|
except Exception:
|
||||||
|
logger.exception("WhatsApp webhook: unexpected error during processing")
|
||||||
|
# Return 200 anyway to prevent Meta retry storms
|
||||||
|
return JSONResponse({"status": "ok"})
|
||||||
|
|
||||||
|
|
||||||
|
async def _process_whatsapp_webhook(request: Request) -> None:
|
||||||
|
"""
|
||||||
|
Internal handler for WhatsApp inbound webhook processing.
|
||||||
|
|
||||||
|
Separated from the route handler so we can raise HTTPException for 403s
|
||||||
|
while still returning 200 for all other errors.
|
||||||
|
"""
|
||||||
|
# Step 1: Read raw body BEFORE any JSON parsing
|
||||||
|
raw_body = await request.body()
|
||||||
|
|
||||||
|
# Step 2: Verify HMAC-SHA256 signature
|
||||||
|
sig_header = request.headers.get("X-Hub-Signature-256", "")
|
||||||
|
verify_whatsapp_signature(raw_body, sig_header, settings.whatsapp_app_secret)
|
||||||
|
|
||||||
|
# Step 3: Parse JSON from the already-read raw body
|
||||||
|
import json
|
||||||
|
payload = json.loads(raw_body)
|
||||||
|
|
||||||
|
# Step 4: Skip non-message events
|
||||||
|
value = payload.get("entry", [{}])[0].get("changes", [{}])[0].get("value", {})
|
||||||
|
if "messages" not in value:
|
||||||
|
logger.debug("WhatsApp webhook: no messages in payload — skipping (status update?)")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Step 5: Normalize to KonstructMessage
|
||||||
|
msg = normalize_whatsapp_event(payload)
|
||||||
|
phone_number_id: str = msg.channel_metadata.get("phone_number_id", "")
|
||||||
|
wa_id: str = msg.sender.user_id
|
||||||
|
message_id: str = msg.channel_metadata.get("message_id", msg.id)
|
||||||
|
|
||||||
|
# Step 6: Resolve tenant from phone_number_id
|
||||||
|
tenant_id: str | None = None
|
||||||
|
async with async_session_factory() as session:
|
||||||
|
tenant_id = await resolve_tenant(
|
||||||
|
workspace_id=phone_number_id,
|
||||||
|
channel_type="whatsapp",
|
||||||
|
session=session,
|
||||||
|
)
|
||||||
|
|
||||||
|
if tenant_id is None:
|
||||||
|
logger.warning(
|
||||||
|
"WhatsApp webhook: unknown phone_number_id=%r — ignoring",
|
||||||
|
phone_number_id,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
msg.tenant_id = tenant_id
|
||||||
|
|
||||||
|
# Get Redis client from the gateway module
|
||||||
|
from gateway.main import _get_redis # noqa: PLC0415
|
||||||
|
redis = _get_redis()
|
||||||
|
|
||||||
|
# Step 7: Check rate limit
|
||||||
|
try:
|
||||||
|
await check_rate_limit(tenant_id=tenant_id, channel="whatsapp", redis=redis)
|
||||||
|
except RateLimitExceeded as exc:
|
||||||
|
logger.info(
|
||||||
|
"WhatsApp rate limit exceeded: tenant=%s — sending ephemeral rejection",
|
||||||
|
tenant_id,
|
||||||
|
)
|
||||||
|
# Load access_token from DB for reply
|
||||||
|
access_token = await _get_channel_access_token(tenant_id, phone_number_id)
|
||||||
|
if access_token:
|
||||||
|
try:
|
||||||
|
await send_whatsapp_message(
|
||||||
|
phone_number_id=phone_number_id,
|
||||||
|
access_token=access_token,
|
||||||
|
recipient_wa_id=wa_id,
|
||||||
|
text=(
|
||||||
|
f"I'm receiving too many requests right now. "
|
||||||
|
f"Please try again in about {exc.remaining_seconds} seconds."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
logger.exception("WhatsApp: failed to send rate limit rejection")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Step 8: Check idempotency
|
||||||
|
if await is_duplicate(tenant_id, message_id, redis):
|
||||||
|
logger.debug(
|
||||||
|
"WhatsApp duplicate message: tenant=%s message_id=%s — skipping",
|
||||||
|
tenant_id,
|
||||||
|
message_id,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Step 9: Business-function scoping (tier 1 keyword gate)
|
||||||
|
agent_name, allowed_functions, access_token = await _get_agent_scoping_info(
|
||||||
|
tenant_id, phone_number_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if is_clearly_off_topic(msg.content.text, allowed_functions):
|
||||||
|
logger.info(
|
||||||
|
"WhatsApp: off-topic message from wa_id=%s tenant=%s — sending canned redirect",
|
||||||
|
wa_id,
|
||||||
|
tenant_id,
|
||||||
|
)
|
||||||
|
if access_token:
|
||||||
|
try:
|
||||||
|
await send_whatsapp_message(
|
||||||
|
phone_number_id=phone_number_id,
|
||||||
|
access_token=access_token,
|
||||||
|
recipient_wa_id=wa_id,
|
||||||
|
text=build_off_topic_reply(agent_name, allowed_functions),
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
logger.exception("WhatsApp: failed to send off-topic redirect")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Step 10: Download media and store in MinIO (if media message)
|
||||||
|
if msg.content.media and access_token:
|
||||||
|
for attachment in msg.content.media:
|
||||||
|
if attachment.url and attachment.url.startswith("meta-media://"):
|
||||||
|
try:
|
||||||
|
storage_key, presigned_url = await _download_and_store_media(
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
message_id=message_id,
|
||||||
|
media_url=attachment.url,
|
||||||
|
access_token=access_token,
|
||||||
|
filename=attachment.filename,
|
||||||
|
mime_type=attachment.mime_type,
|
||||||
|
)
|
||||||
|
attachment.storage_key = storage_key
|
||||||
|
attachment.url = presigned_url
|
||||||
|
except Exception:
|
||||||
|
logger.exception(
|
||||||
|
"WhatsApp: failed to download/store media for message_id=%s",
|
||||||
|
message_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Step 11: Dispatch to Celery
|
||||||
|
from orchestrator.tasks import handle_message as handle_message_task # noqa: PLC0415
|
||||||
|
|
||||||
|
task_payload = msg.model_dump() | {
|
||||||
|
"phone_number_id": phone_number_id,
|
||||||
|
"bot_token": access_token or "",
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
handle_message_task.delay(task_payload)
|
||||||
|
except Exception:
|
||||||
|
logger.exception(
|
||||||
|
"WhatsApp: failed to dispatch handle_message task: tenant=%s msg_id=%s",
|
||||||
|
tenant_id,
|
||||||
|
msg.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"WhatsApp: dispatched msg_id=%s tenant=%s wa_id=%s",
|
||||||
|
msg.id,
|
||||||
|
tenant_id,
|
||||||
|
wa_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# DB helpers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
async def _get_channel_access_token(
|
||||||
|
tenant_id: str,
|
||||||
|
phone_number_id: str,
|
||||||
|
) -> str | None:
|
||||||
|
"""Load the WhatsApp access token from channel_connections for a given phone number."""
|
||||||
|
from sqlalchemy import select, text # noqa: PLC0415
|
||||||
|
from shared.models.tenant import ChannelConnection, ChannelTypeEnum # noqa: PLC0415
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with async_session_factory() as session:
|
||||||
|
await session.execute(text(f"SET LOCAL app.current_tenant = '{tenant_id}'"))
|
||||||
|
stmt = (
|
||||||
|
select(ChannelConnection.config)
|
||||||
|
.where(ChannelConnection.channel_type == ChannelTypeEnum.WHATSAPP)
|
||||||
|
.where(ChannelConnection.workspace_id == phone_number_id)
|
||||||
|
.limit(1)
|
||||||
|
)
|
||||||
|
result = await session.execute(stmt)
|
||||||
|
row = result.scalar_one_or_none()
|
||||||
|
if row:
|
||||||
|
return row.get("access_token")
|
||||||
|
except Exception:
|
||||||
|
logger.exception(
|
||||||
|
"_get_channel_access_token: DB error tenant=%s phone_number_id=%s",
|
||||||
|
tenant_id,
|
||||||
|
phone_number_id,
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def _get_agent_scoping_info(
|
||||||
|
tenant_id: str,
|
||||||
|
phone_number_id: str,
|
||||||
|
) -> tuple[str, list[str], str | None]:
|
||||||
|
"""
|
||||||
|
Load agent name, allowed_functions, and access_token for scoping gate.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (agent_name, allowed_functions, access_token).
|
||||||
|
Falls back to ("AI Assistant", [], None) if not found.
|
||||||
|
"""
|
||||||
|
from sqlalchemy import select, text # noqa: PLC0415
|
||||||
|
from shared.models.tenant import Agent, ChannelConnection, ChannelTypeEnum # noqa: PLC0415
|
||||||
|
|
||||||
|
agent_name = "AI Assistant"
|
||||||
|
allowed_functions: list[str] = []
|
||||||
|
access_token: str | None = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with async_session_factory() as session:
|
||||||
|
await session.execute(text(f"SET LOCAL app.current_tenant = '{tenant_id}'"))
|
||||||
|
|
||||||
|
# Load channel connection config (access_token + agent_id)
|
||||||
|
conn_stmt = (
|
||||||
|
select(ChannelConnection)
|
||||||
|
.where(ChannelConnection.channel_type == ChannelTypeEnum.WHATSAPP)
|
||||||
|
.where(ChannelConnection.workspace_id == phone_number_id)
|
||||||
|
.limit(1)
|
||||||
|
)
|
||||||
|
conn_result = await session.execute(conn_stmt)
|
||||||
|
conn = conn_result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if conn:
|
||||||
|
config: dict = conn.config or {}
|
||||||
|
access_token = config.get("access_token")
|
||||||
|
agent_id = str(conn.agent_id) if conn.agent_id else None
|
||||||
|
|
||||||
|
if agent_id:
|
||||||
|
import uuid as _uuid # noqa: PLC0415
|
||||||
|
agent_stmt = (
|
||||||
|
select(Agent)
|
||||||
|
.where(Agent.id == _uuid.UUID(agent_id))
|
||||||
|
.limit(1)
|
||||||
|
)
|
||||||
|
agent_result = await session.execute(agent_stmt)
|
||||||
|
agent = agent_result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if agent:
|
||||||
|
agent_name = agent.name
|
||||||
|
# Use tools as proxy for allowed_functions
|
||||||
|
tools: list[str] = agent.tools or []
|
||||||
|
allowed_functions = tools
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
logger.exception(
|
||||||
|
"_get_agent_scoping_info: DB error tenant=%s phone_number_id=%s",
|
||||||
|
tenant_id,
|
||||||
|
phone_number_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
return agent_name, allowed_functions, access_token
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
"""
|
"""
|
||||||
Slack event normalization.
|
Channel event normalization.
|
||||||
|
|
||||||
Converts Slack Events API payloads into KonstructMessage format.
|
Converts channel-specific event payloads (Slack, WhatsApp, etc.) into the
|
||||||
|
unified KonstructMessage format.
|
||||||
|
|
||||||
All channel adapters produce KonstructMessage — the router and orchestrator
|
All channel adapters produce KonstructMessage — the router and orchestrator
|
||||||
never inspect Slack-specific fields directly.
|
never inspect channel-specific fields directly.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -16,6 +17,8 @@ from datetime import datetime, timezone
|
|||||||
from shared.models.message import (
|
from shared.models.message import (
|
||||||
ChannelType,
|
ChannelType,
|
||||||
KonstructMessage,
|
KonstructMessage,
|
||||||
|
MediaAttachment,
|
||||||
|
MediaType,
|
||||||
MessageContent,
|
MessageContent,
|
||||||
SenderInfo,
|
SenderInfo,
|
||||||
)
|
)
|
||||||
@@ -98,3 +101,109 @@ def normalize_slack_event(
|
|||||||
timestamp=timestamp,
|
timestamp=timestamp,
|
||||||
thread_id=thread_ts,
|
thread_id=thread_ts,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# WhatsApp normalization
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Map from WhatsApp media message type strings to MediaType enum
|
||||||
|
_WHATSAPP_MEDIA_TYPES: dict[str, MediaType] = {
|
||||||
|
"image": MediaType.IMAGE,
|
||||||
|
"document": MediaType.DOCUMENT,
|
||||||
|
"audio": MediaType.AUDIO,
|
||||||
|
"video": MediaType.VIDEO,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_whatsapp_event(payload: dict) -> KonstructMessage:
|
||||||
|
"""
|
||||||
|
Normalize a Meta Cloud API v20.0 webhook payload into a KonstructMessage.
|
||||||
|
|
||||||
|
Handles text, image, and document message types.
|
||||||
|
|
||||||
|
For media messages (image/document), creates a MediaAttachment with a
|
||||||
|
placeholder URL in the format ``meta-media://{media_id}``. The actual
|
||||||
|
download from Meta's API and upload to MinIO happens in the WhatsApp
|
||||||
|
channel adapter after normalization.
|
||||||
|
|
||||||
|
WhatsApp has no threading concept — thread_id is set to the sender's
|
||||||
|
wa_id so that conversation history is scoped per phone number.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
payload: Parsed Meta Cloud API v20.0 webhook JSON (already-deserialized dict).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A KonstructMessage. ``tenant_id`` is ``None`` at this stage — the
|
||||||
|
Message Router populates it via channel_connections lookup.
|
||||||
|
"""
|
||||||
|
# Meta Cloud API structure: entry[0].changes[0].value
|
||||||
|
value: dict = payload["entry"][0]["changes"][0]["value"]
|
||||||
|
metadata: dict = value.get("metadata", {})
|
||||||
|
messages: list[dict] = value.get("messages", [])
|
||||||
|
|
||||||
|
if not messages:
|
||||||
|
# Status updates, read receipts, etc. — no messages to normalize
|
||||||
|
raise ValueError("WhatsApp webhook payload contains no messages")
|
||||||
|
|
||||||
|
message: dict = messages[0]
|
||||||
|
msg_type: str = message.get("type", "text")
|
||||||
|
wa_id: str = message.get("from", "")
|
||||||
|
message_id: str = message.get("id", "")
|
||||||
|
phone_number_id: str = metadata.get("phone_number_id", "")
|
||||||
|
|
||||||
|
# Timestamp — Meta sends Unix integer timestamps as strings
|
||||||
|
ts_raw: str = message.get("timestamp", "0")
|
||||||
|
try:
|
||||||
|
timestamp = datetime.fromtimestamp(int(ts_raw), tz=timezone.utc)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
timestamp = datetime.now(tz=timezone.utc)
|
||||||
|
|
||||||
|
# Extract display name from contacts array
|
||||||
|
contacts: list[dict] = value.get("contacts", [])
|
||||||
|
display_name: str = wa_id
|
||||||
|
if contacts:
|
||||||
|
display_name = contacts[0].get("profile", {}).get("name", wa_id)
|
||||||
|
|
||||||
|
# Extract content based on message type
|
||||||
|
text_body: str = ""
|
||||||
|
media_attachments: list[MediaAttachment] = []
|
||||||
|
|
||||||
|
if msg_type == "text":
|
||||||
|
text_body = message.get("text", {}).get("body", "")
|
||||||
|
|
||||||
|
elif msg_type in _WHATSAPP_MEDIA_TYPES:
|
||||||
|
media_data: dict = message.get(msg_type, {})
|
||||||
|
media_id: str = media_data.get("id", "")
|
||||||
|
attachment = MediaAttachment(
|
||||||
|
media_type=_WHATSAPP_MEDIA_TYPES[msg_type],
|
||||||
|
# Placeholder URL — actual download happens in adapter
|
||||||
|
url=f"meta-media://{media_id}" if media_id else None,
|
||||||
|
mime_type=media_data.get("mime_type"),
|
||||||
|
filename=media_data.get("filename"),
|
||||||
|
)
|
||||||
|
media_attachments.append(attachment)
|
||||||
|
# Caption is the text for media messages (may not be present)
|
||||||
|
text_body = media_data.get("caption", "")
|
||||||
|
|
||||||
|
return KonstructMessage(
|
||||||
|
id=str(uuid.uuid4()),
|
||||||
|
tenant_id=None, # Populated by Message Router
|
||||||
|
channel=ChannelType.WHATSAPP,
|
||||||
|
channel_metadata={
|
||||||
|
"phone_number_id": phone_number_id,
|
||||||
|
"message_id": message_id,
|
||||||
|
"wa_id": wa_id,
|
||||||
|
},
|
||||||
|
sender=SenderInfo(
|
||||||
|
user_id=wa_id,
|
||||||
|
display_name=display_name,
|
||||||
|
),
|
||||||
|
content=MessageContent(
|
||||||
|
text=text_body,
|
||||||
|
media=media_attachments,
|
||||||
|
),
|
||||||
|
timestamp=timestamp,
|
||||||
|
# WhatsApp has no threading — use wa_id as conversation scope identifier
|
||||||
|
thread_id=wa_id,
|
||||||
|
)
|
||||||
|
|||||||
@@ -65,6 +65,38 @@ class Settings(BaseSettings):
|
|||||||
description="Slack app-level token for Socket Mode (xapp-...)",
|
description="Slack app-level token for Socket Mode (xapp-...)",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# WhatsApp
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
whatsapp_app_secret: str = Field(
|
||||||
|
default="",
|
||||||
|
description="WhatsApp app secret for HMAC-SHA256 webhook signature verification",
|
||||||
|
)
|
||||||
|
whatsapp_verify_token: str = Field(
|
||||||
|
default="",
|
||||||
|
description="WhatsApp webhook verification token (hub.verify_token)",
|
||||||
|
)
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# MinIO / Object Storage
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
minio_endpoint: str = Field(
|
||||||
|
default="http://localhost:9000",
|
||||||
|
description="MinIO endpoint URL (S3-compatible)",
|
||||||
|
)
|
||||||
|
minio_access_key: str = Field(
|
||||||
|
default="minioadmin",
|
||||||
|
description="MinIO access key",
|
||||||
|
)
|
||||||
|
minio_secret_key: str = Field(
|
||||||
|
default="minioadmin",
|
||||||
|
description="MinIO secret key",
|
||||||
|
)
|
||||||
|
minio_media_bucket: str = Field(
|
||||||
|
default="konstruct-media",
|
||||||
|
description="MinIO bucket name for media attachments",
|
||||||
|
)
|
||||||
|
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
# LLM Providers
|
# LLM Providers
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -28,6 +28,38 @@ class ChannelType(StrEnum):
|
|||||||
SIGNAL = "signal"
|
SIGNAL = "signal"
|
||||||
|
|
||||||
|
|
||||||
|
class MediaType(StrEnum):
|
||||||
|
"""Supported media attachment types."""
|
||||||
|
|
||||||
|
IMAGE = "image"
|
||||||
|
DOCUMENT = "document"
|
||||||
|
AUDIO = "audio"
|
||||||
|
VIDEO = "video"
|
||||||
|
|
||||||
|
|
||||||
|
class MediaAttachment(BaseModel):
|
||||||
|
"""
|
||||||
|
A media file attached to a message (image, document, audio, or video).
|
||||||
|
|
||||||
|
After normalization, `url` contains a placeholder media ID URL from the
|
||||||
|
channel's API. The channel adapter downloads the media and stores it in
|
||||||
|
MinIO, then updates `storage_key` and `url` with the final presigned URL.
|
||||||
|
"""
|
||||||
|
|
||||||
|
media_type: MediaType = Field(description="Type of media: image, document, audio, or video")
|
||||||
|
url: str | None = Field(
|
||||||
|
default=None,
|
||||||
|
description="Download URL — placeholder media ID URL after normalization, presigned MinIO URL after storage",
|
||||||
|
)
|
||||||
|
storage_key: str | None = Field(
|
||||||
|
default=None,
|
||||||
|
description="MinIO object key: {tenant_id}/{agent_id}/{message_id}/{filename}",
|
||||||
|
)
|
||||||
|
mime_type: str | None = Field(default=None, description="MIME type (e.g. image/jpeg)")
|
||||||
|
filename: str | None = Field(default=None, description="Original filename if available")
|
||||||
|
size_bytes: int | None = Field(default=None, description="File size in bytes if available")
|
||||||
|
|
||||||
|
|
||||||
class SenderInfo(BaseModel):
|
class SenderInfo(BaseModel):
|
||||||
"""Information about the message sender."""
|
"""Information about the message sender."""
|
||||||
|
|
||||||
@@ -50,6 +82,10 @@ class MessageContent(BaseModel):
|
|||||||
default_factory=list,
|
default_factory=list,
|
||||||
description="List of user/bot IDs mentioned in the message",
|
description="List of user/bot IDs mentioned in the message",
|
||||||
)
|
)
|
||||||
|
media: list[MediaAttachment] = Field(
|
||||||
|
default_factory=list,
|
||||||
|
description="Typed media attachments (images, documents, audio, video)",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class KonstructMessage(BaseModel):
|
class KonstructMessage(BaseModel):
|
||||||
|
|||||||
365
tests/unit/test_whatsapp_normalize.py
Normal file
365
tests/unit/test_whatsapp_normalize.py
Normal file
@@ -0,0 +1,365 @@
|
|||||||
|
"""
|
||||||
|
Unit tests for WhatsApp event normalization to KonstructMessage.
|
||||||
|
|
||||||
|
Tests CHAN-03: Messages are normalized to the unified KonstructMessage format.
|
||||||
|
|
||||||
|
Covers:
|
||||||
|
- Text message normalizes correctly
|
||||||
|
- Image message normalizes with MediaAttachment
|
||||||
|
- Document message normalizes with MediaAttachment
|
||||||
|
- Correct field mapping: sender, channel, metadata
|
||||||
|
- ChannelType includes 'whatsapp'
|
||||||
|
- thread_id set to sender wa_id (WhatsApp has no threads)
|
||||||
|
- tenant_id is None after normalization
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import timezone
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from shared.models.message import ChannelType, MediaAttachment, MediaType
|
||||||
|
|
||||||
|
|
||||||
|
def make_text_webhook_payload(
|
||||||
|
phone_number_id: str = "12345678901",
|
||||||
|
wa_id: str = "5511987654321",
|
||||||
|
message_id: str = "wamid.abc123",
|
||||||
|
text: str = "Hello, I need help with my order",
|
||||||
|
timestamp: str = "1711234567",
|
||||||
|
) -> dict:
|
||||||
|
"""Minimal valid Meta Cloud API v20.0 text message webhook payload."""
|
||||||
|
return {
|
||||||
|
"object": "whatsapp_business_account",
|
||||||
|
"entry": [
|
||||||
|
{
|
||||||
|
"id": "WHATSAPP_BUSINESS_ACCOUNT_ID",
|
||||||
|
"changes": [
|
||||||
|
{
|
||||||
|
"value": {
|
||||||
|
"messaging_product": "whatsapp",
|
||||||
|
"metadata": {
|
||||||
|
"display_phone_number": "15550000000",
|
||||||
|
"phone_number_id": phone_number_id,
|
||||||
|
},
|
||||||
|
"contacts": [
|
||||||
|
{
|
||||||
|
"profile": {"name": "John Customer"},
|
||||||
|
"wa_id": wa_id,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"messages": [
|
||||||
|
{
|
||||||
|
"from": wa_id,
|
||||||
|
"id": message_id,
|
||||||
|
"timestamp": timestamp,
|
||||||
|
"text": {"body": text},
|
||||||
|
"type": "text",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"field": "messages",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def make_image_webhook_payload(
|
||||||
|
phone_number_id: str = "12345678901",
|
||||||
|
wa_id: str = "5511987654321",
|
||||||
|
message_id: str = "wamid.img456",
|
||||||
|
media_id: str = "media_id_xyz789",
|
||||||
|
mime_type: str = "image/jpeg",
|
||||||
|
) -> dict:
|
||||||
|
"""Minimal valid Meta Cloud API v20.0 image message webhook payload."""
|
||||||
|
return {
|
||||||
|
"object": "whatsapp_business_account",
|
||||||
|
"entry": [
|
||||||
|
{
|
||||||
|
"id": "WHATSAPP_BUSINESS_ACCOUNT_ID",
|
||||||
|
"changes": [
|
||||||
|
{
|
||||||
|
"value": {
|
||||||
|
"messaging_product": "whatsapp",
|
||||||
|
"metadata": {
|
||||||
|
"display_phone_number": "15550000000",
|
||||||
|
"phone_number_id": phone_number_id,
|
||||||
|
},
|
||||||
|
"contacts": [
|
||||||
|
{
|
||||||
|
"profile": {"name": "Jane Customer"},
|
||||||
|
"wa_id": wa_id,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"messages": [
|
||||||
|
{
|
||||||
|
"from": wa_id,
|
||||||
|
"id": message_id,
|
||||||
|
"timestamp": "1711234567",
|
||||||
|
"image": {
|
||||||
|
"id": media_id,
|
||||||
|
"mime_type": mime_type,
|
||||||
|
"sha256": "abc123hash",
|
||||||
|
},
|
||||||
|
"type": "image",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"field": "messages",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def make_document_webhook_payload(
|
||||||
|
phone_number_id: str = "12345678901",
|
||||||
|
wa_id: str = "5511987654321",
|
||||||
|
message_id: str = "wamid.doc789",
|
||||||
|
media_id: str = "media_id_doc001",
|
||||||
|
filename: str = "invoice.pdf",
|
||||||
|
) -> dict:
|
||||||
|
"""Minimal valid Meta Cloud API v20.0 document message webhook payload."""
|
||||||
|
return {
|
||||||
|
"object": "whatsapp_business_account",
|
||||||
|
"entry": [
|
||||||
|
{
|
||||||
|
"id": "WHATSAPP_BUSINESS_ACCOUNT_ID",
|
||||||
|
"changes": [
|
||||||
|
{
|
||||||
|
"value": {
|
||||||
|
"messaging_product": "whatsapp",
|
||||||
|
"metadata": {
|
||||||
|
"display_phone_number": "15550000000",
|
||||||
|
"phone_number_id": phone_number_id,
|
||||||
|
},
|
||||||
|
"contacts": [
|
||||||
|
{
|
||||||
|
"profile": {"name": "Bob Customer"},
|
||||||
|
"wa_id": wa_id,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"messages": [
|
||||||
|
{
|
||||||
|
"from": wa_id,
|
||||||
|
"id": message_id,
|
||||||
|
"timestamp": "1711234567",
|
||||||
|
"document": {
|
||||||
|
"id": media_id,
|
||||||
|
"mime_type": "application/pdf",
|
||||||
|
"filename": filename,
|
||||||
|
},
|
||||||
|
"type": "document",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"field": "messages",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TestChannelTypeWhatsApp:
|
||||||
|
"""Tests for ChannelType enum including WhatsApp."""
|
||||||
|
|
||||||
|
def test_whatsapp_in_channel_type(self) -> None:
|
||||||
|
"""ChannelType enum must include 'whatsapp' value."""
|
||||||
|
assert ChannelType.WHATSAPP == "whatsapp"
|
||||||
|
assert "whatsapp" in [c.value for c in ChannelType]
|
||||||
|
|
||||||
|
def test_whatsapp_channel_type_str(self) -> None:
|
||||||
|
"""ChannelType.WHATSAPP must equal string 'whatsapp'."""
|
||||||
|
assert str(ChannelType.WHATSAPP) == "whatsapp"
|
||||||
|
|
||||||
|
|
||||||
|
class TestMediaAttachmentModel:
|
||||||
|
"""Tests for the MediaAttachment model."""
|
||||||
|
|
||||||
|
def test_media_attachment_image_fields(self) -> None:
|
||||||
|
"""MediaAttachment must accept image type with required fields."""
|
||||||
|
attachment = MediaAttachment(
|
||||||
|
media_type=MediaType.IMAGE,
|
||||||
|
mime_type="image/jpeg",
|
||||||
|
)
|
||||||
|
assert attachment.media_type == MediaType.IMAGE
|
||||||
|
assert attachment.mime_type == "image/jpeg"
|
||||||
|
assert attachment.url is None
|
||||||
|
assert attachment.storage_key is None
|
||||||
|
|
||||||
|
def test_media_attachment_document_with_filename(self) -> None:
|
||||||
|
"""MediaAttachment must accept document type with filename."""
|
||||||
|
attachment = MediaAttachment(
|
||||||
|
media_type=MediaType.DOCUMENT,
|
||||||
|
mime_type="application/pdf",
|
||||||
|
filename="report.pdf",
|
||||||
|
size_bytes=102400,
|
||||||
|
)
|
||||||
|
assert attachment.media_type == MediaType.DOCUMENT
|
||||||
|
assert attachment.filename == "report.pdf"
|
||||||
|
assert attachment.size_bytes == 102400
|
||||||
|
|
||||||
|
def test_media_attachment_all_fields_optional_except_type(self) -> None:
|
||||||
|
"""MediaAttachment requires only media_type; all other fields are optional."""
|
||||||
|
attachment = MediaAttachment(media_type=MediaType.AUDIO)
|
||||||
|
assert attachment.media_type == MediaType.AUDIO
|
||||||
|
assert attachment.url is None
|
||||||
|
assert attachment.storage_key is None
|
||||||
|
assert attachment.mime_type is None
|
||||||
|
assert attachment.filename is None
|
||||||
|
assert attachment.size_bytes is None
|
||||||
|
|
||||||
|
|
||||||
|
class TestNormalizeWhatsAppText:
|
||||||
|
"""Tests for normalizing WhatsApp text messages."""
|
||||||
|
|
||||||
|
def test_channel_type_is_whatsapp(self) -> None:
|
||||||
|
"""Normalized message must have channel=whatsapp."""
|
||||||
|
from gateway.normalize import normalize_whatsapp_event
|
||||||
|
|
||||||
|
payload = make_text_webhook_payload()
|
||||||
|
msg = normalize_whatsapp_event(payload)
|
||||||
|
assert msg.channel == ChannelType.WHATSAPP
|
||||||
|
|
||||||
|
def test_sender_user_id_is_wa_id(self) -> None:
|
||||||
|
"""sender.user_id must be the WhatsApp ID (wa_id) of the sender."""
|
||||||
|
from gateway.normalize import normalize_whatsapp_event
|
||||||
|
|
||||||
|
payload = make_text_webhook_payload(wa_id="5511987654321")
|
||||||
|
msg = normalize_whatsapp_event(payload)
|
||||||
|
assert msg.sender.user_id == "5511987654321"
|
||||||
|
|
||||||
|
def test_content_text_extracted(self) -> None:
|
||||||
|
"""content.text must contain the message body text."""
|
||||||
|
from gateway.normalize import normalize_whatsapp_event
|
||||||
|
|
||||||
|
payload = make_text_webhook_payload(text="Can you help me track my order?")
|
||||||
|
msg = normalize_whatsapp_event(payload)
|
||||||
|
assert msg.content.text == "Can you help me track my order?"
|
||||||
|
|
||||||
|
def test_channel_metadata_phone_number_id(self) -> None:
|
||||||
|
"""channel_metadata must contain phone_number_id."""
|
||||||
|
from gateway.normalize import normalize_whatsapp_event
|
||||||
|
|
||||||
|
payload = make_text_webhook_payload(phone_number_id="99988877766")
|
||||||
|
msg = normalize_whatsapp_event(payload)
|
||||||
|
assert msg.channel_metadata["phone_number_id"] == "99988877766"
|
||||||
|
|
||||||
|
def test_channel_metadata_message_id(self) -> None:
|
||||||
|
"""channel_metadata must contain the WhatsApp message ID (wamid)."""
|
||||||
|
from gateway.normalize import normalize_whatsapp_event
|
||||||
|
|
||||||
|
payload = make_text_webhook_payload(message_id="wamid.unique123")
|
||||||
|
msg = normalize_whatsapp_event(payload)
|
||||||
|
assert msg.channel_metadata["message_id"] == "wamid.unique123"
|
||||||
|
|
||||||
|
def test_thread_id_is_sender_wa_id(self) -> None:
|
||||||
|
"""thread_id must be the sender's wa_id (WhatsApp has no threads)."""
|
||||||
|
from gateway.normalize import normalize_whatsapp_event
|
||||||
|
|
||||||
|
payload = make_text_webhook_payload(wa_id="5511999998888")
|
||||||
|
msg = normalize_whatsapp_event(payload)
|
||||||
|
assert msg.thread_id == "5511999998888"
|
||||||
|
|
||||||
|
def test_tenant_id_none_after_normalization(self) -> None:
|
||||||
|
"""tenant_id must be None immediately after normalization."""
|
||||||
|
from gateway.normalize import normalize_whatsapp_event
|
||||||
|
|
||||||
|
payload = make_text_webhook_payload()
|
||||||
|
msg = normalize_whatsapp_event(payload)
|
||||||
|
assert msg.tenant_id is None
|
||||||
|
|
||||||
|
def test_timestamp_is_utc(self) -> None:
|
||||||
|
"""timestamp must be a timezone-aware datetime in UTC."""
|
||||||
|
from gateway.normalize import normalize_whatsapp_event
|
||||||
|
|
||||||
|
payload = make_text_webhook_payload(timestamp="1711234567")
|
||||||
|
msg = normalize_whatsapp_event(payload)
|
||||||
|
assert msg.timestamp.tzinfo is not None
|
||||||
|
assert msg.timestamp.tzinfo == timezone.utc
|
||||||
|
|
||||||
|
def test_no_media_attachments_for_text_message(self) -> None:
|
||||||
|
"""Text message must not produce any media attachments."""
|
||||||
|
from gateway.normalize import normalize_whatsapp_event
|
||||||
|
|
||||||
|
payload = make_text_webhook_payload()
|
||||||
|
msg = normalize_whatsapp_event(payload)
|
||||||
|
assert msg.content.media == []
|
||||||
|
|
||||||
|
|
||||||
|
class TestNormalizeWhatsAppImage:
|
||||||
|
"""Tests for normalizing WhatsApp image messages."""
|
||||||
|
|
||||||
|
def test_image_message_has_media_attachment(self) -> None:
|
||||||
|
"""Image message must produce a MediaAttachment in content.media."""
|
||||||
|
from gateway.normalize import normalize_whatsapp_event
|
||||||
|
|
||||||
|
payload = make_image_webhook_payload()
|
||||||
|
msg = normalize_whatsapp_event(payload)
|
||||||
|
assert len(msg.content.media) == 1
|
||||||
|
|
||||||
|
def test_image_attachment_type_is_image(self) -> None:
|
||||||
|
"""MediaAttachment media_type must be IMAGE for image messages."""
|
||||||
|
from gateway.normalize import normalize_whatsapp_event
|
||||||
|
|
||||||
|
payload = make_image_webhook_payload()
|
||||||
|
msg = normalize_whatsapp_event(payload)
|
||||||
|
assert msg.content.media[0].media_type == MediaType.IMAGE
|
||||||
|
|
||||||
|
def test_image_attachment_mime_type(self) -> None:
|
||||||
|
"""MediaAttachment mime_type must be preserved from webhook payload."""
|
||||||
|
from gateway.normalize import normalize_whatsapp_event
|
||||||
|
|
||||||
|
payload = make_image_webhook_payload(mime_type="image/png")
|
||||||
|
msg = normalize_whatsapp_event(payload)
|
||||||
|
assert msg.content.media[0].mime_type == "image/png"
|
||||||
|
|
||||||
|
def test_image_attachment_url_contains_media_id(self) -> None:
|
||||||
|
"""MediaAttachment url must contain the media_id as a placeholder for later download."""
|
||||||
|
from gateway.normalize import normalize_whatsapp_event
|
||||||
|
|
||||||
|
payload = make_image_webhook_payload(media_id="media_id_xyz789")
|
||||||
|
msg = normalize_whatsapp_event(payload)
|
||||||
|
assert msg.content.media[0].url is not None
|
||||||
|
assert "media_id_xyz789" in msg.content.media[0].url
|
||||||
|
|
||||||
|
def test_image_message_text_is_empty_string(self) -> None:
|
||||||
|
"""Image message with no caption must have empty string as text content."""
|
||||||
|
from gateway.normalize import normalize_whatsapp_event
|
||||||
|
|
||||||
|
payload = make_image_webhook_payload()
|
||||||
|
msg = normalize_whatsapp_event(payload)
|
||||||
|
assert msg.content.text == ""
|
||||||
|
|
||||||
|
|
||||||
|
class TestNormalizeWhatsAppDocument:
|
||||||
|
"""Tests for normalizing WhatsApp document messages."""
|
||||||
|
|
||||||
|
def test_document_message_has_media_attachment(self) -> None:
|
||||||
|
"""Document message must produce a MediaAttachment in content.media."""
|
||||||
|
from gateway.normalize import normalize_whatsapp_event
|
||||||
|
|
||||||
|
payload = make_document_webhook_payload()
|
||||||
|
msg = normalize_whatsapp_event(payload)
|
||||||
|
assert len(msg.content.media) == 1
|
||||||
|
|
||||||
|
def test_document_attachment_type_is_document(self) -> None:
|
||||||
|
"""MediaAttachment media_type must be DOCUMENT for document messages."""
|
||||||
|
from gateway.normalize import normalize_whatsapp_event
|
||||||
|
|
||||||
|
payload = make_document_webhook_payload()
|
||||||
|
msg = normalize_whatsapp_event(payload)
|
||||||
|
assert msg.content.media[0].media_type == MediaType.DOCUMENT
|
||||||
|
|
||||||
|
def test_document_attachment_filename(self) -> None:
|
||||||
|
"""MediaAttachment filename must be preserved from webhook payload."""
|
||||||
|
from gateway.normalize import normalize_whatsapp_event
|
||||||
|
|
||||||
|
payload = make_document_webhook_payload(filename="contract.pdf")
|
||||||
|
msg = normalize_whatsapp_event(payload)
|
||||||
|
assert msg.content.media[0].filename == "contract.pdf"
|
||||||
143
tests/unit/test_whatsapp_verify.py
Normal file
143
tests/unit/test_whatsapp_verify.py
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
"""
|
||||||
|
Unit tests for WhatsApp webhook signature verification.
|
||||||
|
|
||||||
|
Tests CHAN-03: Webhook signature is verified via HMAC-SHA256 on raw body bytes
|
||||||
|
before any JSON parsing.
|
||||||
|
|
||||||
|
Covers:
|
||||||
|
- Valid signature passes (returns raw body bytes)
|
||||||
|
- Invalid signature raises HTTPException(403)
|
||||||
|
- Timing-safe comparison used (hmac.compare_digest)
|
||||||
|
- Hub challenge verification (GET endpoint)
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import hmac
|
||||||
|
import json
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from fastapi import HTTPException
|
||||||
|
|
||||||
|
|
||||||
|
def make_valid_signature(body: bytes, app_secret: str) -> str:
|
||||||
|
"""Helper: generate a valid X-Hub-Signature-256 header value."""
|
||||||
|
sig = hmac.new(app_secret.encode(), body, hashlib.sha256).hexdigest()
|
||||||
|
return f"sha256={sig}"
|
||||||
|
|
||||||
|
|
||||||
|
class TestWhatsAppSignatureVerification:
|
||||||
|
"""Tests for verify_whatsapp_signature function."""
|
||||||
|
|
||||||
|
def test_valid_signature_returns_body(self) -> None:
|
||||||
|
"""verify_whatsapp_signature must return raw body bytes when signature is valid."""
|
||||||
|
from gateway.channels.whatsapp import verify_whatsapp_signature
|
||||||
|
|
||||||
|
app_secret = "test_app_secret_abc123"
|
||||||
|
body = b'{"object":"whatsapp_business_account"}'
|
||||||
|
sig = make_valid_signature(body, app_secret)
|
||||||
|
|
||||||
|
result = verify_whatsapp_signature(body, sig, app_secret)
|
||||||
|
assert result == body
|
||||||
|
|
||||||
|
def test_invalid_signature_raises_403(self) -> None:
|
||||||
|
"""verify_whatsapp_signature must raise HTTPException(403) when signature is invalid."""
|
||||||
|
from gateway.channels.whatsapp import verify_whatsapp_signature
|
||||||
|
|
||||||
|
app_secret = "test_app_secret_abc123"
|
||||||
|
body = b'{"object":"whatsapp_business_account"}'
|
||||||
|
bad_sig = "sha256=deadbeef0000000000000000000000000000000000000000000000000000000"
|
||||||
|
|
||||||
|
with pytest.raises(HTTPException) as exc_info:
|
||||||
|
verify_whatsapp_signature(body, bad_sig, app_secret)
|
||||||
|
|
||||||
|
assert exc_info.value.status_code == 403
|
||||||
|
|
||||||
|
def test_missing_sha256_prefix_raises_403(self) -> None:
|
||||||
|
"""Signature without 'sha256=' prefix must raise HTTPException(403)."""
|
||||||
|
from gateway.channels.whatsapp import verify_whatsapp_signature
|
||||||
|
|
||||||
|
app_secret = "test_app_secret_abc123"
|
||||||
|
body = b'{"test": "data"}'
|
||||||
|
# No 'sha256=' prefix
|
||||||
|
bad_sig = "abcdef1234567890" * 4
|
||||||
|
|
||||||
|
with pytest.raises(HTTPException) as exc_info:
|
||||||
|
verify_whatsapp_signature(body, bad_sig, app_secret)
|
||||||
|
|
||||||
|
assert exc_info.value.status_code == 403
|
||||||
|
|
||||||
|
def test_empty_body_valid_signature(self) -> None:
|
||||||
|
"""verify_whatsapp_signature works on empty body with correct signature."""
|
||||||
|
from gateway.channels.whatsapp import verify_whatsapp_signature
|
||||||
|
|
||||||
|
app_secret = "secret"
|
||||||
|
body = b""
|
||||||
|
sig = make_valid_signature(body, app_secret)
|
||||||
|
|
||||||
|
result = verify_whatsapp_signature(body, sig, app_secret)
|
||||||
|
assert result == body
|
||||||
|
|
||||||
|
def test_tampered_body_raises_403(self) -> None:
|
||||||
|
"""Signature computed on original body fails if body is modified."""
|
||||||
|
from gateway.channels.whatsapp import verify_whatsapp_signature
|
||||||
|
|
||||||
|
app_secret = "secret"
|
||||||
|
original_body = b'{"message": "hello"}'
|
||||||
|
sig = make_valid_signature(original_body, app_secret)
|
||||||
|
|
||||||
|
# Tamper with the body
|
||||||
|
tampered_body = b'{"message": "injected"}'
|
||||||
|
|
||||||
|
with pytest.raises(HTTPException) as exc_info:
|
||||||
|
verify_whatsapp_signature(tampered_body, sig, app_secret)
|
||||||
|
|
||||||
|
assert exc_info.value.status_code == 403
|
||||||
|
|
||||||
|
|
||||||
|
class TestWhatsAppHubVerification:
|
||||||
|
"""Tests for WhatsApp webhook GET verification (hub challenge handshake)."""
|
||||||
|
|
||||||
|
def test_valid_token_returns_challenge(self) -> None:
|
||||||
|
"""GET webhook with valid verify_token must return hub.challenge as plain text."""
|
||||||
|
from gateway.channels.whatsapp import verify_hub_challenge
|
||||||
|
|
||||||
|
token = "my_verify_token_xyz"
|
||||||
|
challenge = "challenge_abc_123"
|
||||||
|
|
||||||
|
result = verify_hub_challenge(
|
||||||
|
hub_mode="subscribe",
|
||||||
|
hub_verify_token=token,
|
||||||
|
hub_challenge=challenge,
|
||||||
|
expected_token=token,
|
||||||
|
)
|
||||||
|
assert result == challenge
|
||||||
|
|
||||||
|
def test_invalid_token_raises_403(self) -> None:
|
||||||
|
"""GET webhook with wrong verify_token must raise HTTPException(403)."""
|
||||||
|
from gateway.channels.whatsapp import verify_hub_challenge
|
||||||
|
|
||||||
|
with pytest.raises(HTTPException) as exc_info:
|
||||||
|
verify_hub_challenge(
|
||||||
|
hub_mode="subscribe",
|
||||||
|
hub_verify_token="wrong_token",
|
||||||
|
hub_challenge="challenge_123",
|
||||||
|
expected_token="correct_token",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert exc_info.value.status_code == 403
|
||||||
|
|
||||||
|
def test_wrong_mode_raises_403(self) -> None:
|
||||||
|
"""GET webhook with mode != 'subscribe' must raise HTTPException(403)."""
|
||||||
|
from gateway.channels.whatsapp import verify_hub_challenge
|
||||||
|
|
||||||
|
with pytest.raises(HTTPException) as exc_info:
|
||||||
|
verify_hub_challenge(
|
||||||
|
hub_mode="unsubscribe",
|
||||||
|
hub_verify_token="correct_token",
|
||||||
|
hub_challenge="challenge_123",
|
||||||
|
expected_token="correct_token",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert exc_info.value.status_code == 403
|
||||||
578
uv.lock
generated
578
uv.lock
generated
@@ -470,6 +470,75 @@ 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" },
|
{ 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 = "cuda-bindings"
|
||||||
|
version = "13.2.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "cuda-pathfinder" },
|
||||||
|
]
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/52/c8/b2589d68acf7e3d63e2be330b84bc25712e97ed799affbca7edd7eae25d6/cuda_bindings-13.2.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e865447abfb83d6a98ad5130ed3c70b1fc295ae3eeee39fd07b4ddb0671b6788", size = 5722404, upload-time = "2026-03-11T00:12:44.041Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1f/92/f899f7bbb5617bb65ec52a6eac1e9a1447a86b916c4194f8a5001b8cde0c/cuda_bindings-13.2.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:46d8776a55d6d5da9dd6e9858fba2efcda2abe6743871dee47dd06eb8cb6d955", size = 6320619, upload-time = "2026-03-11T00:12:45.939Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/df/93/eef988860a3ca985f82c4f3174fc0cdd94e07331ba9a92e8e064c260337f/cuda_bindings-13.2.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6629ca2df6f795b784752409bcaedbd22a7a651b74b56a165ebc0c9dcbd504d0", size = 5614610, upload-time = "2026-03-11T00:12:50.337Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/18/23/6db3aba46864aee357ab2415135b3fe3da7e9f1fa0221fa2a86a5968099c/cuda_bindings-13.2.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7dca0da053d3b4cc4869eff49c61c03f3c5dbaa0bcd712317a358d5b8f3f385d", size = 6149914, upload-time = "2026-03-11T00:12:52.374Z" },
|
||||||
|
{ 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.4.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c0/59/911a1a597264f1fb7ac176995a0f0b6062e37f8c1b6e0f23071a76838507/cuda_pathfinder-1.4.3-py3-none-any.whl", hash = "sha256:4345d8ead1f701c4fb8a99be6bc1843a7348b6ba0ef3b031f5a2d66fb128ae4c", size = 47951, upload-time = "2026-03-16T21:31:25.526Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[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' or sys_platform == 'win32'" },
|
||||||
|
]
|
||||||
|
cudart = [
|
||||||
|
{ name = "nvidia-cuda-runtime", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||||
|
]
|
||||||
|
cufft = [
|
||||||
|
{ name = "nvidia-cufft", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||||
|
]
|
||||||
|
cufile = [
|
||||||
|
{ name = "nvidia-cufile", marker = "sys_platform == 'linux'" },
|
||||||
|
]
|
||||||
|
cupti = [
|
||||||
|
{ name = "nvidia-cuda-cupti", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||||
|
]
|
||||||
|
curand = [
|
||||||
|
{ name = "nvidia-curand", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||||
|
]
|
||||||
|
cusolver = [
|
||||||
|
{ name = "nvidia-cusolver", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||||
|
]
|
||||||
|
cusparse = [
|
||||||
|
{ name = "nvidia-cusparse", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||||
|
]
|
||||||
|
nvjitlink = [
|
||||||
|
{ name = "nvidia-nvjitlink", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||||
|
]
|
||||||
|
nvrtc = [
|
||||||
|
{ name = "nvidia-cuda-nvrtc", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||||
|
]
|
||||||
|
nvtx = [
|
||||||
|
{ name = "nvidia-nvtx", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "deprecated"
|
name = "deprecated"
|
||||||
version = "1.3.1"
|
version = "1.3.1"
|
||||||
@@ -1084,6 +1153,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/67/8a/a342b2f0251f3dac4ca17618265d93bf244a2a4d089126e81e4c1056ac50/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bb00b6d26db67a05fe3e12c76edc75f32077fb51deed13822dc648fa373bc19", size = 343768, upload-time = "2026-02-02T12:37:55.055Z" },
|
{ url = "https://files.pythonhosted.org/packages/67/8a/a342b2f0251f3dac4ca17618265d93bf244a2a4d089126e81e4c1056ac50/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bb00b6d26db67a05fe3e12c76edc75f32077fb51deed13822dc648fa373bc19", size = 343768, upload-time = "2026-02-02T12:37:55.055Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[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]]
|
[[package]]
|
||||||
name = "jsonschema"
|
name = "jsonschema"
|
||||||
version = "4.26.0"
|
version = "4.26.0"
|
||||||
@@ -1215,6 +1293,7 @@ dependencies = [
|
|||||||
{ name = "fastapi", extra = ["standard"] },
|
{ name = "fastapi", extra = ["standard"] },
|
||||||
{ name = "httpx" },
|
{ name = "httpx" },
|
||||||
{ name = "konstruct-shared" },
|
{ name = "konstruct-shared" },
|
||||||
|
{ name = "sentence-transformers" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
@@ -1223,6 +1302,7 @@ requires-dist = [
|
|||||||
{ name = "fastapi", extras = ["standard"], specifier = ">=0.115.0" },
|
{ name = "fastapi", extras = ["standard"], specifier = ">=0.115.0" },
|
||||||
{ name = "httpx", specifier = ">=0.28.0" },
|
{ name = "httpx", specifier = ">=0.28.0" },
|
||||||
{ name = "konstruct-shared", editable = "packages/shared" },
|
{ name = "konstruct-shared", editable = "packages/shared" },
|
||||||
|
{ name = "sentence-transformers", specifier = ">=3.0.0" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1255,6 +1335,7 @@ dependencies = [
|
|||||||
{ name = "celery", extra = ["redis"] },
|
{ name = "celery", extra = ["redis"] },
|
||||||
{ name = "fastapi", extra = ["standard"] },
|
{ name = "fastapi", extra = ["standard"] },
|
||||||
{ name = "httpx" },
|
{ name = "httpx" },
|
||||||
|
{ name = "pgvector" },
|
||||||
{ name = "pydantic", extra = ["email"] },
|
{ name = "pydantic", extra = ["email"] },
|
||||||
{ name = "pydantic-settings" },
|
{ name = "pydantic-settings" },
|
||||||
{ name = "redis" },
|
{ name = "redis" },
|
||||||
@@ -1270,6 +1351,7 @@ requires-dist = [
|
|||||||
{ name = "celery", extras = ["redis"], specifier = ">=5.4.0" },
|
{ name = "celery", extras = ["redis"], specifier = ">=5.4.0" },
|
||||||
{ name = "fastapi", extras = ["standard"], specifier = ">=0.115.0" },
|
{ name = "fastapi", extras = ["standard"], specifier = ">=0.115.0" },
|
||||||
{ name = "httpx", specifier = ">=0.28.0" },
|
{ name = "httpx", specifier = ">=0.28.0" },
|
||||||
|
{ name = "pgvector", specifier = ">=0.3.0" },
|
||||||
{ name = "pydantic", extras = ["email"], specifier = ">=2.12.0" },
|
{ name = "pydantic", extras = ["email"], specifier = ">=2.12.0" },
|
||||||
{ name = "pydantic-settings", specifier = ">=2.8.0" },
|
{ name = "pydantic-settings", specifier = ">=2.8.0" },
|
||||||
{ name = "redis", specifier = ">=5.2.0" },
|
{ name = "redis", specifier = ">=5.2.0" },
|
||||||
@@ -1470,6 +1552,15 @@ 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" },
|
{ 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 = "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]]
|
[[package]]
|
||||||
name = "multidict"
|
name = "multidict"
|
||||||
version = "6.7.1"
|
version = "6.7.1"
|
||||||
@@ -1611,6 +1702,225 @@ 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" },
|
{ 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.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/10/8b/c265f4823726ab832de836cdd184d0986dcf94480f81e8739692a7ac7af2/numpy-2.4.3.tar.gz", hash = "sha256:483a201202b73495f00dbc83796c6ae63137a9bdade074f7648b3e32613412dd", size = 20727743, upload-time = "2026-03-09T07:58:53.426Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a9/ed/6388632536f9788cea23a3a1b629f25b43eaacd7d7377e5d6bc7b9deb69b/numpy-2.4.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:61b0cbabbb6126c8df63b9a3a0c4b1f44ebca5e12ff6997b80fcf267fb3150ef", size = 16669628, upload-time = "2026-03-09T07:56:24.252Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/74/1b/ee2abfc68e1ce728b2958b6ba831d65c62e1b13ce3017c13943f8f9b5b2e/numpy-2.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7395e69ff32526710748f92cd8c9849b361830968ea3e24a676f272653e8983e", size = 14696872, upload-time = "2026-03-09T07:56:26.991Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ba/d1/780400e915ff5638166f11ca9dc2c5815189f3d7cf6f8759a1685e586413/numpy-2.4.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:abdce0f71dcb4a00e4e77f3faf05e4616ceccfe72ccaa07f47ee79cda3b7b0f4", size = 5203489, upload-time = "2026-03-09T07:56:29.414Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0b/bb/baffa907e9da4cc34a6e556d6d90e032f6d7a75ea47968ea92b4858826c4/numpy-2.4.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:48da3a4ee1336454b07497ff7ec83903efa5505792c4e6d9bf83d99dc07a1e18", size = 6550814, upload-time = "2026-03-09T07:56:32.225Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7b/12/8c9f0c6c95f76aeb20fc4a699c33e9f827fa0d0f857747c73bb7b17af945/numpy-2.4.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:32e3bef222ad6b052280311d1d60db8e259e4947052c3ae7dd6817451fc8a4c5", size = 15666601, upload-time = "2026-03-09T07:56:34.461Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bd/79/cc665495e4d57d0aa6fbcc0aa57aa82671dfc78fbf95fe733ed86d98f52a/numpy-2.4.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7dd01a46700b1967487141a66ac1a3cf0dd8ebf1f08db37d46389401512ca97", size = 16621358, upload-time = "2026-03-09T07:56:36.852Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a8/40/b4ecb7224af1065c3539f5ecfff879d090de09608ad1008f02c05c770cb3/numpy-2.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:76f0f283506c28b12bba319c0fab98217e9f9b54e6160e9c79e9f7348ba32e9c", size = 17016135, upload-time = "2026-03-09T07:56:39.337Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f7/b1/6a88e888052eed951afed7a142dcdf3b149a030ca59b4c71eef085858e43/numpy-2.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:737f630a337364665aba3b5a77e56a68cc42d350edd010c345d65a3efa3addcc", size = 18345816, upload-time = "2026-03-09T07:56:42.31Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f3/8f/103a60c5f8c3d7fc678c19cd7b2476110da689ccb80bc18050efbaeae183/numpy-2.4.3-cp312-cp312-win32.whl", hash = "sha256:26952e18d82a1dbbc2f008d402021baa8d6fc8e84347a2072a25e08b46d698b9", size = 5960132, upload-time = "2026-03-09T07:56:44.851Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d7/7c/f5ee1bf6ed888494978046a809df2882aad35d414b622893322df7286879/numpy-2.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:65f3c2455188f09678355f5cae1f959a06b778bc66d535da07bf2ef20cd319d5", size = 12316144, upload-time = "2026-03-09T07:56:47.057Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/71/46/8d1cb3f7a00f2fb6394140e7e6623696e54c6318a9d9691bb4904672cf42/numpy-2.4.3-cp312-cp312-win_arm64.whl", hash = "sha256:2abad5c7fef172b3377502bde47892439bae394a71bc329f31df0fd829b41a9e", size = 10220364, upload-time = "2026-03-09T07:56:49.849Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b6/d0/1fe47a98ce0df229238b77611340aff92d52691bcbc10583303181abf7fc/numpy-2.4.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b346845443716c8e542d54112966383b448f4a3ba5c66409771b8c0889485dd3", size = 16665297, upload-time = "2026-03-09T07:56:52.296Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/27/d9/4e7c3f0e68dfa91f21c6fb6cf839bc829ec920688b1ce7ec722b1a6202fb/numpy-2.4.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2629289168f4897a3c4e23dc98d6f1731f0fc0fe52fb9db19f974041e4cc12b9", size = 14691853, upload-time = "2026-03-09T07:56:54.992Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3a/66/bd096b13a87549683812b53ab211e6d413497f84e794fb3c39191948da97/numpy-2.4.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:bb2e3cf95854233799013779216c57e153c1ee67a0bf92138acca0e429aefaee", size = 5198435, upload-time = "2026-03-09T07:56:57.184Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a2/2f/687722910b5a5601de2135c891108f51dfc873d8e43c8ed9f4ebb440b4a2/numpy-2.4.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:7f3408ff897f8ab07a07fbe2823d7aee6ff644c097cc1f90382511fe982f647f", size = 6546347, upload-time = "2026-03-09T07:56:59.531Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bf/ec/7971c4e98d86c564750393fab8d7d83d0a9432a9d78bb8a163a6dc59967a/numpy-2.4.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:decb0eb8a53c3b009b0962378065589685d66b23467ef5dac16cbe818afde27f", size = 15664626, upload-time = "2026-03-09T07:57:01.385Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7e/eb/7daecbea84ec935b7fc732e18f532073064a3816f0932a40a17f3349185f/numpy-2.4.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5f51900414fc9204a0e0da158ba2ac52b75656e7dce7e77fb9f84bfa343b4cc", size = 16608916, upload-time = "2026-03-09T07:57:04.008Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/df/58/2a2b4a817ffd7472dca4421d9f0776898b364154e30c95f42195041dc03b/numpy-2.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6bd06731541f89cdc01b261ba2c9e037f1543df7472517836b78dfb15bd6e476", size = 17015824, upload-time = "2026-03-09T07:57:06.347Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4a/ca/627a828d44e78a418c55f82dd4caea8ea4a8ef24e5144d9e71016e52fb40/numpy-2.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:22654fe6be0e5206f553a9250762c653d3698e46686eee53b399ab90da59bd92", size = 18334581, upload-time = "2026-03-09T07:57:09.114Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cd/c0/76f93962fc79955fcba30a429b62304332345f22d4daec1cb33653425643/numpy-2.4.3-cp313-cp313-win32.whl", hash = "sha256:d71e379452a2f670ccb689ec801b1218cd3983e253105d6e83780967e899d687", size = 5958618, upload-time = "2026-03-09T07:57:11.432Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b1/3c/88af0040119209b9b5cb59485fa48b76f372c73068dbf9254784b975ac53/numpy-2.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:0a60e17a14d640f49146cb38e3f105f571318db7826d9b6fef7e4dce758faecd", size = 12312824, upload-time = "2026-03-09T07:57:13.586Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/58/ce/3d07743aced3d173f877c3ef6a454c2174ba42b584ab0b7e6d99374f51ed/numpy-2.4.3-cp313-cp313-win_arm64.whl", hash = "sha256:c9619741e9da2059cd9c3f206110b97583c7152c1dc9f8aafd4beb450ac1c89d", size = 10221218, upload-time = "2026-03-09T07:57:16.183Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/62/09/d96b02a91d09e9d97862f4fc8bfebf5400f567d8eb1fe4b0cc4795679c15/numpy-2.4.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7aa4e54f6469300ebca1d9eb80acd5253cdfa36f2c03d79a35883687da430875", size = 14819570, upload-time = "2026-03-09T07:57:18.564Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b5/ca/0b1aba3905fdfa3373d523b2b15b19029f4f3031c87f4066bd9d20ef6c6b/numpy-2.4.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d1b90d840b25874cf5cd20c219af10bac3667db3876d9a495609273ebe679070", size = 5326113, upload-time = "2026-03-09T07:57:21.052Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c0/63/406e0fd32fcaeb94180fd6a4c41e55736d676c54346b7efbce548b94a914/numpy-2.4.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:a749547700de0a20a6718293396ec237bb38218049cfce788e08fcb716e8cf73", size = 6646370, upload-time = "2026-03-09T07:57:22.804Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b6/d0/10f7dc157d4b37af92720a196be6f54f889e90dcd30dce9dc657ed92c257/numpy-2.4.3-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94f3c4a151a2e529adf49c1d54f0f57ff8f9b233ee4d44af623a81553ab86368", size = 15723499, upload-time = "2026-03-09T07:57:24.693Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/66/f1/d1c2bf1161396629701bc284d958dc1efa3a5a542aab83cf11ee6eb4cba5/numpy-2.4.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22c31dc07025123aedf7f2db9e91783df13f1776dc52c6b22c620870dc0fab22", size = 16657164, upload-time = "2026-03-09T07:57:27.676Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1a/be/cca19230b740af199ac47331a21c71e7a3d0ba59661350483c1600d28c37/numpy-2.4.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:148d59127ac95979d6f07e4d460f934ebdd6eed641db9c0db6c73026f2b2101a", size = 17081544, upload-time = "2026-03-09T07:57:30.664Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b9/c5/9602b0cbb703a0936fb40f8a95407e8171935b15846de2f0776e08af04c7/numpy-2.4.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a97cbf7e905c435865c2d939af3d93f99d18eaaa3cabe4256f4304fb51604349", size = 18380290, upload-time = "2026-03-09T07:57:33.763Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ed/81/9f24708953cd30be9ee36ec4778f4b112b45165812f2ada4cc5ea1c1f254/numpy-2.4.3-cp313-cp313t-win32.whl", hash = "sha256:be3b8487d725a77acccc9924f65fd8bce9af7fac8c9820df1049424a2115af6c", size = 6082814, upload-time = "2026-03-09T07:57:36.491Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e2/9e/52f6eaa13e1a799f0ab79066c17f7016a4a8ae0c1aefa58c82b4dab690b4/numpy-2.4.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1ec84fd7c8e652b0f4aaaf2e6e9cc8eaa9b1b80a537e06b2e3a2fb176eedcb26", size = 12452673, upload-time = "2026-03-09T07:57:38.281Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c4/04/b8cece6ead0b30c9fbd99bb835ad7ea0112ac5f39f069788c5558e3b1ab2/numpy-2.4.3-cp313-cp313t-win_arm64.whl", hash = "sha256:120df8c0a81ebbf5b9020c91439fccd85f5e018a927a39f624845be194a2be02", size = 10290907, upload-time = "2026-03-09T07:57:40.747Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/70/ae/3936f79adebf8caf81bd7a599b90a561334a658be4dcc7b6329ebf4ee8de/numpy-2.4.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:5884ce5c7acfae1e4e1b6fde43797d10aa506074d25b531b4f54bde33c0c31d4", size = 16664563, upload-time = "2026-03-09T07:57:43.817Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9b/62/760f2b55866b496bb1fa7da2a6db076bef908110e568b02fcfc1422e2a3a/numpy-2.4.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:297837823f5bc572c5f9379b0c9f3a3365f08492cbdc33bcc3af174372ebb168", size = 14702161, upload-time = "2026-03-09T07:57:46.169Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/32/af/a7a39464e2c0a21526fb4fb76e346fb172ebc92f6d1c7a07c2c139cc17b1/numpy-2.4.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:a111698b4a3f8dcbe54c64a7708f049355abd603e619013c346553c1fd4ca90b", size = 5208738, upload-time = "2026-03-09T07:57:48.506Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/29/8c/2a0cf86a59558fa078d83805589c2de490f29ed4fb336c14313a161d358a/numpy-2.4.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:4bd4741a6a676770e0e97fe9ab2e51de01183df3dcbcec591d26d331a40de950", size = 6543618, upload-time = "2026-03-09T07:57:50.591Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/aa/b8/612ce010c0728b1c363fa4ea3aa4c22fe1c5da1de008486f8c2f5cb92fae/numpy-2.4.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:54f29b877279d51e210e0c80709ee14ccbbad647810e8f3d375561c45ef613dd", size = 15680676, upload-time = "2026-03-09T07:57:52.34Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a9/7e/4f120ecc54ba26ddf3dc348eeb9eb063f421de65c05fc961941798feea18/numpy-2.4.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:679f2a834bae9020f81534671c56fd0cc76dd7e5182f57131478e23d0dc59e24", size = 16613492, upload-time = "2026-03-09T07:57:54.91Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2c/86/1b6020db73be330c4b45d5c6ee4295d59cfeef0e3ea323959d053e5a6909/numpy-2.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d84f0f881cb2225c2dfd7f78a10a5645d487a496c6668d6cc39f0f114164f3d0", size = 17031789, upload-time = "2026-03-09T07:57:57.641Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/07/3a/3b90463bf41ebc21d1b7e06079f03070334374208c0f9a1f05e4ae8455e7/numpy-2.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d213c7e6e8d211888cc359bab7199670a00f5b82c0978b9d1c75baf1eddbeac0", size = 18339941, upload-time = "2026-03-09T07:58:00.577Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a8/74/6d736c4cd962259fd8bae9be27363eb4883a2f9069763747347544c2a487/numpy-2.4.3-cp314-cp314-win32.whl", hash = "sha256:52077feedeff7c76ed7c9f1a0428558e50825347b7545bbb8523da2cd55c547a", size = 6007503, upload-time = "2026-03-09T07:58:03.331Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/48/39/c56ef87af669364356bb011922ef0734fc49dad51964568634c72a009488/numpy-2.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:0448e7f9caefb34b4b7dd2b77f21e8906e5d6f0365ad525f9f4f530b13df2afc", size = 12444915, upload-time = "2026-03-09T07:58:06.353Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9d/1f/ab8528e38d295fd349310807496fabb7cf9fe2e1f70b97bc20a483ea9d4a/numpy-2.4.3-cp314-cp314-win_arm64.whl", hash = "sha256:b44fd60341c4d9783039598efadd03617fa28d041fc37d22b62d08f2027fa0e7", size = 10494875, upload-time = "2026-03-09T07:58:08.734Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e6/ef/b7c35e4d5ef141b836658ab21a66d1a573e15b335b1d111d31f26c8ef80f/numpy-2.4.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0a195f4216be9305a73c0e91c9b026a35f2161237cf1c6de9b681637772ea657", size = 14822225, upload-time = "2026-03-09T07:58:11.034Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cd/8d/7730fa9278cf6648639946cc816e7cc89f0d891602584697923375f801ed/numpy-2.4.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:cd32fbacb9fd1bf041bf8e89e4576b6f00b895f06d00914820ae06a616bdfef7", size = 5328769, upload-time = "2026-03-09T07:58:13.67Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/47/01/d2a137317c958b074d338807c1b6a383406cdf8b8e53b075d804cc3d211d/numpy-2.4.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:2e03c05abaee1f672e9d67bc858f300b5ccba1c21397211e8d77d98350972093", size = 6649461, upload-time = "2026-03-09T07:58:15.912Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5c/34/812ce12bc0f00272a4b0ec0d713cd237cb390666eb6206323d1cc9cedbb2/numpy-2.4.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d1ce23cce91fcea443320a9d0ece9b9305d4368875bab09538f7a5b4131938a", size = 15725809, upload-time = "2026-03-09T07:58:17.787Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/25/c0/2aed473a4823e905e765fee3dc2cbf504bd3e68ccb1150fbdabd5c39f527/numpy-2.4.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c59020932feb24ed49ffd03704fbab89f22aa9c0d4b180ff45542fe8918f5611", size = 16655242, upload-time = "2026-03-09T07:58:20.476Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f2/c8/7e052b2fc87aa0e86de23f20e2c42bd261c624748aa8efd2c78f7bb8d8c6/numpy-2.4.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9684823a78a6cd6ad7511fc5e25b07947d1d5b5e2812c93fe99d7d4195130720", size = 17080660, upload-time = "2026-03-09T07:58:23.067Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f3/3d/0876746044db2adcb11549f214d104f2e1be00f07a67edbb4e2812094847/numpy-2.4.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0200b25c687033316fb39f0ff4e3e690e8957a2c3c8d22499891ec58c37a3eb5", size = 18380384, upload-time = "2026-03-09T07:58:25.839Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/07/12/8160bea39da3335737b10308df4f484235fd297f556745f13092aa039d3b/numpy-2.4.3-cp314-cp314t-win32.whl", hash = "sha256:5e10da9e93247e554bb1d22f8edc51847ddd7dde52d85ce31024c1b4312bfba0", size = 6154547, upload-time = "2026-03-09T07:58:28.289Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/42/f3/76534f61f80d74cc9cdf2e570d3d4eeb92c2280a27c39b0aaf471eda7b48/numpy-2.4.3-cp314-cp314t-win_amd64.whl", hash = "sha256:45f003dbdffb997a03da2d1d0cb41fbd24a87507fb41605c0420a3db5bd4667b", size = 12633645, upload-time = "2026-03-09T07:58:30.384Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1f/b6/7c0d4334c15983cec7f92a69e8ce9b1e6f31857e5ee3a413ac424e6bd63d/numpy-2.4.3-cp314-cp314t-win_arm64.whl", hash = "sha256:4d382735cecd7bcf090172489a525cd7d4087bc331f7df9f60ddc9a296cf208e", size = 10565454, upload-time = "2026-03-09T07:58:33.031Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[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" },
|
||||||
|
]
|
||||||
|
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" },
|
||||||
|
]
|
||||||
|
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" },
|
||||||
|
{ name = "nvidia-cusparse" },
|
||||||
|
{ name = "nvidia-nvjitlink" },
|
||||||
|
]
|
||||||
|
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" },
|
||||||
|
]
|
||||||
|
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-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]]
|
[[package]]
|
||||||
name = "openai"
|
name = "openai"
|
||||||
version = "2.29.0"
|
version = "2.29.0"
|
||||||
@@ -1648,6 +1958,18 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" },
|
{ url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pgvector"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "numpy" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/25/6c/6d8b4b03b958c02fa8687ec6063c49d952a189f8c91ebbe51e877dfab8f7/pgvector-0.4.2.tar.gz", hash = "sha256:322cac0c1dc5d41c9ecf782bd9991b7966685dee3a00bc873631391ed949513a", size = 31354, upload-time = "2025-12-05T01:07:17.87Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5a/26/6cee8a1ce8c43625ec561aff19df07f9776b7525d9002c86bceb3e0ac970/pgvector-0.4.2-py3-none-any.whl", hash = "sha256:549d45f7a18593783d5eec609ea1684a724ba8405c4cb182a0b2b08aeff04e08", size = 27441, upload-time = "2025-12-05T01:07:16.536Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pluggy"
|
name = "pluggy"
|
||||||
version = "1.6.0"
|
version = "1.6.0"
|
||||||
@@ -2338,6 +2660,152 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/8f/e8/726643a3ea68c727da31570bde48c7a10f1aa60eddd628d94078fec586ff/ruff-0.15.7-py3-none-win_arm64.whl", hash = "sha256:18e8d73f1c3fdf27931497972250340f92e8c861722161a9caeb89a58ead6ed2", size = 11023304, upload-time = "2026-03-19T16:26:51.669Z" },
|
{ url = "https://files.pythonhosted.org/packages/8f/e8/726643a3ea68c727da31570bde48c7a10f1aa60eddd628d94078fec586ff/ruff-0.15.7-py3-none-win_arm64.whl", hash = "sha256:18e8d73f1c3fdf27931497972250340f92e8c861722161a9caeb89a58ead6ed2", size = 11023304, upload-time = "2026-03-19T16:26:51.669Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[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 = "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/90/74/e6a7cc4b820e95cc38cf36cd74d5aa2b42e8ffc2d21fe5a9a9c45c1c7630/scikit_learn-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5fb63362b5a7ddab88e52b6dbb47dac3fd7dafeee740dc6c8d8a446ddedade8e", size = 8548242, upload-time = "2025-12-10T07:07:51.568Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/49/d8/9be608c6024d021041c7f0b3928d4749a706f4e2c3832bbede4fb4f58c95/scikit_learn-1.8.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:5025ce924beccb28298246e589c691fe1b8c1c96507e6d27d12c5fadd85bfd76", size = 8079075, upload-time = "2025-12-10T07:07:53.697Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dd/47/f187b4636ff80cc63f21cd40b7b2d177134acaa10f6bb73746130ee8c2e5/scikit_learn-1.8.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4496bb2cf7a43ce1a2d7524a79e40bc5da45cf598dbf9545b7e8316ccba47bb4", size = 8660492, upload-time = "2025-12-10T07:07:55.574Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/97/74/b7a304feb2b49df9fafa9382d4d09061a96ee9a9449a7cbea7988dda0828/scikit_learn-1.8.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0bcfe4d0d14aec44921545fd2af2338c7471de9cb701f1da4c9d85906ab847a", size = 8931904, upload-time = "2025-12-10T07:07:57.666Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9f/c4/0ab22726a04ede56f689476b760f98f8f46607caecff993017ac1b64aa5d/scikit_learn-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:35c007dedb2ffe38fe3ee7d201ebac4a2deccd2408e8621d53067733e3c74809", size = 8019359, upload-time = "2025-12-10T07:07:59.838Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/24/90/344a67811cfd561d7335c1b96ca21455e7e472d281c3c279c4d3f2300236/scikit_learn-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:8c497fff237d7b4e07e9ef1a640887fa4fb765647f86fbe00f969ff6280ce2bb", size = 7641898, upload-time = "2025-12-10T07:08:01.36Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/03/aa/e22e0768512ce9255eba34775be2e85c2048da73da1193e841707f8f039c/scikit_learn-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0d6ae97234d5d7079dc0040990a6f7aeb97cb7fa7e8945f1999a429b23569e0a", size = 8513770, upload-time = "2025-12-10T07:08:03.251Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/58/37/31b83b2594105f61a381fc74ca19e8780ee923be2d496fcd8d2e1147bd99/scikit_learn-1.8.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:edec98c5e7c128328124a029bceb09eda2d526997780fef8d65e9a69eead963e", size = 8044458, upload-time = "2025-12-10T07:08:05.336Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2d/5a/3f1caed8765f33eabb723596666da4ebbf43d11e96550fb18bdec42b467b/scikit_learn-1.8.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:74b66d8689d52ed04c271e1329f0c61635bcaf5b926db9b12d58914cdc01fe57", size = 8610341, upload-time = "2025-12-10T07:08:07.732Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/38/cf/06896db3f71c75902a8e9943b444a56e727418f6b4b4a90c98c934f51ed4/scikit_learn-1.8.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8fdf95767f989b0cfedb85f7ed8ca215d4be728031f56ff5a519ee1e3276dc2e", size = 8900022, upload-time = "2025-12-10T07:08:09.862Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1c/f9/9b7563caf3ec8873e17a31401858efab6b39a882daf6c1bfa88879c0aa11/scikit_learn-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:2de443b9373b3b615aec1bb57f9baa6bb3a9bd093f1269ba95c17d870422b271", size = 7989409, upload-time = "2025-12-10T07:08:12.028Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/49/bd/1f4001503650e72c4f6009ac0c4413cb17d2d601cef6f71c0453da2732fc/scikit_learn-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:eddde82a035681427cbedded4e6eff5e57fa59216c2e3e90b10b19ab1d0a65c3", size = 7619760, upload-time = "2025-12-10T07:08:13.688Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d2/7d/a630359fc9dcc95496588c8d8e3245cc8fd81980251079bc09c70d41d951/scikit_learn-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7cc267b6108f0a1499a734167282c00c4ebf61328566b55ef262d48e9849c735", size = 8826045, upload-time = "2025-12-10T07:08:15.215Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cc/56/a0c86f6930cfcd1c7054a2bc417e26960bb88d32444fe7f71d5c2cfae891/scikit_learn-1.8.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:fe1c011a640a9f0791146011dfd3c7d9669785f9fed2b2a5f9e207536cf5c2fd", size = 8420324, upload-time = "2025-12-10T07:08:17.561Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/46/1e/05962ea1cebc1cf3876667ecb14c283ef755bf409993c5946ade3b77e303/scikit_learn-1.8.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72358cce49465d140cc4e7792015bb1f0296a9742d5622c67e31399b75468b9e", size = 8680651, upload-time = "2025-12-10T07:08:19.952Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fe/56/a85473cd75f200c9759e3a5f0bcab2d116c92a8a02ee08ccd73b870f8bb4/scikit_learn-1.8.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:80832434a6cc114f5219211eec13dcbc16c2bac0e31ef64c6d346cde3cf054cb", size = 8925045, upload-time = "2025-12-10T07:08:22.11Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cc/b7/64d8cfa896c64435ae57f4917a548d7ac7a44762ff9802f75a79b77cb633/scikit_learn-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ee787491dbfe082d9c3013f01f5991658b0f38aa8177e4cd4bf434c58f551702", size = 8507994, upload-time = "2025-12-10T07:08:23.943Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5e/37/e192ea709551799379958b4c4771ec507347027bb7c942662c7fbeba31cb/scikit_learn-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf97c10a3f5a7543f9b88cbf488d33d175e9146115a451ae34568597ba33dcde", size = 7869518, upload-time = "2025-12-10T07:08:25.71Z" },
|
||||||
|
{ 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/35/48/b992b488d6f299dbe3f11a20b24d3dda3d46f1a635ede1c46b5b17a7b163/scipy-1.17.1-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:35c3a56d2ef83efc372eaec584314bd0ef2e2f0d2adb21c55e6ad5b344c0dcb8", size = 31610954, upload-time = "2026-02-23T00:17:49.855Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b2/02/cf107b01494c19dc100f1d0b7ac3cc08666e96ba2d64db7626066cee895e/scipy-1.17.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:fcb310ddb270a06114bb64bbe53c94926b943f5b7f0842194d585c65eb4edd76", size = 28172662, upload-time = "2026-02-23T00:18:01.64Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cf/a9/599c28631bad314d219cf9ffd40e985b24d603fc8a2f4ccc5ae8419a535b/scipy-1.17.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:cc90d2e9c7e5c7f1a482c9875007c095c3194b1cfedca3c2f3291cdc2bc7c086", size = 20344366, upload-time = "2026-02-23T00:18:12.015Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/35/f5/906eda513271c8deb5af284e5ef0206d17a96239af79f9fa0aebfe0e36b4/scipy-1.17.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:c80be5ede8f3f8eded4eff73cc99a25c388ce98e555b17d31da05287015ffa5b", size = 22704017, upload-time = "2026-02-23T00:18:21.502Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/da/34/16f10e3042d2f1d6b66e0428308ab52224b6a23049cb2f5c1756f713815f/scipy-1.17.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e19ebea31758fac5893a2ac360fedd00116cbb7628e650842a6691ba7ca28a21", size = 32927842, upload-time = "2026-02-23T00:18:35.367Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/01/8e/1e35281b8ab6d5d72ebe9911edcdffa3f36b04ed9d51dec6dd140396e220/scipy-1.17.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02ae3b274fde71c5e92ac4d54bc06c42d80e399fec704383dcd99b301df37458", size = 35235890, upload-time = "2026-02-23T00:18:49.188Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c5/5c/9d7f4c88bea6e0d5a4f1bc0506a53a00e9fcb198de372bfe4d3652cef482/scipy-1.17.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8a604bae87c6195d8b1045eddece0514d041604b14f2727bbc2b3020172045eb", size = 35003557, upload-time = "2026-02-23T00:18:54.74Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/65/94/7698add8f276dbab7a9de9fb6b0e02fc13ee61d51c7c3f85ac28b65e1239/scipy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f590cd684941912d10becc07325a3eeb77886fe981415660d9265c4c418d0bea", size = 37625856, upload-time = "2026-02-23T00:19:00.307Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a2/84/dc08d77fbf3d87d3ee27f6a0c6dcce1de5829a64f2eae85a0ecc1f0daa73/scipy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:41b71f4a3a4cab9d366cd9065b288efc4d4f3c0b37a91a8e0947fb5bd7f31d87", size = 36549682, upload-time = "2026-02-23T00:19:07.67Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bc/98/fe9ae9ffb3b54b62559f52dedaebe204b408db8109a8c66fdd04869e6424/scipy-1.17.1-cp312-cp312-win_arm64.whl", hash = "sha256:f4115102802df98b2b0db3cce5cb9b92572633a1197c77b7553e5203f284a5b3", size = 24547340, upload-time = "2026-02-23T00:19:12.024Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/76/27/07ee1b57b65e92645f219b37148a7e7928b82e2b5dbeccecb4dff7c64f0b/scipy-1.17.1-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:5e3c5c011904115f88a39308379c17f91546f77c1667cea98739fe0fccea804c", size = 31590199, upload-time = "2026-02-23T00:19:17.192Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ec/ae/db19f8ab842e9b724bf5dbb7db29302a91f1e55bc4d04b1025d6d605a2c5/scipy-1.17.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6fac755ca3d2c3edcb22f479fceaa241704111414831ddd3bc6056e18516892f", size = 28154001, upload-time = "2026-02-23T00:19:22.241Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5b/58/3ce96251560107b381cbd6e8413c483bbb1228a6b919fa8652b0d4090e7f/scipy-1.17.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:7ff200bf9d24f2e4d5dc6ee8c3ac64d739d3a89e2326ba68aaf6c4a2b838fd7d", size = 20325719, upload-time = "2026-02-23T00:19:26.329Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b2/83/15087d945e0e4d48ce2377498abf5ad171ae013232ae31d06f336e64c999/scipy-1.17.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4b400bdc6f79fa02a4d86640310dde87a21fba0c979efff5248908c6f15fad1b", size = 22683595, upload-time = "2026-02-23T00:19:30.304Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b4/e0/e58fbde4a1a594c8be8114eb4aac1a55bcd6587047efc18a61eb1f5c0d30/scipy-1.17.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b64ca7d4aee0102a97f3ba22124052b4bd2152522355073580bf4845e2550b6", size = 32896429, upload-time = "2026-02-23T00:19:35.536Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f5/5f/f17563f28ff03c7b6799c50d01d5d856a1d55f2676f537ca8d28c7f627cd/scipy-1.17.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:581b2264fc0aa555f3f435a5944da7504ea3a065d7029ad60e7c3d1ae09c5464", size = 35203952, upload-time = "2026-02-23T00:19:42.259Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8d/a5/9afd17de24f657fdfe4df9a3f1ea049b39aef7c06000c13db1530d81ccca/scipy-1.17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:beeda3d4ae615106d7094f7e7cef6218392e4465cc95d25f900bebabfded0950", size = 34979063, upload-time = "2026-02-23T00:19:47.547Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8b/13/88b1d2384b424bf7c924f2038c1c409f8d88bb2a8d49d097861dd64a57b2/scipy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6609bc224e9568f65064cfa72edc0f24ee6655b47575954ec6339534b2798369", size = 37598449, upload-time = "2026-02-23T00:19:53.238Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/35/e5/d6d0e51fc888f692a35134336866341c08655d92614f492c6860dc45bb2c/scipy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:37425bc9175607b0268f493d79a292c39f9d001a357bebb6b88fdfaff13f6448", size = 36510943, upload-time = "2026-02-23T00:20:50.89Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2a/fd/3be73c564e2a01e690e19cc618811540ba5354c67c8680dce3281123fb79/scipy-1.17.1-cp313-cp313-win_arm64.whl", hash = "sha256:5cf36e801231b6a2059bf354720274b7558746f3b1a4efb43fcf557ccd484a87", size = 24545621, upload-time = "2026-02-23T00:20:55.871Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6f/6b/17787db8b8114933a66f9dcc479a8272e4b4da75fe03b0c282f7b0ade8cd/scipy-1.17.1-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:d59c30000a16d8edc7e64152e30220bfbd724c9bbb08368c054e24c651314f0a", size = 31936708, upload-time = "2026-02-23T00:19:58.694Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/38/2e/524405c2b6392765ab1e2b722a41d5da33dc5c7b7278184a8ad29b6cb206/scipy-1.17.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:010f4333c96c9bb1a4516269e33cb5917b08ef2166d5556ca2fd9f082a9e6ea0", size = 28570135, upload-time = "2026-02-23T00:20:03.934Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fd/c3/5bd7199f4ea8556c0c8e39f04ccb014ac37d1468e6cfa6a95c6b3562b76e/scipy-1.17.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:2ceb2d3e01c5f1d83c4189737a42d9cb2fc38a6eeed225e7515eef71ad301dce", size = 20741977, upload-time = "2026-02-23T00:20:07.935Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d9/b8/8ccd9b766ad14c78386599708eb745f6b44f08400a5fd0ade7cf89b6fc93/scipy-1.17.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:844e165636711ef41f80b4103ed234181646b98a53c8f05da12ca5ca289134f6", size = 23029601, upload-time = "2026-02-23T00:20:12.161Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6d/a0/3cb6f4d2fb3e17428ad2880333cac878909ad1a89f678527b5328b93c1d4/scipy-1.17.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:158dd96d2207e21c966063e1635b1063cd7787b627b6f07305315dd73d9c679e", size = 33019667, upload-time = "2026-02-23T00:20:17.208Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f3/c3/2d834a5ac7bf3a0c806ad1508efc02dda3c8c61472a56132d7894c312dea/scipy-1.17.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74cbb80d93260fe2ffa334efa24cb8f2f0f622a9b9febf8b483c0b865bfb3475", size = 35264159, upload-time = "2026-02-23T00:20:23.087Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4d/77/d3ed4becfdbd217c52062fafe35a72388d1bd82c2d0ba5ca19d6fcc93e11/scipy-1.17.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dbc12c9f3d185f5c737d801da555fb74b3dcfa1a50b66a1a93e09190f41fab50", size = 35102771, upload-time = "2026-02-23T00:20:28.636Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bd/12/d19da97efde68ca1ee5538bb261d5d2c062f0c055575128f11a2730e3ac1/scipy-1.17.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94055a11dfebe37c656e70317e1996dc197e1a15bbcc351bcdd4610e128fe1ca", size = 37665910, upload-time = "2026-02-23T00:20:34.743Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/06/1c/1172a88d507a4baaf72c5a09bb6c018fe2ae0ab622e5830b703a46cc9e44/scipy-1.17.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e30bdeaa5deed6bc27b4cc490823cd0347d7dae09119b8803ae576ea0ce52e4c", size = 36562980, upload-time = "2026-02-23T00:20:40.575Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/70/b0/eb757336e5a76dfa7911f63252e3b7d1de00935d7705cf772db5b45ec238/scipy-1.17.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a720477885a9d2411f94a93d16f9d89bad0f28ca23c3f8daa521e2dcc3f44d49", size = 24856543, upload-time = "2026-02-23T00:20:45.313Z" },
|
||||||
|
{ 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.3.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/fe/26/448453925b6ce0c29d8b54327caa71ee4835511aef02070467402273079c/sentence_transformers-5.3.0.tar.gz", hash = "sha256:414a0a881f53a4df0e6cbace75f823bfcb6b94d674c42a384b498959b7c065e2", size = 403330, upload-time = "2026-03-12T14:53:40.778Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e2/9c/2fa7224058cad8df68d84bafee21716f30892cecc7ad1ad73bde61d23754/sentence_transformers-5.3.0-py3-none-any.whl", hash = "sha256:dca6b98db790274a68185d27a65801b58b4caf653a4e556b5f62827509347c7d", size = 512390, upload-time = "2026-03-12T14:53:39.035Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sentry-sdk"
|
name = "sentry-sdk"
|
||||||
version = "2.55.0"
|
version = "2.55.0"
|
||||||
@@ -2351,6 +2819,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/9a/66/20465097782d7e1e742d846407ea7262d338c6e876ddddad38ca8907b38f/sentry_sdk-2.55.0-py2.py3-none-any.whl", hash = "sha256:97026981cb15699394474a196b88503a393cbc58d182ece0d3abe12b9bd978d4", size = 449284, upload-time = "2026-03-17T14:15:49.604Z" },
|
{ url = "https://files.pythonhosted.org/packages/9a/66/20465097782d7e1e742d846407ea7262d338c6e876ddddad38ca8907b38f/sentry_sdk-2.55.0-py2.py3-none-any.whl", hash = "sha256:97026981cb15699394474a196b88503a393cbc58d182ece0d3abe12b9bd978d4", size = 449284, upload-time = "2026-03-17T14:15:49.604Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[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]]
|
[[package]]
|
||||||
name = "shellingham"
|
name = "shellingham"
|
||||||
version = "1.5.4"
|
version = "1.5.4"
|
||||||
@@ -2484,6 +2961,27 @@ 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" },
|
{ 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 = "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]]
|
[[package]]
|
||||||
name = "tiktoken"
|
name = "tiktoken"
|
||||||
version = "0.12.0"
|
version = "0.12.0"
|
||||||
@@ -2557,6 +3055,49 @@ wheels = [
|
|||||||
{ 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" },
|
{ 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/6f/8b/69e3008d78e5cee2b30183340cc425081b78afc5eff3d080daab0adda9aa/torch-2.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4b5866312ee6e52ea625cd211dcb97d6a2cdc1131a5f15cc0d87eec948f6dd34", size = 80606338, upload-time = "2026-03-23T18:11:34.781Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/13/16/42e5915ebe4868caa6bac83a8ed59db57f12e9a61b7d749d584776ed53d5/torch-2.11.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f99924682ef0aa6a4ab3b1b76f40dc6e273fca09f367d15a524266db100a723f", size = 419731115, upload-time = "2026-03-23T18:11:06.944Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1a/c9/82638ef24d7877510f83baf821f5619a61b45568ce21c0a87a91576510aa/torch-2.11.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:0f68f4ac6d95d12e896c3b7a912b5871619542ec54d3649cf48cc1edd4dd2756", size = 530712279, upload-time = "2026-03-23T18:10:31.481Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1c/ff/6756f1c7ee302f6d202120e0f4f05b432b839908f9071157302cedfc5232/torch-2.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:fbf39280699d1b869f55eac536deceaa1b60bd6788ba74f399cc67e60a5fab10", size = 114556047, upload-time = "2026-03-23T18:10:55.931Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/87/89/5ea6722763acee56b045435fb84258db7375c48165ec8be7880ab2b281c5/torch-2.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1e6debd97ccd3205bbb37eb806a9d8219e1139d15419982c09e23ef7d4369d18", size = 80606801, upload-time = "2026-03-23T18:10:18.649Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/32/d1/8ed2173589cbfe744ed54e5a73efc107c0085ba5777ee93a5f4c1ab90553/torch-2.11.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:63a68fa59de8f87acc7e85a5478bb2dddbb3392b7593ec3e78827c793c4b73fd", size = 419732382, upload-time = "2026-03-23T18:08:30.835Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3d/e1/b73f7c575a4b8f87a5928f50a1e35416b5e27295d8be9397d5293e7e8d4c/torch-2.11.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:cc89b9b173d9adfab59fd227f0ab5e5516d9a52b658ae41d64e59d2e55a418db", size = 530711509, upload-time = "2026-03-23T18:08:47.213Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/66/82/3e3fcdd388fbe54e29fd3f991f36846ff4ac90b0d0181e9c8f7236565f82/torch-2.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:4dda3b3f52d121063a731ddb835f010dc137b920d7fec2778e52f60d8e4bf0cd", size = 114555842, upload-time = "2026-03-23T18:09:52.111Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/db/38/8ac78069621b8c2b4979c2f96dc8409ef5e9c4189f6aac629189a78677ca/torch-2.11.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8b394322f49af4362d4f80e424bcaca7efcd049619af03a4cf4501520bdf0fb4", size = 80959574, upload-time = "2026-03-23T18:10:14.214Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6d/6c/56bfb37073e7136e6dd86bfc6af7339946dd684e0ecf2155ac0eee687ae1/torch-2.11.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:2658f34ce7e2dabf4ec73b45e2ca68aedad7a5be87ea756ad656eaf32bf1e1ea", size = 419732324, upload-time = "2026-03-23T18:09:36.604Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/07/f4/1b666b6d61d3394cca306ea543ed03a64aad0a201b6cd159f1d41010aeb1/torch-2.11.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:98bb213c3084cfe176302949bdc360074b18a9da7ab59ef2edc9d9f742504778", size = 530596026, upload-time = "2026-03-23T18:09:20.842Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/48/6b/30d1459fa7e4b67e9e3fe1685ca1d8bb4ce7c62ef436c3a615963c6c866c/torch-2.11.0-cp313-cp313t-win_amd64.whl", hash = "sha256:a97b94bbf62992949b4730c6cd2cc9aee7b335921ee8dc207d930f2ed09ae2db", size = 114793702, upload-time = "2026-03-23T18:09:47.304Z" },
|
||||||
|
{ 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]]
|
[[package]]
|
||||||
name = "tqdm"
|
name = "tqdm"
|
||||||
version = "4.67.3"
|
version = "4.67.3"
|
||||||
@@ -2569,6 +3110,43 @@ 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" },
|
{ 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 = "5.3.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "huggingface-hub" },
|
||||||
|
{ name = "numpy" },
|
||||||
|
{ name = "packaging" },
|
||||||
|
{ name = "pyyaml" },
|
||||||
|
{ name = "regex" },
|
||||||
|
{ name = "safetensors" },
|
||||||
|
{ name = "tokenizers" },
|
||||||
|
{ name = "tqdm" },
|
||||||
|
{ name = "typer" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/fc/1a/70e830d53ecc96ce69cfa8de38f163712d2b43ac52fbd743f39f56025c31/transformers-5.3.0.tar.gz", hash = "sha256:009555b364029da9e2946d41f1c5de9f15e6b1df46b189b7293f33a161b9c557", size = 8830831, upload-time = "2026-03-04T17:41:46.119Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b8/88/ae8320064e32679a5429a2c9ebbc05c2bf32cefb6e076f9b07f6d685a9b4/transformers-5.3.0-py3-none-any.whl", hash = "sha256:50ac8c89c3c7033444fb3f9f53138096b997ebb70d4b5e50a2e810bf12d3d29a", size = 10661827, upload-time = "2026-03-04T17:41:42.722Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "triton"
|
||||||
|
version = "3.6.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/17/5d/08201db32823bdf77a0e2b9039540080b2e5c23a20706ddba942924ebcd6/triton-3.6.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:374f52c11a711fd062b4bfbb201fd9ac0a5febd28a96fb41b4a0f51dde3157f4", size = 176128243, upload-time = "2026-01-20T16:16:07.857Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ab/a8/cdf8b3e4c98132f965f88c2313a4b493266832ad47fb52f23d14d4f86bb5/triton-3.6.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74caf5e34b66d9f3a429af689c1c7128daba1d8208df60e81106b115c00d6fca", size = 188266850, upload-time = "2026-01-20T16:00:43.041Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3c/12/34d71b350e89a204c2c7777a9bba0dcf2f19a5bfdd70b57c4dbc5ffd7154/triton-3.6.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:448e02fe6dc898e9e5aa89cf0ee5c371e99df5aa5e8ad976a80b93334f3494fd", size = 176133521, upload-time = "2026-01-20T16:16:13.321Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f9/0b/37d991d8c130ce81a8728ae3c25b6e60935838e9be1b58791f5997b24a54/triton-3.6.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10c7f76c6e72d2ef08df639e3d0d30729112f47a56b0c81672edc05ee5116ac9", size = 188289450, upload-time = "2026-01-20T16:00:49.136Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ce/4e/41b0c8033b503fd3cfcd12392cdd256945026a91ff02452bef40ec34bee7/triton-3.6.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1722e172d34e32abc3eb7711d0025bb69d7959ebea84e3b7f7a341cd7ed694d6", size = 176276087, upload-time = "2026-01-20T16:16:18.989Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/35/f8/9c66bfc55361ec6d0e4040a0337fb5924ceb23de4648b8a81ae9d33b2b38/triton-3.6.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d002e07d7180fd65e622134fbd980c9a3d4211fb85224b56a0a0efbd422ab72f", size = 188400296, upload-time = "2026-01-20T16:00:56.042Z" },
|
||||||
|
{ 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]]
|
[[package]]
|
||||||
name = "typer"
|
name = "typer"
|
||||||
version = "0.24.1"
|
version = "0.24.1"
|
||||||
|
|||||||
Reference in New Issue
Block a user