Merge remote-tracking branch 'origin/main' into fix/ghas-path-boundary

# Conflicts:
#	CHANGELOG.md
#	backend/core/path_authorization.py
#	frontend/src-tauri/src/commands.rs
#	tests/test_network_share.py
This commit is contained in:
debpalash
2026-08-10 04:32:23 +00:00
40 changed files with 1490 additions and 343 deletions
+4
View File
@@ -42,11 +42,15 @@ The bundled TTS model package (`pyproject.toml`) is versioned independently.
### Fixed
- Remote backends can no longer probe or overwrite arbitrary host files through native-only tools, and imported or persisted paths cannot escape their VoiceStudio data folders. (#1455)
- An error whose text merely contained the digits 401 — a file path, a byte count, a job id — no longer tells you to fix your Hugging Face token. (#1427)
- Custom MLX model IDs and saved voice instructions are now validated in bounded time, so malformed input cannot stall the backend. (#1446)
- Streaming and provider failures now return stable recovery guidance without exposing exception details. (#1462)
- Server-mode settings mutations require the admin API key, while host destinations and executable paths can only be selected through the native desktop app. (#1448)
- Automatic model-mirror checks now reject untrusted URLs before opening a network connection. (#1447)
- Sidecar engines no longer break when a library they load prints to the console. Those bytes landed in the middle of the engine's data stream, failing the generation and leaving the connection scrambled for every request after it. (#1428) — thanks @1335-Group!
- A generation abandoned while stuck on an internal lock now says so, instead of blaming your hardware and suggesting shorter text. Nothing had been computed, so none of that advice applied. (#1416, #1419)
- A machine with a GPU that ends up on CPU now says why — a missing device node, a permissions problem, a card newer than the installed ROCm, an `HSA_OVERRIDE_GFX_VERSION` that is doing more harm than good, or an NVIDIA driver the container can't reach each read differently. Before, all of them looked identical to having no GPU at all. (#1274, #1228)
- The first generation on an engine that still has to install itself no longer gives up part-way. The install reports progress now, so the generation waits for it instead of hitting its own five-minute limit. (#1414)
- A slow machine is no longer told its IndexTTS-2 install isn't there. The check that confirms an engine's virtualenv gave up after 10 seconds and counted that as a broken install, so a cold first run 500'd; it now waits longer and treats slow as unproven, not broken. (#1414) — thanks @OracleNightmare!
- A broken Python environment now says so, instead of blaming the app's own install. A missing or mismatched torch/transformers surfaced as "omnivoice not importable" and sent people reinstalling the wrong thing. (#1415)
- A model that fails to load at startup no longer leaves the app looking healthy while producing nothing — the failure and its remedy now show up in the model status. (#1415)
+28 -14
View File
@@ -734,7 +734,7 @@ async def dub_transcribe_stream(
preflight_error = asr_model_missing_detail(e.payload)
preflight_payload = e.payload
except Exception as e:
logger.exception("transcribe preflight: ASR load failed (job=%r)", job_id)
logger.error("Transcription preflight ASR load failed")
from core.failure import build_failure
f = build_failure(e, stage="transcribe-preflight", include_diagnostic=False)
preflight_error = "ASR backend initialization failed: " + f["reason"] + (
@@ -765,9 +765,10 @@ async def dub_transcribe_stream(
try:
audio_np, sr = await loop.run_in_executor(_cpu_pool, _load)
except Exception as e:
except Exception:
# Terminal error → always emit `done` (see preflight note, #578).
yield _sse_event("error", {"detail": f"audio load failed: {e}", "retryable": True})
from core.public_errors import stream_failure
yield _sse_event("error", stream_failure("transcription_failed"))
yield _sse_event("done", {})
return
@@ -867,9 +868,16 @@ async def dub_transcribe_stream(
continue
turns.append({"start": s0 + offset, "end": s1 + offset, "speaker": spk})
return {"chunks": shifted, "language": r.get("language"), "speaker_turns": turns}
except Exception as e:
logger.exception("chunk transcribe failed (backend=%s)", _asr_backend.id)
return {"chunks": [], "language": None, "error": str(e)}
except Exception:
logger.error("Chunk transcription failed (backend=%s)", _asr_backend.id)
from core.public_errors import stream_failure
failure = stream_failure("transcription_failed")
return {
"chunks": [],
"language": None,
"error": failure["detail"],
"error_code": failure["code"],
}
# Retry a failed/timed-out chunk once on a fresh pool before giving
# up. Otherwise a transient wedge on the FIRST chunk (whisperx often
@@ -900,7 +908,7 @@ async def dub_transcribe_stream(
yield _sse_event("ping", {})
try:
part = task.result()
except ASRTimeoutError as e:
except ASRTimeoutError:
# The guard already reset the pool; keep the actionable
# message (it names the durable fixes, and — after repeated
# timeouts — the crash-isolated engine escape hatch).
@@ -910,7 +918,14 @@ async def dub_transcribe_stream(
i + 1, chunks_n, transcribe_timeout_s, _attempt,
_CHUNK_TRANSCRIBE_ATTEMPTS, job_id,
)
part = {"chunks": [], "language": None, "error": str(e)}
from core.public_errors import stream_failure
failure = stream_failure("transcription_timeout")
part = {
"chunks": [],
"language": None,
"error": failure["detail"],
"error_code": failure["code"],
}
# Success → keep it. Failure/timeout → retry once on a fresh
# worker (the internal _transcribe_chunk except returns an
# error-part; the timeout path already reset the pool).
@@ -964,6 +979,7 @@ async def dub_transcribe_stream(
"segments": chunk_segs,
"progress": (i + 1) / chunks_n,
"error": part.get("error"),
"error_code": part.get("error_code"),
})
if job.get("aborted"):
@@ -1447,12 +1463,10 @@ async def dub_transcribe_stream(
try:
async for ev in _gen_body():
yield ev
except Exception as e: # noqa: BLE001 — last-resort stream finalizer
logger.exception("transcribe stream crashed (job=%r)", job_id)
from core.failure import build_failure
f = build_failure(e, stage="transcribe", include_diagnostic=False)
detail = f["reason"] + (f"{f['hint']}" if f.get("hint") else "")
yield _sse_event("error", {"detail": detail, "retryable": True})
except Exception: # noqa: BLE001 — last-resort stream finalizer
logger.error("Transcription stream failed unexpectedly")
from core.public_errors import stream_failure
yield _sse_event("error", stream_failure("transcription_failed"))
yield _sse_event("done", {})
finally:
# Last-resort VRAM release (see _loaded_asr above): covers crashes,
+4 -3
View File
@@ -527,7 +527,6 @@ async def dub_download(
# path or ffmpeg argv (export dir, retime work path, slice paths). Real
# job ids are short uuid slices — alnum/hyphen/underscore only.
job_dir = _job_dir_or_400(job_id)
save_path = _consume_native_save(save_authorization)
job = _get_job(job_id)
if not job:
raise HTTPException(status_code=404, detail="Job not found")
@@ -605,6 +604,7 @@ async def dub_download(
safe_name = "".join(c for c in base_name if c.isalnum() or c in "-_ ").strip() or "output"
dl_name = f"dubbed_{safe_name}_{safe_lang}_{stamp}.{fmt}"
media_type = _MEDIA_TYPES.get(f".{fmt}", "audio/mp4")
save_path = _consume_native_save(save_authorization)
if save_path:
return _native_save(out_path, save_path, dl_name, media_type=media_type)
return FileResponse(
@@ -884,6 +884,7 @@ async def dub_download(
if retime_warning is not None:
extra_headers["X-Dub-Export-Warning"] = "video-retime-fallback"
save_path = _consume_native_save(save_authorization)
if save_path:
result = _native_save(output_path, save_path, dl_name, media_type="video/mp4")
if retime_warning is not None:
@@ -1430,7 +1431,6 @@ async def dub_download_audio(
save_authorization: str = Header("", alias="X-VoiceStudio-Path-Authorization"),
):
job_dir = _job_dir_or_400(job_id)
save_path = _consume_native_save(save_authorization)
lang = _safe_lang_or_400(lang)
job = _get_job(job_id)
if not job:
@@ -1473,6 +1473,7 @@ async def dub_download_audio(
base_name = os.path.splitext(job.get('filename', 'audio'))[0]
safe_name = ''.join(c for c in base_name if c.isalnum() or c in '-_ ').strip() or 'audio'
dl_name = f"dubbed_audio_{lang_label}_{safe_name}_{stamp}.wav"
save_path = _consume_native_save(save_authorization)
if save_path:
return _native_save(wav_path, save_path, dl_name, media_type="audio/wav")
return FileResponse(
@@ -1671,7 +1672,6 @@ async def dub_download_mp3(
bitrate: str = Query("192k"),
):
job_dir = _job_dir_or_400(job_id)
save_path = _consume_native_save(save_authorization)
lang = _safe_lang_or_400(lang)
job = _get_job(job_id)
if not job:
@@ -1741,6 +1741,7 @@ async def dub_download_mp3(
base_name = os.path.splitext(job.get('filename', 'audio'))[0]
safe_name = ''.join(c for c in base_name if c.isalnum() or c in '-_ ').strip() or 'audio'
dl_name = f"dubbed_{lang_label}_{safe_name}_{stamp}.mp3"
save_path = _consume_native_save(save_authorization)
if save_path:
return _native_save(mp3_path, save_path, dl_name, media_type="audio/mpeg")
return FileResponse(
+15 -4
View File
@@ -16,11 +16,12 @@ Environment variables (`OMNIVOICE_TTS_BACKEND`, `OMNIVOICE_ASR_BACKEND`,
a backend without Settings silently undoing it.
"""
import os
import re
import threading
from time import perf_counter
from fastapi import APIRouter, Depends, HTTPException
from huggingface_hub import utils as hf_utils
from huggingface_hub.errors import HFValidationError
from pydantic import BaseModel
from api.dependencies import require_loopback
@@ -37,6 +38,16 @@ _FAMILIES = {
"llm": (llm_backend, "llm_backend"),
}
def _is_hf_repo_id(value: str) -> bool:
"""Validate the route's ``owner/repo`` contract in bounded time."""
if not isinstance(value, str) or len(value) > 96 or value.count("/") != 1:
return False
try:
hf_utils.validate_repo_id(value)
except (HFValidationError, TypeError):
return False
return True
@router.get("/engines")
def list_all_engines():
@@ -579,11 +590,11 @@ def select_engine(req: SelectEngineRequest):
# Anything else (typo'd key, malformed id) is rejected outright
# rather than silently persisted as a "custom repo" that then fails
# to resolve at load time.
if req.model_id not in known_keys and not re.fullmatch(r"[\w.-]+/[\w.-]+", req.model_id):
if req.model_id not in known_keys and not _is_hf_repo_id(req.model_id):
raise HTTPException(
400,
f"Unknown mlx-audio model: {req.model_id!r}. Expected one of "
f"{sorted(known_keys)} or a HF repo id like 'owner/name'.",
"Unknown mlx-audio model. Expected a curated model key or a "
"Hugging Face repo ID like 'owner/name'.",
)
prefs.set_("mlx_audio_model_id", req.model_id)
prefs.set_(pref_key, req.backend_id)
+13 -11
View File
@@ -1538,17 +1538,19 @@ async def generate_speech(
# In-band error frame carries the machine-readable retryable
# marker (#1190) — an NDJSON consumer can back off instead of
# guessing from the prose.
logger.error("Streaming generate timed out: %s", e)
yield _line({
"type": "error", "detail": str(e), "retryable": True,
"retry_after": getattr(e, "retry_after", 30),
})
except ValueError as e:
logger.error("Streaming generate validation failed: %s", e)
yield _line({"type": "error", "detail": str(e)})
except Exception as e:
logger.error("Streaming generate failed: %s\n%s", e, traceback.format_exc())
yield _line({"type": "error", "detail": _safe_exc_text(e)})
logger.error("Streaming generation capacity unavailable")
from core.public_errors import stream_failure
failure = stream_failure("generation_busy")
failure["retry_after"] = getattr(e, "retry_after", 30)
yield _line({"type": "error", **failure})
except ValueError:
logger.error("Streaming generation request rejected")
from core.public_errors import stream_failure
yield _line({"type": "error", **stream_failure("invalid_request")})
except Exception:
logger.error("Streaming generation failed unexpectedly")
from core.public_errors import stream_failure
yield _line({"type": "error", **stream_failure("generation_failed")})
finally:
# Ownership of the temp reference clip moves to this generator
# in stream mode (the route returns before rendering starts).
+6 -4
View File
@@ -69,10 +69,12 @@ def _voice_asset(value) -> Path | None:
if not value:
return None
try:
return resolve_within(VOICES_DIR, value)
except UnsafePath:
logger.warning("Ignoring voice asset outside the voices directory")
return None
resolved = resolve_within(VOICES_DIR, value)
except UnsafePath as exc:
raise HTTPException(status_code=400, detail="Voice profile contains an invalid asset path") from exc
if not resolved.is_file():
raise HTTPException(status_code=400, detail="Voice profile reference audio is missing")
return resolved
# ── Export ──────────────────────────────────────────────────────────────────
+10 -32
View File
@@ -444,42 +444,20 @@ def test_llm_provider(provider_id: str):
"reply": reply[:80],
"latency_ms": int((_time.monotonic() - t0) * 1000),
}
except Exception as e: # noqa: BLE001 — surface a clean, scrubbed error to the UI
except Exception as e: # noqa: BLE001 — classify without exposing diagnostics
kind = _classify_llm_error(e)
detail = _scrub_llm_detail(e, api_key)
# A 404 from a LOCAL server is almost never a wrong URL — the request
# reached it — it is a model name the server does not have loaded. The
# generic "check the model name and Base URL path" sends the user to
# audit a URL that works. Ask what IS loaded and say so (#1332).
from core.public_errors import provider_failure
failure = provider_failure(kind)
# A successful local catalog probe proves the cached model is stale.
# Invalidate it, but never include catalog or exception text in the
# response: both are controlled by the provider.
if kind == "not_found" and p.local:
available = _local_models(base_url, api_key)
asked = llm_providers.resolve_model(p)
if available:
detail = (
f"{p.display_name} is running, but has no model named "
f"{asked!r}. Loaded right now: {', '.join(available[:10])}"
+ ("" if len(available) > 10 else "")
+ ". Pick one in the Model field above."
)
# The cached discovery, if any, produced a name this server
# rejects — most likely the user swapped the loaded model
# inside the local app. Drop it so the next attempt re-asks
# rather than repeating the same 404 until the TTL expires.
if available is not None:
llm_providers.forget_discovered_models(p.id)
elif available == []:
detail = (
f"{p.display_name} is running, but reports no loaded models, "
f"so {asked!r} cannot be served. Load a model in "
f"{p.display_name} first, then test again."
)
llm_providers.forget_discovered_models(p.id)
# available is None: the model listing itself failed, so nothing
# here is established. Keep the generic 404 text rather than
# inventing a diagnosis the lookup did not support.
return {
"ok": False,
"kind": kind,
"detail": detail,
**failure,
"latency_ms": int((_time.monotonic() - t0) * 1000),
}
@@ -528,10 +506,10 @@ def list_llm_provider_models(provider_id: str):
# can say "first 200 shown" rather than implying it's the full list.
return {"ok": True, "models": ids[:200], "truncated": len(ids) > 200}
except Exception as e: # noqa: BLE001
from core.public_errors import provider_failure
return {
"ok": False,
"kind": _classify_llm_error(e),
"detail": _scrub_llm_detail(e, api_key),
**provider_failure(_classify_llm_error(e)),
"models": [],
}
+19 -6
View File
@@ -5,12 +5,12 @@ SoniTranslate sidecar integration.
"""
import logging
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from typing import Optional
from services import sonitranslate as soni
from api.dependencies import require_native_access
from services import sonitranslate as soni
router = APIRouter(prefix="/engines/sonitranslate", tags=["SoniTranslate"])
logger = logging.getLogger("omnivoice.api")
@@ -64,12 +64,12 @@ async def sonitranslate_stop():
class DubRequest(BaseModel):
video_path: str
video_authorization: str
target_language: str = "Spanish (es)"
source_language: str = "Automatic detection"
tts_voice: str = "es-ES-AlvaroNeural-Male"
max_speakers: int = 1
output_dir: Optional[str] = None
output_authorization: str | None = None
@router.post("/dub", dependencies=[Depends(require_native_access)])
@@ -90,15 +90,28 @@ async def sonitranslate_dub(body: DubRequest):
AudioSeal provenance mark that every built-in synthesis path carries.
"""
try:
from core.path_authorization import PathAuthorizationError, consume
try:
video_path = consume(body.video_authorization, "soni_input")
output_dir = (
consume(body.output_authorization, "soni_output_dir")
if body.output_authorization
else None
)
except PathAuthorizationError as exc:
raise HTTPException(status_code=403, detail=str(exc)) from exc
result = await soni.dub_video(
video_path=body.video_path,
video_path=video_path,
target_language=body.target_language,
source_language=body.source_language,
tts_voice=body.tts_voice,
max_speakers=body.max_speakers,
output_dir=body.output_dir,
output_dir=output_dir,
)
return result
except HTTPException:
raise
except Exception as e:
logger.exception("SoniTranslate dub failed")
raise HTTPException(status_code=500, detail=str(e))
+29 -2
View File
@@ -29,6 +29,24 @@ from core.logging_filter import REDACTED, _HF_TOKEN_RE
# Env vars whose *name* implies a credential — their values are redacted.
_SECRET_NAME_RE = re.compile(r"(TOKEN|KEY|SECRET)", re.IGNORECASE)
#: An HTTP 401, not any number or identifier that contains 401.
#:
#: Digit-only boundaries are not enough: they reject `4012` and `1401` but
#: still accept `pytest-401`, `x401y` and `401.0`, so an error that mentions
#: Hugging Face anywhere and carries one of those elsewhere still classified
#: as an auth failure (CodeRabbit, #1427).
#:
#: The three guards, each for a family the others let through:
#: `(?<![\w.-])` identifier / path / dotted-version prefixes — `x401y`,
#: `pytest-401`, `v1.401`
#: `(?![\w-])` identifier and hyphen suffixes — `401k`, `401-retry`
#: `(?!\.\d)` dotted numerics — `401.0`, `401.25` (a trailing sentence
#: full stop is still fine: `status 401.`)
#:
#: Matches what a real status code looks like: `401`, `(401)`, `status 401:`,
#: `HTTP/1.1 401`.
_HTTP_401 = re.compile(r"(?<![\w.-])401(?![\w-])(?!\.\d)")
_REDACTED_VALUE = "***REDACTED***"
# One-line "what to do" per docs-taxonomy key. Keys mirror error_docs_map's
@@ -404,8 +422,17 @@ def classify(reason: str) -> str:
or "sslcertverificationerror" in low
):
return "SSL_HANDSHAKE_FAILURE"
if ("huggingface" in low or "hf_token" in low or "401" in low or "unauthorized" in low) and (
"token" in low or "auth" in low or "401" in low or "unauthorized" in low
# A bare 401 is no longer sufficient evidence on its own. It used to
# satisfy BOTH halves of this condition, which made the `and` vacuous, so
# any message containing those three digits — a path, a byte count, an id
# — classified as an auth failure by itself. CI hit it when pytest's
# numbered temp directory reached `pytest-401` and an audio-save error
# came back as "set a valid HF_TOKEN". A real 401 always arrives with the
# word Unauthorized or an HF URL beside it, so requiring that costs
# nothing and closes the class.
has_401 = _HTTP_401.search(low) is not None
if ("huggingface" in low or "hf_token" in low or "unauthorized" in low) and (
"token" in low or "auth" in low or has_401 or "unauthorized" in low
):
return "HF_AUTH_FAILED"
# #874: a model download that failed because the CONFIGURED HF mirror is
+8 -1
View File
@@ -15,7 +15,14 @@ import stat
from core.config import DATA_DIR
_TOKEN_RE = re.compile(r"[0-9a-f]{64}\Z")
_KINDS = {"models_dir", "ffmpeg", "ffprobe", "dub_export"}
_KINDS = {
"models_dir",
"ffmpeg",
"ffprobe",
"dub_export",
"soni_input",
"soni_output_dir",
}
_AUTH_DIR = os.path.join(DATA_DIR, ".path-authorizations")
+53
View File
@@ -0,0 +1,53 @@
"""Data-independent error metadata safe for API and streaming responses."""
from __future__ import annotations
_PROVIDER_DETAILS = {
"auth": "Authentication failed. Check the provider API key.",
"not_found": "Provider or model not found. Check the model and Base URL.",
"rate_limit": "The provider rate limit was reached. Try again later.",
"network": "The provider could not be reached. Check the connection and Base URL.",
"config": "Configure the provider before using it.",
"error": "The provider request failed. Try again.",
}
def provider_failure(kind: str) -> dict[str, str]:
"""Return a stable provider error class and remediation message."""
safe_kind = kind if kind in _PROVIDER_DETAILS else "error"
return {"kind": safe_kind, "detail": _PROVIDER_DETAILS[safe_kind]}
def stream_failure(code: str) -> dict[str, object]:
"""Return stable stream metadata selected only from an internal code."""
failures: dict[str, dict[str, object]] = {
"generation_busy": {
"code": "generation_busy",
"detail": "Generation capacity is busy. Try again shortly.",
"retryable": True,
},
"invalid_request": {
"code": "invalid_request",
"detail": "The generation request could not be processed.",
"retryable": False,
},
"generation_failed": {
"code": "generation_failed",
"detail": "Generation failed. Check the selected engine and try again.",
"retryable": True,
},
"transcription_failed": {
"code": "transcription_failed",
"detail": "Transcription failed. Check the selected ASR engine and try again.",
"retryable": True,
},
"transcription_timeout": {
"code": "transcription_timeout",
"detail": (
"Transcription timed out while the backend is running. Increase "
"OMNIVOICE_TRANSCRIBE_CHUNK_TIMEOUT_S or select the "
"faster-whisper-isolated engine, then try again."
),
"retryable": True,
},
}
return dict(failures.get(code, failures["generation_failed"]))
+3 -3
View File
@@ -160,10 +160,10 @@ def parse(text: str) -> Direction:
try:
body = llm.chat(system=_LLM_PROMPT, user=text)
except Exception as e:
logger.warning("director LLM parse failed: %s", e)
except Exception:
logger.warning("director LLM parse failed; using heuristic parser")
d = _heuristic_parse(text)
d.error = f"llm-parse-failed: {e}"
d.error = "llm-parse-failed"
return d
raw = body.strip()
+6 -3
View File
@@ -184,9 +184,12 @@ def adjust_for_slot(
system=system, user="\n".join(user_lines),
temperature=0.2, # pinned like the Fast path — default 1.0 drifts/invents
)
except Exception as e:
logger.warning("speech-rate attempt %d failed: %s", attempt, e)
return {"text": best[0], "rate_ratio": best[1], "attempts": attempt - 1, "error": str(e)}
except Exception:
logger.warning("speech-rate provider attempt %d failed", attempt)
return {
"text": best[0], "rate_ratio": best[1],
"attempts": attempt - 1, "error": "fit-provider-failed",
}
if next_text and next_text.strip():
candidate = next_text.strip()
+100 -1
View File
@@ -40,6 +40,7 @@ from __future__ import annotations
import atexit
import base64
import contextlib
import json
import logging
import os
@@ -263,6 +264,91 @@ def _ensure_reaper_running() -> None:
# ── Base class ─────────────────────────────────────────────────────────────
#: How often to prove liveness while an engine's venv is being resolved.
#: Matches the sidecar's own cold-load cadence (#1367) so the guarded waiter
#: sees the same rhythm from both steps.
_RESOLVE_HEARTBEAT_S = 5.0
@contextlib.contextmanager
def _heartbeat_while_resolving(engine_id: str):
"""Report progress while a slow engine-venv resolution runs (#1414).
A probe that spawns interpreters, or a bootstrap that installs torch, can
outlast the generate budget on its own. Both are demonstrably *working*
the whole time, so the deadline should extend rather than expire which
is what the execution clock's load heartbeat is for.
Two details that are easy to get wrong:
* **Pool jobs only.** An off-pool caller never runs on a thread the clock
tracks, and heartbeating from one would credit an ident a pool worker
might later reuse up to a grace period of unearned extension, which
is the #1379 lesson.
* **The resolving thread's ident, not the beater's.** The heartbeat runs
on a helper thread so it can tick while resolution blocks, but the job
the clock is watching is the caller's. Capturing the ident up front is
what makes the extension land on the right job.
Never raises: a failed heartbeat must not fail a generation.
"""
try:
from services.model_manager import running_on_gpu_pool
on_pool = running_on_gpu_pool()
except Exception: # noqa: BLE001 — best-effort by construction
on_pool = False
if not on_pool:
yield
return
ident = threading.get_ident()
stop = threading.Event()
def _beat():
try:
from services.model_manager import (
MODEL_LOAD_HEARTBEAT_GRACE_S, _MODEL_LOAD_ACTIVITY,
)
except Exception: # noqa: BLE001
return
while not stop.wait(_RESOLVE_HEARTBEAT_S):
try:
_MODEL_LOAD_ACTIVITY[ident] = (
time.monotonic(), MODEL_LOAD_HEARTBEAT_GRACE_S,
)
except Exception: # noqa: BLE001
return
t = threading.Thread(
target=_beat, name=f"{engine_id}-resolve-heartbeat", daemon=True,
)
t.start()
try:
yield
finally:
stop.set()
# Join, don't just signal. `_beat()` can be past its `stop.wait()` and
# already committed to a write at the moment the flag is set, so
# signalling alone lets that write land at an arbitrary later point.
#
# That matters because of what runs next: `_run_on_gpu_pool`'s `_job`
# pops this ident from `_MODEL_LOAD_ACTIVITY` in its `finally`
# (model_manager.py) precisely so a stale beat cannot vouch for a
# future job — GPU-pool idents are reused. A write arriving after that
# pop resurrects the entry, and the next job scheduled onto this
# worker inherits a heartbeat it never emitted: the wedge detector
# reads it as live progress and keeps extending a job that is stuck.
#
# Joining orders the last write BEFORE the pop, so the pop clears it.
# This must not be bounded: returning while the helper is still alive
# would recreate the late-write race this join closes. The helper's
# only work after ``wait`` is an in-memory mapping assignment guarded
# by its own broad exception handler, so there is no blocking external
# operation to time out here.
t.join()
class SubprocessBackend(TTSBackend):
"""Long-lived sidecar-process TTS backend. Subclasses provide
``venv_python()`` and ``sidecar_script()``; the base class owns
@@ -379,7 +465,20 @@ class SubprocessBackend(TTSBackend):
else:
kwargs["start_new_session"] = True
python_path = str(self.venv_python())
# `venv_python()` resolves the engine's interpreter, and on a cold
# first run that is not cheap: it spawns each candidate to import the
# engine (bounded, but tens of seconds on a slow disk), and if none is
# installed it can run the whole `uv venv` + `uv pip install`
# bootstrap — minutes, by design.
#
# All of that happens on a GPU-pool worker, inside a generate request
# whose execution budget is 300s by default. Nothing along the way
# reported progress, so the budget expired mid-install and the job was
# abandoned and blamed on the machine's compute (#1414). The sidecar's
# own cold load already heartbeats for exactly this reason (#1367);
# resolution is the step before it that never did.
with _heartbeat_while_resolving(self.id):
python_path = str(self.venv_python())
script_path = str(self.sidecar_script())
# #1172 class: validate the interpreter before exec so a broken /
# half-installed engine venv (0-byte or truncated python, dangling
+2 -2
View File
@@ -197,7 +197,7 @@ time, so in production **restart the backend** to apply a change. Default empty
Admin routes — `/system/*` (including `set-env`, **RCE-class**),
`/api/settings/*`, engine install/uninstall, media tools, MCP bindings — sit on
a stricter gate (`require_loopback`, `backend/api/dependencies.py`) than
a stricter gate (`require_admin`, `backend/api/dependencies.py`) than
consumption. On the desktop build they are **true-loopback-only**: no PIN, key,
or trusted network reaches them from another machine.
@@ -263,7 +263,7 @@ same origin.) If you only moved the Vite dev server's port, set
| Code | Meaning | What to do |
|---|---|---|
| **401** | Consumption auth failed — `{"detail": "PIN required"}` or `{"detail": "API key required"}`. | Supply the PIN / key (header, cookie, or query param above). A WebSocket surfaces this as close code **1008**. |
| **403** | `{"detail": "loopback origin required"}` — you reached a **loopback-gated** route (admin: `/system/*`, `/api/settings/*`; or a `require_local` route from outside a trusted network) from a non-loopback origin. | A PIN won't help. Run the request from the box itself; for `require_local` routes add the caller to `OMNIVOICE_TRUSTED_NETWORKS`; for **admin** routes use `OMNIVOICE_SERVER_MODE=1` **and** present the **API key** (the PIN/trusted-network don't reach admin). |
| **403** | Authorization failed: loopback/native access was required, a server-mode mutation lacked the API key, or a native path capability was invalid, expired, or for a different operation. | A PIN cannot grant admin or filesystem access. Run native operations from the desktop app; configure and present the API key for remote server-mode mutations; reopen the native picker if a one-shot capability expired. |
| **429** | **Not an auth failure.** The GPU pool is saturated (admission control) or a model download is rate-limited. Ships with `Retry-After` and `X-VoiceStudio-Retryable: true`. | Back off for `Retry-After` seconds and retry the identical request. |
---
+72 -23
View File
@@ -7,6 +7,7 @@ use std::time::Duration;
use serde::{Deserialize, Serialize};
use tauri::image::Image;
use tauri_plugin_dialog::DialogExt;
use crate::{AppFlags, TrayHandle, DictationShortcutState};
use crate::{TRAY_ICON_DEFAULT, TRAY_ICON_RECORDING};
@@ -21,27 +22,40 @@ struct AuthorizedHostPath {
path: String,
}
#[derive(Serialize)]
pub struct AuthorizedPathSelection {
authorization: String,
path: String,
}
pub fn path_authorization_dir<R: tauri::Runtime>(app: &tauri::AppHandle<R>) -> PathBuf {
crate::setup::resolved_data_dir(app)
.unwrap_or_else(crate::setup::default_data_dir)
.join(".path-authorizations")
}
fn validate_host_path(kind: &str, raw: &str) -> Result<PathBuf, String> {
if !matches!(kind, "models_dir" | "ffmpeg" | "ffprobe" | "dub_export") {
fn validate_host_path(kind: &str, path: PathBuf) -> Result<PathBuf, String> {
if !matches!(
kind,
"models_dir"
| "ffmpeg"
| "ffprobe"
| "dub_export"
| "soni_input"
| "soni_output_dir"
) {
return Err("Unsupported host-path capability".into());
}
if raw.chars().any(|c| c.is_control()) {
if path.to_string_lossy().chars().any(|c| c.is_control()) {
return Err("Path contains invalid control characters".into());
}
if kind == "models_dir" && raw.is_empty() {
if kind == "models_dir" && path.as_os_str().is_empty() {
return Ok(PathBuf::new()); // explicit reset to the platform default
}
let path = PathBuf::from(raw);
if !path.is_absolute() {
return Err("Path must be absolute".into());
}
if kind == "models_dir" {
if matches!(kind, "models_dir" | "soni_output_dir") {
fs::create_dir_all(&path).map_err(|e| format!("Directory is not writable: {e}"))?;
let probe = path.join(".voicestudio-write-test");
fs::write(&probe, b"ok").map_err(|e| format!("Directory is not writable: {e}"))?;
@@ -53,19 +67,29 @@ fn validate_host_path(kind: &str, raw: &str) -> Result<PathBuf, String> {
if !parent.is_dir() {
return Err("Save destination directory does not exist".into());
}
} else if kind == "soni_input" {
if !path.is_file() {
return Err("Selected media input is not a file".into());
}
} else {
if !path.is_file() {
return Err("Selected media tool is not a file".into());
}
let status = crate::tools::no_window(
let output = crate::tools::no_window(
std::process::Command::new(&path)
.arg("-version")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null()),
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped()),
)
.status()
.output()
.map_err(|e| format!("Selected media tool could not run: {e}"))?;
if !status.success() {
let version_text = format!(
"{}{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
)
.to_ascii_lowercase();
if !output.status.success() || !version_text.contains(kind) {
return Err("Selected media tool failed its version check".into());
}
}
@@ -73,12 +97,34 @@ fn validate_host_path(kind: &str, raw: &str) -> Result<PathBuf, String> {
}
#[tauri::command]
pub fn authorize_host_path(
pub async fn authorize_host_path(
app: tauri::AppHandle,
kind: String,
path: String,
) -> Result<String, String> {
let validated = validate_host_path(&kind, path.trim())?;
suggested_name: Option<String>,
reset: Option<bool>,
) -> Result<Option<AuthorizedPathSelection>, String> {
let selected = if kind == "models_dir" && reset.unwrap_or(false) {
Some(PathBuf::new())
} else {
let dialog = app.dialog().file();
let picked = match kind.as_str() {
"models_dir" | "soni_output_dir" => dialog.blocking_pick_folder(),
"ffmpeg" | "ffprobe" | "soni_input" => dialog.blocking_pick_file(),
"dub_export" => {
let mut save = app.dialog().file();
if let Some(name) = suggested_name.as_deref() {
save = save.set_file_name(name);
}
save.blocking_save_file()
}
_ => return Err("Unsupported host-path capability".into()),
};
picked.and_then(|value| value.into_path().ok())
};
let Some(selected) = selected else {
return Ok(None);
};
let validated = validate_host_path(&kind, selected)?;
let mut random = [0_u8; 32];
getrandom::fill(&mut random).map_err(|e| format!("Secure randomness unavailable: {e}"))?;
let token: String = random.iter().map(|b| format!("{b:02x}")).collect();
@@ -104,7 +150,10 @@ pub fn authorize_host_path(
fs::set_permissions(&target, fs::Permissions::from_mode(0o600))
.map_err(|e| format!("Could not protect authorization: {e}"))?;
}
Ok(token)
Ok(Some(AuthorizedPathSelection {
authorization: token,
path: validated.to_string_lossy().into_owned(),
}))
}
#[cfg(test)]
@@ -114,14 +163,14 @@ mod host_path_authorization_tests {
#[test]
fn rejects_unknown_relative_and_control_character_paths() {
assert!(validate_host_path("shell", "/tmp/tool").is_err());
assert!(validate_host_path("models_dir", "relative/models").is_err());
assert!(validate_host_path("models_dir", "/tmp/bad\npath").is_err());
assert!(validate_host_path("shell", PathBuf::from("/tmp/tool")).is_err());
assert!(validate_host_path("models_dir", PathBuf::from("relative/models")).is_err());
assert!(validate_host_path("models_dir", PathBuf::from("/tmp/bad\npath")).is_err());
}
#[test]
fn empty_models_path_is_the_authorized_default_reset() {
assert_eq!(validate_host_path("models_dir", "").unwrap(), PathBuf::new());
assert_eq!(validate_host_path("models_dir", PathBuf::new()).unwrap(), PathBuf::new());
}
#[test]
@@ -129,13 +178,13 @@ mod host_path_authorization_tests {
let parent = std::env::temp_dir();
let destination = parent.join("voicestudio-authorized-export.wav");
assert_eq!(
validate_host_path("dub_export", destination.to_str().unwrap()).unwrap(),
validate_host_path("dub_export", destination.clone()).unwrap(),
destination,
);
assert!(validate_host_path("dub_export", "relative/export.wav").is_err());
assert!(validate_host_path("dub_export", PathBuf::from("relative/export.wav")).is_err());
assert!(validate_host_path(
"dub_export",
parent.join("missing-directory/export.wav").to_str().unwrap(),
parent.join("missing-directory/export.wav"),
)
.is_err());
}
+8 -5
View File
@@ -240,7 +240,9 @@ const STARTUP_GRACE_MS = 120_000;
const RECONCILE_MS = 12_000;
const RECONCILE_INTERVAL_MS = 1000;
export async function apiFetch(path: string, opts: RequestInit = {}): Promise<Response> {
export type ApiFetchOptions = RequestInit & { retryTransport?: boolean };
export async function apiFetch(path: string, opts: ApiFetchOptions = {}): Promise<Response> {
const pin = typeof sessionStorage !== 'undefined' ? sessionStorage.getItem('ov_pin') : null;
const key = _apiKey();
// Only modify the request when a PIN/API key is set, so the default call
@@ -249,9 +251,10 @@ export async function apiFetch(path: string, opts: RequestInit = {}): Promise<Re
const extra: Record<string, string> = {};
if (pin) extra['X-OmniVoice-Pin'] = pin;
if (key) extra['Authorization'] = `Bearer ${key}`;
const { retryTransport = true, ...requestOpts } = opts;
const finalOpts: RequestInit = Object.keys(extra).length
? { ...opts, headers: { ...(opts.headers as Record<string, string>), ...extra } }
: opts;
? { ...requestOpts, headers: { ...(requestOpts.headers as Record<string, string>), ...extra } }
: requestOpts;
const signal = finalOpts.signal as AbortSignal | null | undefined;
let lastDetail = '';
// The shell's last word on the backend. When it still says `ready` after we've
@@ -283,7 +286,7 @@ export async function apiFetch(path: string, opts: RequestInit = {}): Promise<Re
// lets callers distinguish a transport failure from an HTTP error.
if (signal?.aborted || (e as Error)?.name === 'AbortError') throw e;
lastDetail = String((e as Error)?.message || e);
if (attempt < TRANSPORT_RETRY_BACKOFF_MS.length) {
if (retryTransport && attempt < TRANSPORT_RETRY_BACKOFF_MS.length) {
await new Promise((r) => setTimeout(r, TRANSPORT_RETRY_BACKOFF_MS[attempt]));
continue;
}
@@ -292,7 +295,7 @@ export async function apiFetch(path: string, opts: RequestInit = {}): Promise<Re
// import — not 2.9 s). Keep waiting exactly as long as the shell says
// "starting", bounded by STARTUP_GRACE_MS.
const elapsed = Date.now() - startedAt;
if (elapsed < STARTUP_GRACE_MS) {
if (retryTransport && elapsed < STARTUP_GRACE_MS) {
try {
lastStage = await backendLifecycleStage();
} catch {
@@ -272,11 +272,9 @@ export default function AudioToolsPanel() {
try {
const { invoke } = await import('@tauri-apps/api/core');
const tool = path.includes('/ffprobe/') ? 'ffprobe' : 'ffmpeg';
const authorization = await invoke('authorize_host_path', {
kind: tool,
path: body?.path || '',
});
requestBody = { authorization };
const selection = await invoke('authorize_host_path', { kind: tool });
if (!selection) return;
requestBody = { authorization: selection.authorization };
} catch (e) {
toast.error(
t('settings.audio_tools_path_failed', {
@@ -59,7 +59,7 @@ describe('AudioToolsPanel — power-user surface for the media tools', () => {
vi.clearAllMocks();
apiJson.mockResolvedValue(JSON.parse(JSON.stringify(STATUS)));
apiFetch.mockResolvedValue(okResponse);
invoke.mockResolvedValue('c'.repeat(64));
invoke.mockResolvedValue({ authorization: 'c'.repeat(64), path: '/opt/tools/ffmpeg' });
});
it('renders one row per tool with version, path, and origin badge', async () => {
@@ -98,7 +98,6 @@ describe('AudioToolsPanel — power-user surface for the media tools', () => {
await waitFor(() =>
expect(invoke).toHaveBeenCalledWith('authorize_host_path', {
kind: 'ffmpeg',
path: '/opt/tools/ffmpeg',
}),
);
expect(apiFetch).toHaveBeenCalledWith(
@@ -48,16 +48,17 @@ export default function StoragePanel() {
refresh();
}, [refresh]);
const save = async (path) => {
const save = async (reset = false) => {
setSaving(true);
setError(null);
try {
const { invoke } = await import('@tauri-apps/api/core');
const authorization = await invoke('authorize_host_path', { kind: 'models_dir', path });
const selection = await invoke('authorize_host_path', { kind: 'models_dir', reset });
if (!selection) return;
const res = await apiFetch('/api/settings/storage/models-dir', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ authorization }),
body: JSON.stringify({ authorization: selection.authorization }),
});
if (!res.ok) {
const b = await res.json().catch(() => ({}));
@@ -67,7 +68,7 @@ export default function StoragePanel() {
setConfigured(b?.configured || '');
setRestart(Boolean(b?.restart_required));
toast.success(
path
selection.path
? 'Models directory saved — restart to apply'
: 'Reverted to default — restart to apply',
);
@@ -116,7 +117,7 @@ export default function StoragePanel() {
type="text"
value={input}
placeholder={def || '~/.cache/huggingface'}
onChange={(e) => setInput(e.target.value)}
readOnly
disabled={saving || loading}
spellCheck={false}
aria-label="Models directory"
@@ -124,7 +125,7 @@ export default function StoragePanel() {
/>
<button
className="flex-none cursor-pointer rounded-[var(--chrome-radius-pill)] [border:1px_solid_transparent] bg-[var(--chrome-accent)] px-[var(--space-4)] py-[var(--space-2)] font-sans text-[length:var(--text-base)] text-[var(--chrome-bg)] disabled:cursor-default disabled:opacity-50"
onClick={() => save(input.trim())}
onClick={() => save(false)}
disabled={saving || loading}
data-testid="models-dir-save"
>
@@ -133,8 +134,7 @@ export default function StoragePanel() {
<button
className="flex-none cursor-pointer rounded-[var(--chrome-radius-pill)] [border:1px_solid_var(--chrome-border)] bg-transparent px-[var(--space-4)] py-[var(--space-2)] font-sans text-[length:var(--text-base)] text-[var(--chrome-fg-muted)] hover:enabled:bg-[var(--chrome-hover-bg)] hover:enabled:text-[var(--chrome-fg)] disabled:cursor-default disabled:opacity-50"
onClick={() => {
setInput('');
save('');
save(true);
}}
disabled={saving || loading || !configured}
title="Revert to the default cache location"
@@ -21,17 +21,16 @@ describe('StoragePanel native path boundary', () => {
ok: true,
json: async () => ({ configured: '/private/models', restart_required: true }),
});
invoke.mockResolvedValue('d'.repeat(64));
invoke.mockResolvedValue({ authorization: 'd'.repeat(64), path: '/private/models' });
render(<StoragePanel />);
const input = await screen.findByTestId('models-dir-input');
fireEvent.change(input, { target: { value: '/private/models' } });
await screen.findByTestId('models-dir-input');
fireEvent.click(screen.getByTestId('models-dir-save'));
await waitFor(() =>
expect(invoke).toHaveBeenCalledWith('authorize_host_path', {
kind: 'models_dir',
path: '/private/models',
reset: false,
}),
);
expect(apiFetch).toHaveBeenCalledWith(
+13
View File
@@ -39,6 +39,19 @@ describe('apiFetch transport-retry', () => {
expect(fetchMock).toHaveBeenCalledTimes(1);
});
it('can disable transport retries for one-shot capability requests', async () => {
const fetchMock = vi.fn().mockRejectedValue(new TypeError('Failed to fetch'));
vi.stubGlobal('fetch', fetchMock);
await expect(apiFetch('/one-shot', { retryTransport: false })).rejects.toMatchObject({
status: 0,
});
const transportCalls = fetchMock.mock.calls.filter((call) =>
String(call[0]).endsWith('/one-shot'),
);
expect(transportCalls).toHaveLength(1);
});
it('does NOT call fetch once the signal is already aborted', async () => {
const fetchMock = vi.fn().mockRejectedValue(new TypeError('Failed to fetch'));
vi.stubGlobal('fetch', fetchMock);
+27 -22
View File
@@ -84,6 +84,33 @@ export async function downloadMedia(url, fallbackName, opts = {}) {
// ── Tauri: native save dialog + server-side copy ────────────────────────
if (isTauri) {
try {
// Dynamic dub saves are selected inside the native command. A webview
// path is never treated as filesystem authority.
if (!sourceFilename && !['srt', 'vtt'].includes(extGuess)) {
const { invoke } = await import('@tauri-apps/api/core');
const selection = await invoke('authorize_host_path', {
kind: 'dub_export',
suggestedName: fallbackName,
});
if (!selection) return;
toast.loading(i18n.t('app.toast_saving', { name: fallbackName }), { id: fallbackName });
const res = await apiFetch(url, {
headers: { 'X-VoiceStudio-Path-Authorization': selection.authorization },
retryTransport: false,
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const ctype = res.headers.get('content-type') || '';
if (!ctype.includes('application/json')) {
throw new Error(
`Server returned ${ctype || 'an unknown content type'} instead of a JSON save confirmation`,
);
}
const data = await res.json();
toast.success(i18n.t('app.toast_saved', { path: data.path }), { id: fallbackName });
onValueMoment?.();
await recordHistory(data.display_name || fallbackName, data.path);
return;
}
const { save } = await import('@tauri-apps/plugin-dialog');
const destPath = await save({
defaultPath: fallbackName,
@@ -120,28 +147,6 @@ export async function downloadMedia(url, fallbackName, opts = {}) {
return;
}
// (c) Dynamic dub endpoint: bind the native picker result to a one-shot
// capability. The host path never enters an HTTP query or body. Guard the content-type so a
// raw-body response surfaces a clear error, not a JSON.parse crash (#309).
const { invoke } = await import('@tauri-apps/api/core');
const authorization = await invoke('authorize_host_path', {
kind: 'dub_export',
path: destPath,
});
const res = await apiFetch(url, {
headers: { 'X-VoiceStudio-Path-Authorization': authorization },
});
if (!res.ok) throw new Error(`HTTP ${res.status}`); // a 4xx/5xx isn't a successful save
const ctype = res.headers.get('content-type') || '';
if (!ctype.includes('application/json')) {
throw new Error(
`Server returned ${ctype || 'an unknown content type'} instead of a JSON save confirmation`,
);
}
const data = await res.json();
toast.success(i18n.t('app.toast_saved', { path: data.path }), { id: fallbackName });
onValueMoment?.();
await recordHistory(data.display_name || fallbackName, data.path);
} catch (err) {
console.error(err);
toast.error(i18n.t('app.toast_save_error', { message: err.message }), { id: fallbackName });
+9 -4
View File
@@ -99,7 +99,6 @@ describe('downloadMedia — Tauri branch (isTauri=true)', () => {
it('dynamic endpoint uses a one-shot native path authorization and records history', async () => {
const downloadMedia = await loadDownloadMedia({ tauri: true });
save.mockResolvedValueOnce('/Users/me/Movies/dubbed_video.mp4');
apiFetch.mockResolvedValueOnce({
ok: true,
headers: { get: () => 'application/json' },
@@ -108,7 +107,10 @@ describe('downloadMedia — Tauri branch (isTauri=true)', () => {
display_name: 'dubbed_video.mp4',
}),
});
invoke.mockResolvedValueOnce('a'.repeat(64));
invoke.mockResolvedValueOnce({
authorization: 'a'.repeat(64),
path: '/Users/me/Movies/dubbed_video.mp4',
});
await downloadMedia(
'http://x/dub/download/job/dubbed_video.mp4?preserve_bg=1',
@@ -117,11 +119,14 @@ describe('downloadMedia — Tauri branch (isTauri=true)', () => {
expect(invoke).toHaveBeenCalledWith('authorize_host_path', {
kind: 'dub_export',
path: '/Users/me/Movies/dubbed_video.mp4',
suggestedName: 'dubbed_video.mp4',
});
expect(apiFetch).toHaveBeenCalledWith(
'http://x/dub/download/job/dubbed_video.mp4?preserve_bg=1',
{ headers: { 'X-VoiceStudio-Path-Authorization': 'a'.repeat(64) } },
{
headers: { 'X-VoiceStudio-Path-Authorization': 'a'.repeat(64) },
retryTransport: false,
},
);
expect(apiFetch.mock.calls[0][0]).not.toContain('save_path=');
expect(exportAction).not.toHaveBeenCalled(); // dynamic endpoint copies itself
+6 -1
View File
@@ -108,7 +108,12 @@ def sanitize_instruct(raw):
"""
if not raw:
return ""
return _valid_instruct_from_items(re.split(r"\s*[,]\s*", str(raw).strip()))
# Strip each comma-delimited item after splitting. A pattern with ``\s*``
# on both sides of the delimiter backtracks quadratically when a poisoned
# stored value contains a long whitespace run without a comma (GHAS #778).
return _valid_instruct_from_items(
item.strip() for item in re.split(r"[,]", str(raw).strip())
)
def instruct_from_vd_states(vd_states):
@@ -20,6 +20,7 @@ from __future__ import annotations
import re
import sys
from time import perf_counter
import pytest
@@ -420,6 +421,45 @@ def test_select_mlx_audio_raw_repo_id_accepted(fresh_app, monkeypatch):
assert _prefs.get("mlx_audio_model_id") == "mlx-community/Some-Other-Model-4bit"
def test_select_mlx_audio_repo_id_accepts_underscore_prefixes(fresh_app, monkeypatch):
_make_mlx_audio_available(monkeypatch)
r = _client(fresh_app).post(
"/engines/select",
json={
"family": "tts", "backend_id": "mlx-audio",
"model_id": "_owner/_repo",
},
)
assert r.status_code == 200, r.text
@pytest.mark.parametrize(
"model_id",
[
"owner/repo/extra",
"-owner/repo",
"owner/.repo",
"owner/repo--name",
"owner/repo..name",
"owner/repo.",
"owner/repo.git",
f"owner/{'a' * 97}",
"-" * 100_000,
],
)
def test_select_mlx_audio_rejects_malformed_repo_ids(fresh_app, monkeypatch, model_id):
_make_mlx_audio_available(monkeypatch)
started = perf_counter()
r = _client(fresh_app).post(
"/engines/select",
json={"family": "tts", "backend_id": "mlx-audio", "model_id": model_id},
)
assert r.status_code == 400
assert perf_counter() - started < 0.5
assert len(r.content) < 256
assert model_id[:100] not in r.text
def test_select_mlx_audio_without_model_id_does_not_touch_pref(fresh_app, monkeypatch):
"""Selecting mlx-audio without a model_id (e.g. an older frontend) must
leave any existing mlx_audio_model_id pref untouched."""
+2 -1
View File
@@ -366,7 +366,8 @@ class TestEndpoints:
_offline_asr_missing(cached=True):
r = client.get("/dub/transcribe-stream/j4")
assert r.status_code == 200
assert "audio load failed" in r.text
assert "Transcription failed. Check the selected ASR engine and try again." in r.text
assert str(wav) not in r.text
backend.unload.assert_called_once()
+126
View File
@@ -0,0 +1,126 @@
"""Three digits in a path are not an authentication failure.
`classify()` matched a bare ``"401"`` substring, and used it to satisfy *both*
halves of the HF-auth condition so any message containing those digits
anywhere classified as ``HF_AUTH_FAILED`` on its own. Paths, byte counts, job
ids and durations all qualify.
It surfaced in CI when pytest's numbered temp directory reached
``pytest-401``: an audio-save failure came back telling the user to set a
valid ``HF_TOKEN``. That is worse than an unclassified error it is a
confident wrong instruction, attached to a docs deeplink, in an auto-filed bug
report. And because it depends on a counter that changes between runs, it is
the kind of bug that passes locally forever.
Two independent guards now: the digits must be a standalone token, and they
are no longer sufficient evidence by themselves.
"""
from __future__ import annotations
import pytest
@pytest.fixture
def classify():
from core.failure import classify as _classify
return _classify
# ── the digits alone must never classify ──────────────────────────────────
NOT_AUTH = [
# The exact CI failure.
"Error opening '/tmp/pytest-of-runner/pytest-401/t0/speech.wav': System error.",
"wrote 4012 bytes to disk",
"job 1401 failed to start",
"sample rate 44100, offset 401",
"/home/user/Music/401 tracks/out.wav could not be written",
"took 2401ms",
]
@pytest.mark.parametrize("text", NOT_AUTH)
def test_a_number_containing_401_is_not_an_auth_failure(classify, text):
assert classify(text) != "HF_AUTH_FAILED"
def test_the_ci_failure_keeps_its_own_class(classify):
"""The scratch counter must not steal the exact audio-write class."""
ordinary = (
"Writing the audio file failed: RuntimeError: System error. — target "
"/tmp/pytest-of-runner/pytest-3/t0/speech.wav"
)
unlucky = (
"Writing the audio file failed: RuntimeError: System error. — target "
"/tmp/pytest-of-runner/pytest-401/t0/speech.wav"
)
assert classify(ordinary) == "AUDIO_IO_FAILED"
assert classify(unlucky) == "AUDIO_IO_FAILED"
# ── real auth failures still classify ─────────────────────────────────────
IS_AUTH = [
"401 Client Error: Unauthorized for url: https://huggingface.co/api/models/x",
"Invalid credentials in Authorization header (huggingface.co)",
"huggingface.co returned 401",
"hf_token is invalid or expired",
"Unauthorized: your token does not have access to this repo",
]
@pytest.mark.parametrize("text", IS_AUTH)
def test_a_real_auth_failure_still_classifies(classify, text):
assert classify(text) == "HF_AUTH_FAILED"
# ── the token boundary, independently ─────────────────────────────────────
@pytest.mark.parametrize(
"text,expected",
[
("huggingface.co status 401", True),
("huggingface.co status 4012", False),
("huggingface.co status 1401", False),
("huggingface.co (401)", True),
("huggingface.co HTTP/1.1 401", True),
# A trailing sentence full stop is punctuation, not a decimal point.
("huggingface.co status 401.", True),
# Digit-only boundaries accepted all of these (CodeRabbit, #1427).
# Each is a different family, and each needs its own guard:
# identifier context, hyphenated context, and dotted numerics.
("huggingface.co x401y", False),
("huggingface.co pytest-401", False),
("huggingface.co 401.0", False),
("huggingface.co 401.25", False),
("huggingface.co v1.401", False),
("huggingface.co 401k", False),
("huggingface.co run-401-retry", False),
("huggingface.co /tmp/pytest-of-runner/pytest-401/speech.wav", False),
("huggingface.co clip401.wav", False),
],
)
def test_401_is_matched_as_a_whole_number(text, expected):
from core.failure import _HTTP_401
assert bool(_HTTP_401.search(text)) is expected
@pytest.mark.parametrize(
"text",
[
# The residual the digit-only boundary left open: an HF marker in the
# same message meant any of these still reached HF_AUTH_FAILED, because
# the subject half was satisfied by "huggingface" and the symptom half
# by the stray digits.
"huggingface.co upload failed writing /tmp/pytest-401/chunk.bin",
"huggingface.co cache entry clip401.wav could not be opened",
"huggingface.co model v1.401 is not available",
],
)
def test_an_hf_marker_does_not_make_stray_digits_an_auth_failure(classify, text):
assert classify(text) != "HF_AUTH_FAILED", (
"a message that merely mentions Hugging Face and happens to contain "
"401 elsewhere was given the 'set a valid HF_TOKEN' remedy"
)
+76 -4
View File
@@ -160,7 +160,9 @@ def test_transcribe_stream_surfaces_model_load_failure(tmp_path, monkeypatch):
assert "CUDA driver init failed: simulated" in body, body
def test_transcribe_stream_never_closes_without_terminal_event(tmp_path, monkeypatch):
def test_transcribe_stream_never_closes_without_terminal_event(
tmp_path, monkeypatch, caplog
):
"""Regression #516: an unanticipated exception INSIDE the stream body (one
that escapes the per-chunk handler, e.g. segmentation blowing up) must still
end the stream with a terminal `error` then `done` never a silent
@@ -203,7 +205,7 @@ def test_transcribe_stream_never_closes_without_terminal_event(tmp_path, monkeyp
# Make the post-chunk segmentation (outside the per-chunk try/except) blow
# up — the exact class of "unanticipated escape" the guard must catch.
def _boom_segment(*a, **k):
raise RuntimeError("segmentation exploded: simulated")
raise RuntimeError("API_KEY=dub-secret /home/alice/private-video.mp4")
monkeypatch.setattr(dc, "segment_transcript", _boom_segment)
# Don't touch the GPU/TTS during the test.
monkeypatch.setattr(dc, "offload_tts_for_asr", lambda *a, **k: None)
@@ -222,12 +224,79 @@ def test_transcribe_stream_never_closes_without_terminal_event(tmp_path, monkeyp
# The stream must end with a terminal error followed by done.
assert "event: error" in body, body
assert "segmentation exploded: simulated" in body, body
assert "transcription_failed" in body, body
assert "Transcription failed. Check the selected ASR engine and try again." in body, body
assert "dub-secret" not in body, body
assert "Traceback" not in body, body
assert "dub-secret" not in caplog.text
assert "/home/alice/private-video.mp4" not in caplog.text
err_idx = body.rfind("event: error")
done_idx = body.rfind("event: done")
assert done_idx > err_idx >= 0, f"error must precede the terminal done: {body}"
def test_transcribe_chunk_failure_uses_stable_public_metadata(
tmp_path, monkeypatch, caplog
):
"""Inner ASR failures must not serialize provider secrets or local paths."""
import asyncio
from api.routers import dub_core as dc
job_id = "t_chunk_secret"
audio = tmp_path / "a.wav"
_make_wav(audio, seconds=1.0)
dc._dub_jobs[job_id] = {
"audio_path": str(audio), "vocals_path": None, "scene_cuts": [],
}
fake_model = MagicMock()
fake_model._asr_pipe = MagicMock()
async def _ok_model():
return fake_model
class _FailingASR:
id = "fake"
def ensure_loaded(self):
pass
def transcribe(self, path, *, word_timestamps=True):
raise RuntimeError("TOKEN=chunk-secret /home/alice/private-audio.wav")
def unload(self):
pass
monkeypatch.setattr(dc, "get_model", _ok_model)
monkeypatch.setattr(dc, "_CHUNK_TRANSCRIBE_ATTEMPTS", 1)
monkeypatch.setattr(dc, "offload_tts_for_asr", lambda *a, **k: None)
monkeypatch.setattr(
"services.asr_backend.get_active_asr_backend",
lambda *a, **k: _FailingASR(),
)
async def _collect():
resp = await dc.dub_transcribe_stream(job_id)
parts = []
async for chunk in resp.body_iterator:
parts.append(
chunk.decode() if isinstance(chunk, (bytes, bytearray)) else str(chunk)
)
return "".join(parts)
try:
body = asyncio.run(_collect())
finally:
dc._dub_jobs.pop(job_id, None)
assert "transcription_failed" in body, body
assert "Transcription failed. Check the selected ASR engine and try again." in body
assert "chunk-secret" not in body
assert "/home/alice/private-audio.wav" not in body
assert "chunk-secret" not in caplog.text
assert "/home/alice/private-audio.wav" not in caplog.text
def test_transcribe_stream_surfaces_asr_load_failure_at_preflight(tmp_path, monkeypatch):
"""Regression #578: the reported failure mode is the *ASR model* failing to
load (WhisperX: faster-whisper weights / CTranslate2-cuDNN mismatch / the
@@ -334,7 +403,10 @@ def test_transcribe_stream_preflight_crash_is_a_structured_error(monkeypatch):
body = asyncio.run(_collect())
assert "event: error" in body, body
assert "job store exploded: simulated" in body, body
assert "transcription_failed" in body, body
assert "Transcription failed. Check the selected ASR engine and try again." in body, body
assert "job store exploded: simulated" not in body, body
assert "Traceback" not in body, body
err_idx = body.rfind("event: error")
done_idx = body.rfind("event: done")
assert done_idx > err_idx >= 0, f"error must precede the terminal done: {body}"
+83 -6
View File
@@ -9,8 +9,6 @@ from contextlib import contextmanager
from types import SimpleNamespace
import pytest
from api.dependencies import require_native_access
from core.path_security import UnsafePath, resolve_within, safe_filename
from fastapi import HTTPException
@@ -20,11 +18,13 @@ def _request(host: str | None):
@pytest.mark.parametrize("host", ["127.0.0.1", "::1", "localhost"])
def test_native_filesystem_capabilities_allow_true_loopback(host):
from api.dependencies import require_native_access
require_native_access(_request(host))
@pytest.mark.parametrize("host", ["172.17.0.1", "192.168.1.4", None])
def test_native_filesystem_capabilities_reject_remote_even_in_server_mode(monkeypatch, host):
from api.dependencies import require_native_access
monkeypatch.setenv("OMNIVOICE_SERVER_MODE", "1")
monkeypatch.setenv("OMNIVOICE_API_KEY", "operator-secret")
with pytest.raises(HTTPException) as exc:
@@ -37,11 +37,13 @@ def test_native_filesystem_capabilities_reject_remote_even_in_server_mode(monkey
["../secret.wav", "folder/voice.wav", r"folder\voice.wav", r"C:\secret.wav", ".", "..", ""],
)
def test_safe_filename_rejects_posix_and_windows_escapes(name):
from core.path_security import UnsafePath, safe_filename
with pytest.raises(UnsafePath):
safe_filename(name)
def test_resolve_within_accepts_relative_and_existing_absolute_paths(tmp_path):
from core.path_security import resolve_within
root = tmp_path / "root"
root.mkdir()
item = root / "voice.wav"
@@ -50,6 +52,7 @@ def test_resolve_within_accepts_relative_and_existing_absolute_paths(tmp_path):
def test_resolve_within_rejects_parent_and_absolute_escape(tmp_path):
from core.path_security import UnsafePath, resolve_within
root = tmp_path / "root"
root.mkdir()
with pytest.raises(UnsafePath):
@@ -63,11 +66,15 @@ def test_resolve_within_rejects_parent_and_absolute_escape(tmp_path):
def test_resolve_within_rejects_symlink_escape(tmp_path):
from core.path_security import UnsafePath, resolve_within
root = tmp_path / "root"
outside = tmp_path / "outside"
root.mkdir()
outside.mkdir()
(root / "link").symlink_to(outside, target_is_directory=True)
try:
(root / "link").symlink_to(outside, target_is_directory=True)
except OSError:
pytest.skip("symlink creation is unavailable on this host")
with pytest.raises(UnsafePath):
resolve_within(root, "link/secret.wav")
@@ -90,7 +97,9 @@ def test_marketplace_db_asset_cannot_escape_voices(tmp_path, monkeypatch):
secret = tmp_path / "secret.wav"
secret.write_bytes(b"secret")
monkeypatch.setattr(marketplace, "VOICES_DIR", str(voices))
assert marketplace._voice_asset(secret) is None
with pytest.raises(HTTPException) as exc:
marketplace._voice_asset(secret)
assert exc.value.status_code == 400
def test_profile_lock_rejects_history_path_outside_outputs(tmp_path, monkeypatch):
@@ -132,7 +141,10 @@ def test_dub_artifact_rejects_db_path_and_symlink_escapes(tmp_path, monkeypatch)
root.mkdir()
outside.mkdir()
(outside / "secret.wav").write_bytes(b"secret")
(root / "link").symlink_to(outside, target_is_directory=True)
try:
(root / "link").symlink_to(outside, target_is_directory=True)
except OSError:
pytest.skip("symlink creation is unavailable on this host")
monkeypatch.setattr(dub_export, "DUB_DIR", str(root))
for value in (outside / "secret.wav", root / "link" / "secret.wav"):
@@ -164,7 +176,10 @@ def test_dub_artifact_rebase_rejects_unanchored_traversal_and_symlink(tmp_path,
current.mkdir(parents=True)
outside.mkdir()
(outside / "secret.wav").write_bytes(b"secret")
(current / "job_123").symlink_to(outside, target_is_directory=True)
try:
(current / "job_123").symlink_to(outside, target_is_directory=True)
except OSError:
pytest.skip("symlink creation is unavailable on this host")
monkeypatch.setattr(dub_export, "DUB_DIR", str(current))
rejected = [
@@ -211,3 +226,65 @@ def test_dub_routes_never_accept_an_http_destination_path():
parameters = inspect.signature(route).parameters
assert "save_path" not in parameters
assert "save_authorization" in parameters
def test_dub_authorization_survives_validation_failure(tmp_path, monkeypatch):
"""A one-shot save token is consumed only when an artifact is ready to write."""
from api.routers import dub_export
from core import path_authorization
from fastapi.testclient import TestClient
from main import app
auth_dir = tmp_path / "authorizations"
auth_dir.mkdir()
monkeypatch.setattr(path_authorization, "_AUTH_DIR", str(auth_dir))
monkeypatch.setattr(dub_export, "_get_job", lambda _job_id: None)
token = "b" * 64
capability = auth_dir / f"{token}.json"
capability.write_text(
json.dumps({"token": token, "kind": "dub_export", "path": str(tmp_path / "out.wav")}),
encoding="utf-8",
)
response = TestClient(app, client=("127.0.0.1", 50000)).get(
"/dub/download/missing-job",
headers={"X-VoiceStudio-Path-Authorization": token},
)
assert response.status_code == 404
assert capability.is_file()
def test_soni_dub_accepts_capabilities_not_raw_paths(monkeypatch):
from api.routers import sonitranslate
from core import path_authorization
from pydantic import ValidationError
with pytest.raises(ValidationError):
sonitranslate.DubRequest(video_path="/tmp/input.mp4")
consumed = []
monkeypatch.setattr(
path_authorization,
"consume",
lambda token, kind: consumed.append((token, kind)) or f"/authorized/{kind}",
)
monkeypatch.setattr(
sonitranslate.soni,
"dub_video",
lambda **_kwargs: None,
)
async def fake_dub_video(**kwargs):
return kwargs
monkeypatch.setattr(sonitranslate.soni, "dub_video", fake_dub_video)
body = sonitranslate.DubRequest(
video_authorization="c" * 64,
output_authorization="d" * 64,
)
result = asyncio.run(sonitranslate.sonitranslate_dub(body))
assert result["video_path"] == "/authorized/soni_input"
assert result["output_dir"] == "/authorized/soni_output_dir"
assert consumed == [
("c" * 64, "soni_input"),
("d" * 64, "soni_output_dir"),
]
+13 -4
View File
@@ -111,7 +111,9 @@ def _make_deterministic_engine(engine_id="stream-fake", *, delay_s=0.0,
def generate(self, text, **kw) -> torch.Tensor:
type(self).calls.append((text, time.monotonic()))
if fail_on_call is not None and len(type(self).calls) == fail_on_call:
raise RuntimeError("engine exploded mid-stream (test)")
raise RuntimeError(
"TOKEN=stream-secret /home/alice/private-reference.wav"
)
if delay_s:
time.sleep(delay_s)
# Deterministic, text-dependent waveform (crc-seeded sine-ish ramp).
@@ -277,8 +279,9 @@ def test_stream_final_file_identical_to_classic_output(client, monkeypatch,
assert done["duration"] > 0
def test_stream_midstream_error_yields_error_event(client, monkeypatch,
no_omnivoice_model):
def test_stream_midstream_error_yields_error_event(
client, monkeypatch, no_omnivoice_model, caplog
):
"""Chunk 2 blowing up must surface as an in-band error event AFTER the
already-delivered chunk no done, no history row, no saved file."""
fake = _make_deterministic_engine(fail_on_call=2)
@@ -294,7 +297,13 @@ def test_stream_midstream_error_yields_error_event(client, monkeypatch,
assert "chunk" in types # chunk 0 was delivered before the crash
assert types[-1] == "error"
assert "done" not in types
assert events[-1][0]["detail"] # actionable message for the fallback log
error = events[-1][0]
assert error["code"] == "generation_failed"
assert error["detail"] == "Generation failed. Check the selected engine and try again."
assert "stream-secret" not in repr(error)
assert "Traceback" not in repr(error)
assert "stream-secret" not in caplog.text
assert "/home/alice/private-reference.wav" not in caplog.text
after_ids = {h["id"] for h in client.get("/history").json()}
assert after_ids == before_ids # nothing was recorded for the failure
@@ -0,0 +1,161 @@
"""Regression evidence for reviewed GHAS false positives.
These tests intentionally inspect the narrow security invariants at the
reported sinks. They keep future refactors from making a dismissed alert
silently become exploitable while avoiding heavyweight model imports.
"""
from __future__ import annotations
import ast
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
def _source(path: str) -> str:
return (ROOT / path).read_text(encoding="utf-8")
def _function(path: str, name: str) -> tuple[ast.FunctionDef | ast.AsyncFunctionDef, str]:
source = _source(path)
tree = ast.parse(source)
node = next(
item
for item in ast.walk(tree)
if isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)) and item.name == name
)
return node, ast.get_source_segment(source, node) or ""
def test_dynamic_updates_only_interpolate_allowlisted_columns_and_placeholders():
cases = (
(
"backend/api/routers/glossary.py",
"update_term",
'("source", "target", "note")',
"UPDATE glossary_terms",
),
(
"backend/api/routers/profiles.py",
"update_profile",
'("name", "ref_text", "instruct", "language", "personality")',
"UPDATE voice_profiles",
),
)
for path, function, allowlist, table in cases:
_, body = _function(path, function)
assert allowlist in body
assert 'fields.append(f"{col} = ?")' in body
assert table in body
assert "params" in body
# User values and IDs are never formatted into the statement.
assert "{val}" not in body
assert "{profile_id}" not in body
assert "{term_id}" not in body
assert "{project_id}" not in body
def test_history_reference_query_varies_only_placeholder_arity():
_, body = _function(
"backend/api/routers/generation.py", "_remove_wav_if_unreferenced"
)
assert 'placeholders = ",".join("?" for _ in exclude_ids)' in body
assert 'f" AND id NOT IN ({placeholders})"' in body
assert "(audio_path, *exclude_ids)" in body
assert "{audio_path}" not in body
assert "{exclude_ids}" not in body
def test_media_download_sink_requires_https_size_and_sha256():
_, body = _function("backend/services/media_tools.py", "_download")
assert 'if not url.startswith("https://")' in body
assert "done != expected_size" in body
assert "digest != expected_sha256" in body
assert body.index('url.startswith("https://")') < body.index("urlopen(")
def test_pypi_metadata_and_wheel_digest_reach_the_verified_download_sink():
source = _source("backend/services/media_tools.py")
assert '_PYPI_YTDLP_URL = "https://pypi.org/pypi/yt-dlp/json"' in source
_, fetch = _function("backend/services/media_tools.py", "_fetch_pypi_ytdlp")
assert 'artifact["url"]' in fetch
assert 'artifact["digests"]["sha256"]' in fetch
_, update = _function("backend/services/media_tools.py", "_do_update_ytdlp")
assert "version, url, sha = _fetch_pypi_ytdlp()" in update
assert '_download(url, whl, sha, None, op="ytdlp_update")' in update
def test_diagnostic_probe_is_a_guarded_constant_https_head_request():
source = _source("backend/core/diagnose.py")
assert '_HUB_URL = "https://huggingface.co"' in source
_, body = _function("backend/core/diagnose.py", "_check_network")
assert 'if not _HUB_URL.startswith("https://")' in body
assert 'Request(_HUB_URL, method="HEAD")' in body
assert body.index('_HUB_URL.startswith("https://")') < body.index("urlopen(")
def test_health_check_url_and_server_are_both_pinned_to_loopback():
source = _source("backend/main.py")
assert 'HEALTH_URL = f"http://127.0.0.1:{_port}/health"' in source
assert 'uvicorn.run(app, host="127.0.0.1", port=_port' in source
assert "_port = network_share.backend_port()" in source
def test_huggingface_cache_probe_is_forced_offline():
_, body = _function("backend/services/model_manager.py", "_checkpoint_in_local_cache")
assert "snapshot_download(checkpoint, local_files_only=True)" in body
assert "local_files_only=False" not in body
def test_pep562_exports_are_backed_by_lazy_attribute_resolvers():
package = _source("omnivoice/__init__.py")
assert '__all__ = ["OmniVoice", "OmniVoiceConfig", "OmniVoiceGenerationConfig"]' in package
assert "def __getattr__(name):" in package
assert "if name in __all__:" in package
assert "return getattr(_m, name)" in package
backend = _source("backend/engines/omnivoice_gguf/backend.py")
assert '"OmniVoiceGGUFBackend",' in backend
assert 'if name == "OmniVoiceGGUFBackend":' in backend
assert "return _make_backend_class()" in backend
def test_secret_error_logs_never_include_plaintext_or_ciphertext_variables():
_, body = _function("backend/services/settings_store.py", "get_secret")
tree = ast.parse(body)
log_calls = [
call
for call in ast.walk(tree)
if isinstance(call, ast.Call)
and isinstance(call.func, ast.Attribute)
and call.func.attr in {"error", "warning", "exception"}
]
assert len(log_calls) == 3
for call in log_calls:
argument_names = {
node.id for arg in call.args[1:] for node in ast.walk(arg) if isinstance(node, ast.Name)
}
assert argument_names <= {"name"}
rendered = ast.unparse(call)
assert "row" not in rendered
assert "key" not in rendered
def test_dataset_script_handles_are_closed_by_outer_finally_blocks():
cases = (
("omnivoice/scripts/denoise_audio.py", "main"),
("omnivoice/scripts/extract_audio_tokens.py", "main"),
("omnivoice/scripts/extract_audio_tokens_add_noise.py", "main"),
)
for path, function in cases:
_, body = _function(path, function)
assert "tar_writer = None" in body
assert "jsonl_file = None" in body
assert "finally:" in body
finally_body = body.rsplit("finally:", 1)[1]
assert "if tar_writer is not None:" in finally_body
assert "tar_writer.close()" in finally_body
assert "if jsonl_file is not None:" in finally_body
assert "jsonl_file.close()" in finally_body
+14 -1
View File
@@ -179,7 +179,9 @@ def test_probe_failure_detail_is_scrubbed(settings_mod, monkeypatch):
"boom key=gsk-test-123 at /Users/someone/secret"))
body = settings_mod.test_llm_provider("groq")
assert body["ok"] is False
assert "gsk-test-123" not in body["detail"]
assert body["detail"] == "The provider request failed. Try again."
assert "gsk-test-123" not in repr(body)
assert "/Users/someone" not in repr(body)
# ── /models discovery ───────────────────────────────────────────────────────
@@ -204,6 +206,17 @@ def test_models_failure_is_classified(settings_mod, monkeypatch):
_fake_openai(monkeypatch, raise_exc=exc)
body = settings_mod.list_llm_provider_models("groq")
assert body["ok"] is False and body["kind"] == "auth" and body["models"] == []
assert body["detail"] == "Authentication failed. Check the provider API key."
def test_models_failure_omits_trace_path_and_secret(settings_mod, monkeypatch):
_configure_groq(settings_mod)
private = "Traceback: key=gsk-test-123 at /home/alice/provider.py"
_fake_openai(monkeypatch, raise_exc=RuntimeError(private))
body = settings_mod.list_llm_provider_models("groq")
assert body["kind"] == "error"
assert body["detail"] == "The provider request failed. Try again."
assert private not in repr(body)
def test_models_not_truncated_under_cap(settings_mod, monkeypatch):
+36
View File
@@ -268,6 +268,24 @@ def test_disabled_direction_parse_uses_heuristic(skills, store, monkeypatch):
assert d.tokens.get("energy") == ["urgent"] # heuristic still delivers
def test_direction_failure_returns_stable_error(skills, store, monkeypatch, caplog):
_activate_groq(store)
from services import director
private = "Traceback: token=private-value at /home/alice/director.py"
class _Fake:
id = "openai-compat"
def chat(self, **kw):
raise RuntimeError(private)
monkeypatch.setattr(director, "get_active_llm_backend", lambda: _Fake())
d = director.parse("urgent and surprised")
assert d.error == "llm-parse-failed"
assert private not in repr(d)
assert private not in caplog.text
def test_disabled_slot_fitting_returns_no_llm_marker(skills, store, monkeypatch):
_activate_groq(store)
from services import speech_rate
@@ -287,6 +305,24 @@ def test_disabled_slot_fitting_returns_no_llm_marker(skills, store, monkeypatch)
assert res["error"] == "no-llm" and res["text"] == long_text
def test_slot_fit_failure_returns_stable_error(skills, store, monkeypatch, caplog):
_activate_groq(store)
from services import speech_rate
private = "Traceback: token=private-value at /home/alice/rate.py"
class _Fake:
id = "openai-compat"
def chat(self, **kw):
raise RuntimeError(private)
monkeypatch.setattr(speech_rate, "get_active_llm_backend", lambda: _Fake())
res = speech_rate.adjust_for_slot("x" * 30, slot_seconds=1.0, target_lang="en")
assert res["error"] == "fit-provider-failed"
assert private not in repr(res)
assert private not in caplog.text
def test_disabled_glossary_extract_503s(skills, store):
_activate_groq(store)
from fastapi import HTTPException
+1 -2
View File
@@ -284,8 +284,7 @@ def test_server_mode_admin_read_keeps_bare_docker_bootstrap(monkeypatch):
def test_server_mode_admin_read_keeps_pin_only_discovery(monkeypatch):
monkeypatch.setenv("OMNIVOICE_SERVER_MODE", "1")
monkeypatch.delenv("OMNIVOICE_API_KEY", raising=False)
monkeypatch.setenv("OMNIVOICE_SHARE_PIN", "123456")
require_admin(_req_full("172.17.0.1", method="GET"))
require_admin(_req_full("172.17.0.1", method="GET", pin="123456"))
def test_server_mode_admin_mutation_allows_api_key(monkeypatch):
+2 -2
View File
@@ -65,11 +65,11 @@ def test_rejects_unwritable_dir(env, monkeypatch, tmp_path):
assert ei.value.status_code == 400
def test_rejects_path_with_null_byte(env):
def test_rejects_path_with_null_byte(env, tmp_path):
# An embedded NUL would otherwise blow up os.makedirs with a ValueError
# (→ 500). Validate up front and return a clean 400 instead.
with pytest.raises(fastapi.HTTPException) as ei:
s.set_models_dir(_body("/tmp/mo\x00dels"))
s.set_models_dir(_body(str(tmp_path / "mo\x00dels")))
assert ei.value.status_code == 400
+9 -2
View File
@@ -76,14 +76,21 @@ def test_network_state_endpoint_defaults_disabled():
def test_pin_only_remote_discovery_never_returns_share_pin(monkeypatch):
import importlib
from main import app
# Resolve the exact module instance held by the live router. The full suite
# deliberately replaces app modules in sys.modules, so the module-level
# ``ns`` test helper may no longer be the endpoint's dependency.
live_network_share = importlib.import_module("api.routers.system").network_share
monkeypatch.setenv("OMNIVOICE_SERVER_MODE", "1")
monkeypatch.delenv("OMNIVOICE_API_KEY", raising=False)
monkeypatch.setattr(
ns,
live_network_share,
"_state",
ns.ShareState(True, 3901, "123456", ["192.168.1.10"]),
live_network_share.ShareState(True, 3901, "123456", ["192.168.1.10"]),
)
# Keep the consumption middleware inert: this endpoint is testing the
# intentional admin read-only exception itself, before a PIN is supplied.
+286
View File
@@ -0,0 +1,286 @@
"""Resolving an engine's venv is work, and work must report progress (#1414).
`SubprocessBackend._spawn()` calls `venv_python()`, and on a cold first run
that is not cheap: the probe spawns each candidate interpreter to import the
engine (tens of seconds on a slow disk), and if none is installed it can run
the whole `uv venv` + `uv pip install` bootstrap which is bounded at 900 s
*by design*, because installing torch takes minutes.
All of that happens on a GPU-pool worker, inside a generate request whose
execution budget defaults to 300 s. Nothing along the way reported progress,
so the budget expired part-way through and the job was abandoned and, until
#1424, blamed on the machine's compute. The first generation that triggers a
bootstrap could therefore never succeed, no matter how good the hardware.
The sidecar's own cold model load already heartbeats for exactly this reason
(#1367). Resolution is the step immediately before it that never did.
"""
from __future__ import annotations
import threading
from contextlib import contextmanager
from pathlib import Path
import pytest
@pytest.fixture
def sb():
"""Resolved at run time, not import time."""
import services.subprocess_backend as _sb
return _sb
@pytest.fixture
def mm():
import services.model_manager as _mm
return _mm
def test_a_slow_resolution_extends_the_deadline(sb, mm, monkeypatch):
"""The whole point: a resolution that outlives one heartbeat interval
leaves proof of life on the execution clock."""
monkeypatch.setattr(sb, "_RESOLVE_HEARTBEAT_S", 0)
monkeypatch.setattr(mm, "running_on_gpu_pool", lambda: True)
ident = threading.get_ident()
mm._MODEL_LOAD_ACTIVITY.pop(ident, None)
wrote = threading.Event()
class _SignallingMap(dict):
def __setitem__(self, key, value):
super().__setitem__(key, value)
wrote.set()
monkeypatch.setattr(mm, "_MODEL_LOAD_ACTIVITY", _SignallingMap())
with sb._heartbeat_while_resolving("indextts2"):
assert wrote.wait(2), "the heartbeat thread never reported progress"
assert ident in mm._MODEL_LOAD_ACTIVITY, (
"a slow venv resolution reported no progress — the generate budget "
"expires part-way through the install it is waiting for"
)
mm._MODEL_LOAD_ACTIVITY.pop(ident, None)
def test_it_credits_the_resolving_thread_not_the_beater(sb, mm, monkeypatch):
"""The heartbeat runs on a helper thread so it can tick while resolution
blocks but the job the clock is watching is the caller's. Crediting the
helper's ident would extend nothing and would poison an ident a pool
worker may later reuse.
Asserted against the beater's OWN ident rather than a before/after diff of
the activity map: other tests in the suite have live pool workers, so the
diff is not this test's to own.
"""
monkeypatch.setattr(sb, "_RESOLVE_HEARTBEAT_S", 0)
monkeypatch.setattr(mm, "running_on_gpu_pool", lambda: True)
ident = threading.get_ident()
mm._MODEL_LOAD_ACTIVITY.pop(ident, None)
beater_idents: set[int] = set()
wrote = threading.Event()
class _SignallingMap(dict):
def __setitem__(self, key, value):
super().__setitem__(key, value)
wrote.set()
monkeypatch.setattr(mm, "_MODEL_LOAD_ACTIVITY", _SignallingMap())
real_thread = threading.Thread
class _Recording(real_thread):
def run(self):
beater_idents.add(threading.get_ident())
super().run()
monkeypatch.setattr(sb.threading, "Thread", _Recording)
with sb._heartbeat_while_resolving("indextts2"):
assert wrote.wait(2), "the heartbeat thread never reported progress"
assert ident in mm._MODEL_LOAD_ACTIVITY, "the caller's job was never credited"
assert beater_idents, "no heartbeat thread ran"
assert not (beater_idents & set(mm._MODEL_LOAD_ACTIVITY)), (
"the heartbeat thread credited its own ident — that extends nothing "
"and poisons an ident a pool worker may later reuse"
)
mm._MODEL_LOAD_ACTIVITY.pop(ident, None)
def test_an_off_pool_caller_never_heartbeats(sb, mm, monkeypatch):
"""#1379's lesson: an off-pool thread's ident is not tracked by the clock,
and a pool worker that later reuses it would inherit unearned extension."""
monkeypatch.setattr(mm, "running_on_gpu_pool", lambda: False)
ident = threading.get_ident()
mm._MODEL_LOAD_ACTIVITY.pop(ident, None)
class _UnexpectedThread:
def __init__(self, *args, **kwargs):
raise AssertionError("an off-pool call started a heartbeat helper")
monkeypatch.setattr(sb.threading, "Thread", _UnexpectedThread)
with sb._heartbeat_while_resolving("indextts2"):
pass
assert ident not in mm._MODEL_LOAD_ACTIVITY
def test_the_beater_stops_when_resolution_finishes(sb, mm, monkeypatch):
"""A thread per spawn that never exits would accumulate one per generate."""
monkeypatch.setattr(sb, "_RESOLVE_HEARTBEAT_S", 0)
monkeypatch.setattr(mm, "running_on_gpu_pool", lambda: True)
exited = threading.Event()
real_thread = threading.Thread
class _Recording(real_thread):
def run(self):
try:
super().run()
finally:
exited.set()
monkeypatch.setattr(sb.threading, "Thread", _Recording)
with sb._heartbeat_while_resolving("indextts2"):
pass
assert exited.wait(2), "heartbeat helper survived context exit"
mm._MODEL_LOAD_ACTIVITY.pop(threading.get_ident(), None)
def test_a_broken_heartbeat_does_not_break_the_spawn(sb, monkeypatch):
"""Never raises: a generation must not fail because progress reporting
could not import or could not write."""
monkeypatch.setattr(sb, "_RESOLVE_HEARTBEAT_S", 0.01)
import services.model_manager as mm
monkeypatch.setattr(
mm, "running_on_gpu_pool",
lambda: (_ for _ in ()).throw(RuntimeError("clock is broken")),
)
with sb._heartbeat_while_resolving("indextts2"):
pass # the point is that this block is reached and exits cleanly
def test_spawn_wraps_the_resolution(sb, monkeypatch):
"""A guard against the wrapper being dropped in a later refactor: the
heartbeat is worthless if `venv_python()` is called outside it."""
active = False
resolved_inside = threading.Event()
@contextmanager
def _recording_heartbeat(_engine_id):
nonlocal active
active = True
try:
yield
finally:
active = False
class _StopAfterResolution(RuntimeError):
pass
class _Backend(sb.SubprocessBackend):
id = "test"
@property
def sample_rate(self):
return 24_000
@property
def supported_languages(self):
return ["en"]
@classmethod
def is_available(cls):
return True, "ready"
@classmethod
def venv_python(cls):
assert active, "venv_python() ran outside the heartbeat context"
resolved_inside.set()
return Path("python")
@classmethod
def sidecar_script(cls):
raise _StopAfterResolution
monkeypatch.setattr(sb, "_heartbeat_while_resolving", _recording_heartbeat)
backend = _Backend.__new__(_Backend)
backend._proc = None
with pytest.raises(_StopAfterResolution):
backend._spawn()
assert resolved_inside.is_set()
def test_no_heartbeat_write_escapes_the_context(sb, mm, monkeypatch):
"""The late-write race (CodeRabbit, #1426).
`_beat()` can be past its `stop.wait()` and already committed to a write
at the moment the context exits. Signalling the stop flag without joining
lets that write land afterwards and `_run_on_gpu_pool`'s `_job` pops
this ident right after, precisely so a stale beat cannot vouch for a later
job on the same (reused) worker ident. A write that arrives after the pop
resurrects the entry, and the next job inherits a heartbeat it never sent:
the wedge detector reads it as progress and keeps extending a stuck job.
The interleaving is forced rather than waited for. A patched writer parks
inside the write until the test releases it, so the exit path must be the
thing that waits if it only signals, the write lands after the context
and the assertion catches it deterministically, on every run and every
scheduler.
"""
monkeypatch.setattr(sb, "_RESOLVE_HEARTBEAT_S", 0)
monkeypatch.setattr(mm, "running_on_gpu_pool", lambda: True)
ident = threading.get_ident()
mm._MODEL_LOAD_ACTIVITY.pop(ident, None)
in_write = threading.Event()
release = threading.Event()
write_finished = threading.Event()
class _ParkingMap(dict):
"""Stalls the heartbeat mid-write so the exit path has to wait."""
def __setitem__(self, key, value):
in_write.set()
assert release.wait(5), "test did not release the parked write"
super().__setitem__(key, value)
write_finished.set()
context_exited = threading.Event()
monkeypatch.setattr(mm, "_MODEL_LOAD_ACTIVITY", _ParkingMap())
joined = threading.Event()
real_thread = threading.Thread
class _JoinRecordingThread(real_thread):
def join(self, *args, **kwargs):
joined.set()
return super().join(*args, **kwargs)
monkeypatch.setattr(sb.threading, "Thread", _JoinRecordingThread)
def _run_context():
with sb._heartbeat_while_resolving("indextts2"):
assert in_write.wait(5), "heartbeat never attempted a write"
context_exited.set()
runner = real_thread(target=_run_context)
runner.start()
try:
assert in_write.wait(5), "heartbeat never reached the parked write"
assert joined.wait(2), "context exit did not wait for the heartbeat helper"
assert not context_exited.is_set(), "context exited before the write finished"
finally:
release.set()
runner.join(5)
assert write_finished.is_set(), "parked heartbeat write did not finish"
assert not runner.is_alive(), "resolve context did not exit after the write"
assert context_exited.is_set()
mm._MODEL_LOAD_ACTIVITY.pop(ident, None)
+23
View File
@@ -7,6 +7,8 @@ strip poison ("[object Object]", freeform prose) down to whitelist tags,
recovering a design from ``vd_states`` when the stored value is unusable.
"""
import json
from statistics import median
import time
import pytest
@@ -57,6 +59,27 @@ def test_sanitize_handles_empty_and_poison(bad):
assert sanitize_instruct(bad) == ""
def test_sanitize_long_whitespace_run_is_linear_time():
sizes = (2_000, 8_000, 32_000)
timings = []
for size in sizes:
poisoned = "female" + (" " * size) + "not-a-tag"
assert sanitize_instruct(poisoned) == "" # warm timer/cache paths
samples = []
for _ in range(5):
started = time.perf_counter_ns()
assert sanitize_instruct(poisoned) == ""
samples.append(time.perf_counter_ns() - started)
timings.append(median(samples))
# Input grows 4x per step and 16x overall. Generous multipliers and a
# 20 ms scheduling floor tolerate noisy shared runners while still
# separating linear split/strip behavior from the former quadratic regex.
for smaller, larger in zip(timings, timings[1:]):
assert larger <= max(smaller * 10, 20_000_000)
assert timings[-1] <= max(timings[0] * 48, 20_000_000)
def test_instruct_from_vd_states_dict_drops_auto():
vd = {"gender": "female", "age": "Auto", "pitch": "high pitch", "accent": "british accent"}
assert instruct_from_vd_states(vd) == "female, high pitch, british accent"
Generated
+168 -161
View File
@@ -70,7 +70,7 @@ wheels = [
[[package]]
name = "aiohttp"
version = "3.14.1"
version = "3.14.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiohappyeyeballs" },
@@ -82,108 +82,108 @@ dependencies = [
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
{ name = "yarl" },
]
sdist = { url = "https://files.pythonhosted.org/packages/82/78/8ea7308cac6934de8c74a14f3d5f65d1c89287426688be79538d0e5c013d/aiohttp-3.14.1.tar.gz", hash = "sha256:307f2cff90a764d329e77040603fa032db89c5c24fdad50c4c15334cba744035", size = 7955794, upload-time = "2026-06-07T21:09:35.529Z" }
sdist = { url = "https://files.pythonhosted.org/packages/58/d9/22ce5786ac0c1653ae8b6c23bded02c1686d11f0dbb45b31ce128e0df985/aiohttp-3.14.3.tar.gz", hash = "sha256:9491196535a88924a60afd5b5f434b5b203b6cc616250878dbdb223a8f7844bc", size = 7971213, upload-time = "2026-07-23T01:57:27.037Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/26/dd/bf526e6f0a1120dd6f2df2e97bacfe4d358f13d17a0ff5847301a1375a51/aiohttp-3.14.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:aa00140699487bd435fde4342d85c94cb256b7cd3a5b9c3396c67f19922afda2", size = 765225, upload-time = "2026-06-07T21:06:07.957Z" },
{ url = "https://files.pythonhosted.org/packages/8f/e1/a2872aa55495a70f61310d411541c6ee23812d9a884e000c716e1bc3edbf/aiohttp-3.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1c1af67559445498b502030c35c59db59966f47041ca9de5b4e707f86bd10b5f", size = 518743, upload-time = "2026-06-07T21:06:09.749Z" },
{ url = "https://files.pythonhosted.org/packages/5b/e7/c60c7b209e509cc787de3cea0550a518538cfc08003e1c1e14c1c63fff71/aiohttp-3.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d44ec478e713ee7f29b439f7eb8dc2b9d4079e11ae114d2c2ac3d5daf30516c8", size = 514139, upload-time = "2026-06-07T21:06:11.26Z" },
{ url = "https://files.pythonhosted.org/packages/5b/8d/614ace2f579702c9840ab1e1447fd8509e35b0b904f7196418fa2f57b25d/aiohttp-3.14.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d3b1a184a9a8f548a6b73f1e26b96b052193e4b3175ed7342aaf1151a1f00a04", size = 1784088, upload-time = "2026-06-07T21:06:12.887Z" },
{ url = "https://files.pythonhosted.org/packages/49/e0/726e90f99542bf292f81a96a12cc4847deb86f3ccf62c6f4014a201f4d33/aiohttp-3.14.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5f2504bc0322437c9a1ff6d3333ca56c7477b727c995f036b976ae17b98372c8", size = 1737835, upload-time = "2026-06-07T21:06:14.564Z" },
{ url = "https://files.pythonhosted.org/packages/0b/4b/d176d5c4db9d33dacf0543102ea59503bc1d528af4cfd0b719949ca49389/aiohttp-3.14.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:73f05ea02013e02512c3bf42714f1208c57168c779cc6fe23516e4543089d0a6", size = 1842801, upload-time = "2026-06-07T21:06:16.228Z" },
{ url = "https://files.pythonhosted.org/packages/dc/d6/5a99b563690ea0cbed912ae94a2ce33993a5709a651a3a4fe761e7dd973a/aiohttp-3.14.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:797457503c2d426bee06eef808d07b31ede30b65e054444e7de64cad0061b7af", size = 1929992, upload-time = "2026-06-07T21:06:17.947Z" },
{ url = "https://files.pythonhosted.org/packages/76/7f/a987b14a3859094b3cea3f4825219c3e5536242564af6e3f9c2f6c994eb2/aiohttp-3.14.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b821a1f7dedf7e37450654e620038ac3b2e81e8fa6ea269337e97101978ec730", size = 1786989, upload-time = "2026-06-07T21:06:19.677Z" },
{ url = "https://files.pythonhosted.org/packages/f1/1a/420e5c85a3e73349372ed22ce0b6af86bfa6ce16a4b20a64a2e94608c781/aiohttp-3.14.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4cd96b5ba05d67ed0cf00b5b405c8cd99586d8e3481e8ee0a831057591af7621", size = 1640129, upload-time = "2026-06-07T21:06:22.558Z" },
{ url = "https://files.pythonhosted.org/packages/a7/80/18a592ed3be0a402cc03670bd72ee1f8563ddbe1d8d5542dbf868f274136/aiohttp-3.14.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d459b98a932296c6f0e94f87511a0b1b90a8a02c30a50e60a297619cd5a58ee", size = 1756576, upload-time = "2026-06-07T21:06:24.8Z" },
{ url = "https://files.pythonhosted.org/packages/ec/0b/8b3d5713373858ff71a617daf6e3b0e81ad63e79d09a3cf2f6b6b983939c/aiohttp-3.14.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:764457a7be60825fb770a644852ff717bcbb5042f189f2bd16df61a81b3f6573", size = 1754668, upload-time = "2026-06-07T21:06:26.528Z" },
{ url = "https://files.pythonhosted.org/packages/9f/49/fd564575cf225821d7ba5a117cb8bc27213d8a7e1811162afb43ae077039/aiohttp-3.14.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f7a16ef45b081454ef844502d87a848876c490c4cb5c650c230f6ec79ed2c1e7", size = 1817019, upload-time = "2026-06-07T21:06:28.297Z" },
{ url = "https://files.pythonhosted.org/packages/ed/1b/e850c9ae6fc91356552ae668bb6c51e93fa29c8aef13398a10b56678557f/aiohttp-3.14.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2fbc3ed048b3475b9f0cbcb9978e9d2d3511acd91ead203af26ed9f0056004cf", size = 1631638, upload-time = "2026-06-07T21:06:30.242Z" },
{ url = "https://files.pythonhosted.org/packages/eb/94/3c337ba72451a89806ace6f75bddc92bafc5b8d53d90115a512858024b63/aiohttp-3.14.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:bedb0cd073cc2dc035e30aeb99444389d3cd2113afe4ef9fcd23d439f5bade85", size = 1835660, upload-time = "2026-06-07T21:06:31.943Z" },
{ url = "https://files.pythonhosted.org/packages/2b/9c/9c18cf367a0498212d9ba7daf990b504a5e8ae064cda4b504e2647c89c03/aiohttp-3.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b6feea921016eb3d4e04d65fc4e9ca402d1a3801f562aef94989f54694917af3", size = 1775698, upload-time = "2026-06-07T21:06:33.72Z" },
{ url = "https://files.pythonhosted.org/packages/b5/63/a251a9d2a6cb45065b2ddc0bde2b3dd10108740a9a42f632c66405a761a2/aiohttp-3.14.1-cp311-cp311-win32.whl", hash = "sha256:313701e488100074ce99850404ee36e741abf6330179fec908a1944ecf570126", size = 458386, upload-time = "2026-06-07T21:06:35.279Z" },
{ url = "https://files.pythonhosted.org/packages/17/ca/69274c51dcd6e8947d77b2806cf47a4a15f2c846e2cbeb1882547d3da283/aiohttp-3.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:03ab4530fdcb3a543a122ba4b65ac9919da9fe9f78a03d328a6e38ff962f7aa5", size = 483406, upload-time = "2026-06-07T21:06:36.824Z" },
{ url = "https://files.pythonhosted.org/packages/2c/8a/c25904f77690c3688ec140f87591ef11a0cfe36bf3d5c0f1f38056fb62b3/aiohttp-3.14.1-cp311-cp311-win_arm64.whl", hash = "sha256:486f7d16ed54c39c2cbd7ca71fd8ba2b8bb7860df65bd7b6ed640bab96a38a8b", size = 452987, upload-time = "2026-06-07T21:06:38.371Z" },
{ url = "https://files.pythonhosted.org/packages/1d/21/151624b51cd92553d95424daf4bf19f19ce9be9002d19253e7e7ce67197b/aiohttp-3.14.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d35143e27778b4bb0fb189562d7f275bff79c62ab8e98459717c0ea617ff2480", size = 757402, upload-time = "2026-06-07T21:06:40.311Z" },
{ url = "https://files.pythonhosted.org/packages/c2/82/280619e0bd7bf2454987e19282616e84762255dd9c8468f62382e8c191f1/aiohttp-3.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bcfb80a2cc36fba2534e5e5b5264dc7ae6fcd9bf15256da3e53d2f499e6fa29d", size = 512310, upload-time = "2026-06-07T21:06:42.207Z" },
{ url = "https://files.pythonhosted.org/packages/55/b2/2aac325583aaa1353045f96dffa586d8a34e8322e14a7ba49cffeb103ab4/aiohttp-3.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27fd7c91e51729b4f7e1577865fa6d34c9adccbc39aabe9000285b48af9f0ec2", size = 512448, upload-time = "2026-06-07T21:06:43.813Z" },
{ url = "https://files.pythonhosted.org/packages/8a/72/a60607cb849faa8af8a356c9329ea2eb6f395d49e82cc82ccba1fd8deb8f/aiohttp-3.14.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:64c567bf9eaf664280116a8688f63016e6b32db2505908e2bdaca1b6438142f2", size = 1766854, upload-time = "2026-06-07T21:06:45.391Z" },
{ url = "https://files.pythonhosted.org/packages/b5/d3/d9fe1c9ec7557ab4d0d82bebaa728c6418f0b93295ec2f4ab015f7710cc7/aiohttp-3.14.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f5e6ff2bdbb8f4cd3fbe41f99e25bbcd58e3bf9f13d3dd31a11e7917251cc77a", size = 1740884, upload-time = "2026-06-07T21:06:47.413Z" },
{ url = "https://files.pythonhosted.org/packages/c1/dc/f2cecfaf9337ba3e63f181500814ff502aa3d00d9c7ec93a9d23d10a27b2/aiohttp-3.14.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2f73e01dc37122325caf079982621262f96d74823c179038a82fddfc50359264", size = 1810034, upload-time = "2026-06-07T21:06:50.165Z" },
{ url = "https://files.pythonhosted.org/packages/66/d7/2ff65c5e65c0d7476daf7e15c032e0805e36811185b9623e3238ad6c763e/aiohttp-3.14.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bb2c0c80d431c0d03f2c7dbf125150fedd4f0de17366a7ca33f7ccb822391842", size = 1904054, upload-time = "2026-06-07T21:06:52.035Z" },
{ url = "https://files.pythonhosted.org/packages/20/9c/d445818389df371f56d141d881153ba23183c4735a03f7356ffb43f7757d/aiohttp-3.14.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e6fc1a85fa7194a1a7d19f44e8609180f4a8eb5fa4c7ed8b4355f080fad235c", size = 1790278, upload-time = "2026-06-07T21:06:54.049Z" },
{ url = "https://files.pythonhosted.org/packages/4d/aa/bf04cb4d865fc6101c2229a294ad744973b72e513fdc5a6b791e6983d72a/aiohttp-3.14.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:686b6c0d3911ec387b444ddf5dc62fb7f7c0a7d5186a7861626496a5ab4aff95", size = 1591795, upload-time = "2026-06-07T21:06:55.911Z" },
{ url = "https://files.pythonhosted.org/packages/dc/b4/4dac0038960427ba832f6609dfb4ea5437d7fd80c72001b9e48f834f428b/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c6fa4dc7ad6f8109c70bb1499e589f76b0b792baf39f9b017eb92c8a81d0a199", size = 1728397, upload-time = "2026-06-07T21:06:57.777Z" },
{ url = "https://files.pythonhosted.org/packages/2b/f9/7cd4e8ad7aa3b75f17d56bb5498dd604a93d4e6eece822ba0568c413fff0/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:87a5eea1b2a5e21e1ebdbb33ad4165359189327e63fc4e4894693e7f821ac817", size = 1766504, upload-time = "2026-06-07T21:07:00.009Z" },
{ url = "https://files.pythonhosted.org/packages/f9/df/fc01d9fcad0f73fed3f3d361f1f94f975947b50dff82919f6dc2bf4316cc/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c1421eb01d4fd608d88cc8290211d177a58532b55ad94076fb349c5bf467f0a", size = 1777806, upload-time = "2026-06-07T21:07:02.064Z" },
{ url = "https://files.pythonhosted.org/packages/41/09/47e2d090bddcc8fb4ccb4c314aadc32d7c5d9bb55f50f6ad1c92fc15d501/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:34b257ec41345c1e8f2df68fa908a7952f5de932723871eb633ecbbff396c9a4", size = 1580707, upload-time = "2026-06-07T21:07:03.942Z" },
{ url = "https://files.pythonhosted.org/packages/3d/36/f1a4ce904ae0b6930cfe9afc96d0896f7ec1a620c400405d63783bb95a9c/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:de538791a80e5d862addbc183f70f0158ac9b9bb872bb147f1fd2a683691e087", size = 1798121, upload-time = "2026-06-07T21:07:05.987Z" },
{ url = "https://files.pythonhosted.org/packages/70/0a/e0075ce9ca0279ee1d4f0c0b85f54fea02ebc83c3007651a72bece658fec/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6f71173be42d3241d428f760122febb748de0623f44308a6f120d0dd9ec572e3", size = 1767580, upload-time = "2026-06-07T21:07:07.873Z" },
{ url = "https://files.pythonhosted.org/packages/3e/61/a0c0a8f327a9c52095cdd8e312391b00d3ed64ab6c72bb5c33d8ec251cf7/aiohttp-3.14.1-cp312-cp312-win32.whl", hash = "sha256:ec8dc383ee57ea3e883477dcca3f11b65d58199f1080acaf4cd6ad9a99698be4", size = 452771, upload-time = "2026-06-07T21:07:09.669Z" },
{ url = "https://files.pythonhosted.org/packages/df/d9/ea367c75f16ac9c6cdc8febb25e8318fa21a2b1bc8d6514d4b2d890bface/aiohttp-3.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:2aa92c87868cd13674989f9ee83e5f9f7ea4237589b728048e1f0c8f6caa3271", size = 479873, upload-time = "2026-06-07T21:07:11.538Z" },
{ url = "https://files.pythonhosted.org/packages/03/64/8d96784a7851156db8a4c6c3f6f91042fdf39fb15a4cc38c8b3c14833c45/aiohttp-3.14.1-cp312-cp312-win_arm64.whl", hash = "sha256:2c840c90759922cb5e6dda94596e079a30fb5a5ba548e7e0dc00574703940847", size = 448073, upload-time = "2026-06-07T21:07:13.637Z" },
{ url = "https://files.pythonhosted.org/packages/bc/97/bd137012dd97e1649162b099135a80e1fd59aaa807b2430fc448d1029aff/aiohttp-3.14.1-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:b3a03285a7f9c7b016324574a6d92a1c895da6b978cb8f1deee3ac72bc6da178", size = 506882, upload-time = "2026-06-07T21:07:15.501Z" },
{ url = "https://files.pythonhosted.org/packages/ef/79/e5cc690e9d922a66887ceeaca53a8ffd5a7b0be3816142b7abc433742d89/aiohttp-3.14.1-cp313-cp313-android_21_x86_64.whl", hash = "sha256:2a73f487ab8ef5abbb24b7aa9b73e98eaba9e9e031804ff2416f02eca315ccaf", size = 515270, upload-time = "2026-06-07T21:07:17.53Z" },
{ url = "https://files.pythonhosted.org/packages/fe/22/a73ccbf9dbd6e26dda0b24d5fd5db7da92ee3383a79f47677ffb834c5c5b/aiohttp-3.14.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:915fbb7b41b115192259f8c9ae58f3ddc444d2b5579917270211858e606a4afd", size = 485841, upload-time = "2026-06-07T21:07:19.555Z" },
{ url = "https://files.pythonhosted.org/packages/3b/b9/57ed8eaf596321c2ad747bd480fb1700dbd7177c60dfc9e4c187f629662e/aiohttp-3.14.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:7fb4bdf95b0561a79f259f9d28fbc109728c5ee7f27aff6391f0ca703a329abe", size = 492088, upload-time = "2026-06-07T21:07:21.581Z" },
{ url = "https://files.pythonhosted.org/packages/78/c0/5ebe5270a7c140d7c6f79dcb018640225f14d406c149e4eec04a7d82fe71/aiohttp-3.14.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:1b9748363260121d2927704f5d4fc498150669ca3ae93625986ee89c8f80dcd4", size = 501564, upload-time = "2026-06-07T21:07:23.388Z" },
{ url = "https://files.pythonhosted.org/packages/75/7f/8cdaa24fc7983865e0915153b96a9ac5bcdd3548d64c5a27d17cecccad2d/aiohttp-3.14.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:86a6dab78b0e43e2897a3bbe15745aa60dc5423ca437b7b0b164c069bf91b876", size = 751998, upload-time = "2026-06-07T21:07:25.046Z" },
{ url = "https://files.pythonhosted.org/packages/b2/f4/c4227aacfacc5cb0cc2d119b65301d177912a6842cd64e120c47af76064f/aiohttp-3.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4dfd6e47d3c44c2279907607f73a4240b88c69eb8b90da7e2441a8045dfd21da", size = 510918, upload-time = "2026-06-07T21:07:27.28Z" },
{ url = "https://files.pythonhosted.org/packages/ab/01/a2d5f96cd4e74424864d30bc0a7e44d0a12dacdcfa91b5b2d1bd3dca6bf3/aiohttp-3.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:317acd9f8602858dc7d59679812c376c7f0b97bcbbf16e0d6237f54141d8a8a6", size = 508657, upload-time = "2026-06-07T21:07:29.252Z" },
{ url = "https://files.pythonhosted.org/packages/e8/ed/3c0fb5c500fdd8e7ebc10d1889c04384fffa1a9163eac1356088ca9da1b1/aiohttp-3.14.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd869c427324e5cb15195793de951295710db28be7d818247f3097b4ab5d4b96", size = 1757907, upload-time = "2026-06-07T21:07:31.03Z" },
{ url = "https://files.pythonhosted.org/packages/0b/ab/d4c924d9bd5be3050c226612413ce68cb54c70d2c31b661bfc8d9a5b6a70/aiohttp-3.14.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:93b032b5ec3255473c143627d21a69ac74ae12f7f33974cb587c564d11b1066f", size = 1737565, upload-time = "2026-06-07T21:07:33.031Z" },
{ url = "https://files.pythonhosted.org/packages/19/2a/37326821ff779084020cdc33224d20b19f42f4183a500ff92022a739eda7/aiohttp-3.14.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f234b4deb12f3ad59127e037bc57c40c21e45b45282df7d3a55a0f409f595296", size = 1799018, upload-time = "2026-06-07T21:07:35.003Z" },
{ url = "https://files.pythonhosted.org/packages/b3/4f/6e947ba73e4ce09070761c05ed3a8ceb7c21f5e46798671d8b2aac0e4626/aiohttp-3.14.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9af6779bfb46abf124068327abcdf9ce95c9ef8287a3e8da76ccf2d0f16c28fa", size = 1894416, upload-time = "2026-06-07T21:07:36.956Z" },
{ url = "https://files.pythonhosted.org/packages/9d/6e/dbf1d0625dc711fb2851f4f3c3055c39ed58bae92082d8c627dbe6013736/aiohttp-3.14.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:faccab372e66bc76d5731525e7f1143c922271725b9d38c9f97edcc66266b451", size = 1783881, upload-time = "2026-06-07T21:07:39.063Z" },
{ url = "https://files.pythonhosted.org/packages/44/c2/5e25098a67268ed369483ae7d1a58bd0a13d03aab860d2a0e4a6eb25b046/aiohttp-3.14.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f380468b09d2a81633ee863b0ec5648d364bd17bb8ecfb8c2f387f7ac1faf42c", size = 1587572, upload-time = "2026-06-07T21:07:41.058Z" },
{ url = "https://files.pythonhosted.org/packages/2a/bd/cf9cee17e140f942a3de73e658a543aa8fbf35a5fc67a9d2538d52d77f0b/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:97e704dcd26271f5bda3fa07c3ce0fb76d6d3f8659f4baa1a24442cc9ba177ca", size = 1722137, upload-time = "2026-06-07T21:07:43.014Z" },
{ url = "https://files.pythonhosted.org/packages/89/6d/5684f8c59045c96f81a18cefbc1fbbd79d25b88f1c622f2a5c5c08fcb632/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:269b76ac5394092b95bc4a098f4fc6c191c083c3bd12775d1e30e663132f6a09", size = 1755953, upload-time = "2026-06-07T21:07:45.933Z" },
{ url = "https://files.pythonhosted.org/packages/a8/40/35caf3170f8359760740a7d9aa0fff2e344bef98e1d1186f5a0f6dec17e6/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c0b3e614340c889d575451696374c9d17affd54cd607ca0babed8f8c37b9397", size = 1766479, upload-time = "2026-06-07T21:07:48.047Z" },
{ url = "https://files.pythonhosted.org/packages/6d/a1/b0c61e7a137f0d81de49a82023a6df73c3c16d6fefb0f8e4a93d21639002/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:5663ee9257cfa1add7253a7da3035a02f31b6600ec48261585e1800a81533080", size = 1580077, upload-time = "2026-06-07T21:07:50.069Z" },
{ url = "https://files.pythonhosted.org/packages/0b/41/194ea4623693009fcefebef7aef63c141754f153e9cd0d39d3b9e36c175c/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:603a2c834142172ffddc054067f5ec0ca65d57a0aa98a71bc81952573208e345", size = 1791688, upload-time = "2026-06-07T21:07:52.106Z" },
{ url = "https://files.pythonhosted.org/packages/ba/45/4de841f005cfe1fd63e2a2fe011262c515e2a62aa6994b15947e7d717ac9/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cb21957bb8aca671c1765e32f58164cf0c50e6bf41c0bbbd16da20732ecaf588", size = 1761094, upload-time = "2026-06-07T21:07:54.113Z" },
{ url = "https://files.pythonhosted.org/packages/e4/ae/dbce10533d3896d544d5053939ed75b7dc31a1b0973d959b1b5ae21028d6/aiohttp-3.14.1-cp313-cp313-win32.whl", hash = "sha256:e509a55f681e6158c20f70f102f9cf61fb20fbc382272bc6d94b7343f2582780", size = 452662, upload-time = "2026-06-07T21:07:56.06Z" },
{ url = "https://files.pythonhosted.org/packages/7b/d9/0bf1a19362c32f06229da5e7ddfcec91f93474d6307f7a2d3135e9c674dc/aiohttp-3.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:1ac8531b638959718e18c2207fbfe297819875da46a740b29dfa29beba64355a", size = 479748, upload-time = "2026-06-07T21:07:58.319Z" },
{ url = "https://files.pythonhosted.org/packages/22/0a/62e7232dc9484fbec112ceb32efb6a624cc7994ec6e2b019286f17c4e8f2/aiohttp-3.14.1-cp313-cp313-win_arm64.whl", hash = "sha256:250d14af67f6b6a1a4a811049b1afa69d61d617fca6bf33149b3ab1a6dbcf7b8", size = 447723, upload-time = "2026-06-07T21:08:00.154Z" },
{ url = "https://files.pythonhosted.org/packages/c4/a1/5fafa04e1ca91ddb47608699d60649c1c6db3cf41c99e78fc4056f9513db/aiohttp-3.14.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:7c106c26852ca1c2047c6b80384f17100b4e439af276f21ef3d4e2f450ae7e15", size = 508531, upload-time = "2026-06-07T21:08:02.093Z" },
{ url = "https://files.pythonhosted.org/packages/fa/2e/bfa02f699d87ffc86d5959270b28f1cb410add3ccaced8ed2e0b8a5238fc/aiohttp-3.14.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:20205f7f5ade7aaec9f4b500549bbc071b046453aed72f9c06dcab87896a83e8", size = 514718, upload-time = "2026-06-07T21:08:04.476Z" },
{ url = "https://files.pythonhosted.org/packages/85/a5/9594ad6289eebbc97d167c44213d557807f90e59115caad24de21ad2c3b1/aiohttp-3.14.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:62a759436b29e677181a9e76bab8b8f689a29cb9c535f45f7c48c9c830d3f8c3", size = 487918, upload-time = "2026-06-07T21:08:06.377Z" },
{ url = "https://files.pythonhosted.org/packages/b4/61/16a32c36c3c49edec122a3dc811f2057df2f94d3b14aa107c8017d981618/aiohttp-3.14.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:2964cbf553df4d7a57348da44d961d871895fc1ee4e8c322b2a95612c7b17fba", size = 494014, upload-time = "2026-06-07T21:08:08.263Z" },
{ url = "https://files.pythonhosted.org/packages/9b/89/3ebcf96ed99c05bec9c434aaac6963fd3cbab4a786ae739908a144d9ce44/aiohttp-3.14.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:237651caadc3a59badd39319c54642b5299e9cc98a3a194310e55d5bb9f5e397", size = 502398, upload-time = "2026-06-07T21:08:10.244Z" },
{ url = "https://files.pythonhosted.org/packages/fd/3d/b74870a0c2d40c355928cd5b96c7a11fa821b8a40fc41365e64479b151fb/aiohttp-3.14.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:896e12dfdbbab9d8f7e16d2b28c6769a60126fa92095d1ebf9473d02593a2448", size = 758018, upload-time = "2026-06-07T21:08:12.447Z" },
{ url = "https://files.pythonhosted.org/packages/d3/66/f42f5c984d99e49c6cff5f26f590750f2e2f7ef1fcfb99966ab5be1b632e/aiohttp-3.14.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d03f281ed22579314ba00821ce20115a7c0ac430660b4cc05704a3f818b3e004", size = 512462, upload-time = "2026-06-07T21:08:14.624Z" },
{ url = "https://files.pythonhosted.org/packages/e9/a7/248e1aebe0c7810b0271e021a0f2a5eb6e78a051885b3c9df49f42a5802d/aiohttp-3.14.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:07eabb979d236335fed927e137a928c9adfb7df3b9ec7aa31726f133a62be983", size = 512824, upload-time = "2026-06-07T21:08:16.572Z" },
{ url = "https://files.pythonhosted.org/packages/26/97/2aa0e5ba0727dc3bd5aaebb7ccbc510f7dfb7fb961ec87497cd496635ab1/aiohttp-3.14.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4fe1f1087cbadb280b5e1bb054a4f00d1423c74d6626c5e48400d871d34ecefe", size = 1749898, upload-time = "2026-06-07T21:08:18.635Z" },
{ url = "https://files.pythonhosted.org/packages/00/8d/e97f6c96c891d457c8479d92a514ba194d0412f981d72c70341ee18488ed/aiohttp-3.14.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:367a9314fdc79dab0fac96e216cb41dd73c85bdca85306ce8999118ba7e0f333", size = 1710114, upload-time = "2026-06-07T21:08:20.892Z" },
{ url = "https://files.pythonhosted.org/packages/6f/e6/aa8d7e863048c8fceb5cd6ce74017311cec3ead07847387e12265fb4444e/aiohttp-3.14.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a24f677ebe83749039e7bdf862ff0bbb16818ae4193d4ef96505e269375bcce0", size = 1802541, upload-time = "2026-06-07T21:08:23.044Z" },
{ url = "https://files.pythonhosted.org/packages/83/a8/72193137de57fda4ebfae4563182d082c8856e3b6e9871d0b46f028fb369/aiohttp-3.14.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c83afe0ba876be7e943d2e0ba645809ad441575d2840c895c21ee5de93b9377a", size = 1875776, upload-time = "2026-06-07T21:08:25.288Z" },
{ url = "https://files.pythonhosted.org/packages/a0/18/938441025db6769a3464596b2410af3afde0b21eb2f204c6f766f68af4bd/aiohttp-3.14.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:634e385930fb6d2d479cf3aa66515955863b77a5e3c2b5894ca259a25b308602", size = 1760329, upload-time = "2026-06-07T21:08:27.363Z" },
{ url = "https://files.pythonhosted.org/packages/60/29/bf2496b4065e76e09fe48015aaffe5ce161d8f089b06ac6982070f653076/aiohttp-3.14.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eeea07c4397bbc57719c4eed8f9c284874d4f175f9b6d57f7a1546b976d455ca", size = 1587293, upload-time = "2026-06-07T21:08:29.805Z" },
{ url = "https://files.pythonhosted.org/packages/49/a2/2136674d52123b1354bd05dd5753c318db47dc0c927cc70b27bab3755456/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:335c0cc3e3545ce98dcb9cfcb836f40c3411f43fa03dab757597d80c89af8a35", size = 1714756, upload-time = "2026-06-07T21:08:32.094Z" },
{ url = "https://files.pythonhosted.org/packages/a7/b9/e5fd2e6f915503081c0f9b1e8540947037929c70c191da2e4d54b31a21a1/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:ae6be797afdef264e8a84864a85b196ca06045586481b3df8a967322fd2fa844", size = 1721052, upload-time = "2026-06-07T21:08:34.167Z" },
{ url = "https://files.pythonhosted.org/packages/63/5a/2833e324a2263e104e31e2e91bc5bbee81bc499afd32203faee048a883f0/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:8560b4d712474335d08907db7973f71912d3a9a8f1dee992ec06b5d2fe359496", size = 1766888, upload-time = "2026-06-07T21:08:36.95Z" },
{ url = "https://files.pythonhosted.org/packages/57/fa/dea6511870913162f3b2e8c42a7614eb203a4540b8c2da43e0bfb0548f3c/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7edd08e0a5deb1e8564a2fcd8f4561014a3f05252334671bbf55ddd47db0e5", size = 1581679, upload-time = "2026-06-07T21:08:39.292Z" },
{ url = "https://files.pythonhosted.org/packages/14/bd/3cf0d55e71784b33534e9710a67d382d900598b4787fbce6cc7317f8c42a/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:b6ff7fcee63287ae57b5df3e4f5957ce032122802509246dec1a5bcc55904c95", size = 1782021, upload-time = "2026-06-07T21:08:41.407Z" },
{ url = "https://files.pythonhosted.org/packages/c1/af/14bb5843eccbe234f4dfb78ab73e549d99727247e62ae5d62cbd22eaf5b0/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6ffbb2f4ec1ceaff7e07d43922954da26b223d188bf30658e561b98e23089444", size = 1742574, upload-time = "2026-06-07T21:08:43.795Z" },
{ url = "https://files.pythonhosted.org/packages/f2/1e/fbeb7af9210a67ac0f9c9bec0f8f4568497924e33137a3d5b48e1cf85f3f/aiohttp-3.14.1-cp314-cp314-win32.whl", hash = "sha256:a9875b46d910cff3ea2f5962f9d266b465459fe634e22556ab9bd6fc1192eea0", size = 457773, upload-time = "2026-06-07T21:08:46.168Z" },
{ url = "https://files.pythonhosted.org/packages/f0/2b/13e8d741a9ec5db7d900c060554cf8352ab85e44e2a4469ebb9d377bda17/aiohttp-3.14.1-cp314-cp314-win_amd64.whl", hash = "sha256:af8b4b81a960eeaf1234971ac3cd0ba5901f3cd42eae42a46b4d089a8b492719", size = 485001, upload-time = "2026-06-07T21:08:48.401Z" },
{ url = "https://files.pythonhosted.org/packages/df/30/491acfa2c4d6c3ff59c49a14fc1b50be3241e25bbb0c84c09e2da4d11395/aiohttp-3.14.1-cp314-cp314-win_arm64.whl", hash = "sha256:cf4491381b1b57425c315a56a439251b1bdac07b2275f19a8c44bc57744532ec", size = 453809, upload-time = "2026-06-07T21:08:50.7Z" },
{ url = "https://files.pythonhosted.org/packages/34/e3/19dbe1a1f4cc6230eb9e314de7fe68053b0992f9302b27d12141a0b5db53/aiohttp-3.14.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:819c054312f1af92947e6a55883d1b66feefab11531a7fc45e0fb9b63880b5c2", size = 793320, upload-time = "2026-06-07T21:08:52.775Z" },
{ url = "https://files.pythonhosted.org/packages/7f/20/1b7182219ba1b108430d6e4dc53d25ae02dcfcf5a045b33af4e8c5167527/aiohttp-3.14.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10ee9c1753a8f706345b22496c79fbddb5be0599e0823f3738b1534058e25340", size = 529077, upload-time = "2026-06-07T21:08:55Z" },
{ url = "https://files.pythonhosted.org/packages/b9/c8/14ce60ec31a2e5f5274bb17d383a6f7a3aabca31ac04eee05585bbadab16/aiohttp-3.14.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1601cc37baf5750ccacae618ec2daf020769581695550e3b654a911f859c563d", size = 532476, upload-time = "2026-06-07T21:08:57.176Z" },
{ url = "https://files.pythonhosted.org/packages/7e/02/9ac85e081e53da2e061b02fa7758fe0a12d17b8ce2d1f5e6c7cb76730328/aiohttp-3.14.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d6e0ac9da31c9c04c84e1c0182ad8d6df35965a85cae29cd71d089621b3ae94", size = 1922347, upload-time = "2026-06-07T21:08:59.563Z" },
{ url = "https://files.pythonhosted.org/packages/c0/3e/d3ba07a0ab38b5389e10bec4362d21e10a4f667cba2d79ba30837b3a5059/aiohttp-3.14.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9e8f2d660c350b3d0e259c7a7e3d9b7fc8b41210cbcc3d4a7076ff0a5e5c2fdc", size = 1786465, upload-time = "2026-06-07T21:09:01.909Z" },
{ url = "https://files.pythonhosted.org/packages/0b/cb/e2ee978a00cfb2df829704a69528b18154eba5939f45bc1efa8f33aee4c5/aiohttp-3.14.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4691802dda97be727f79d86818acaad7eb8e9252626a1d6b519fedbb92d5e251", size = 1909423, upload-time = "2026-06-07T21:09:04.357Z" },
{ url = "https://files.pythonhosted.org/packages/73/5d/1430334858b1022b58ae50399a918f0bd6fe8fa7fa183598d657ff61e040/aiohttp-3.14.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c389c482a7e9b9dc3ee2701ac46c4125297a3818875b9c305ddb603c04828fd1", size = 2001906, upload-time = "2026-06-07T21:09:06.722Z" },
{ url = "https://files.pythonhosted.org/packages/66/4e/560c7472d3d198a23aa5c8b19a5115bf6a9b77b7d3e4bb363da320430ad2/aiohttp-3.14.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fc0cacab7ba4e56f0f81c82a98c09bed2f39c940107b03a34b168bdf7597edd3", size = 1877095, upload-time = "2026-06-07T21:09:09.011Z" },
{ url = "https://files.pythonhosted.org/packages/0d/f1/4745806578d447db4a784a8591e2dae3afdfc2bcb96f8f81271b13df6543/aiohttp-3.14.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:979ed4717f59b8bb12e3963378fa285d93d367e15bcd66c721311826d3c44a6c", size = 1676222, upload-time = "2026-06-07T21:09:11.461Z" },
{ url = "https://files.pythonhosted.org/packages/6a/c9/48255813cca749a229ef0ab476004ec623728ad79a9c0840616f6c076325/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:38e1e7daaea81df51c952e18483f323d878499a1e2bfe564790e0f9701d6f203", size = 1842922, upload-time = "2026-06-07T21:09:14.118Z" },
{ url = "https://files.pythonhosted.org/packages/3d/c0/bbd054e2bee909f529523a5af3891052606af5143c09f5f183ec3b234676/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:4132e72c608fe9fecb8f409113567605915b83e9bdd3ea56538d2f9cd35002f1", size = 1825035, upload-time = "2026-06-07T21:09:16.447Z" },
{ url = "https://files.pythonhosted.org/packages/a8/ae/90395d4376deceb74e09ec26b6adf7d2015a6f8802d6d84446af860fef04/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:eefd9cc9b6d4a2db5f00a26bc3e4f9acf71926a6ec557cd56c9c6f27c290b665", size = 1849512, upload-time = "2026-06-07T21:09:18.742Z" },
{ url = "https://files.pythonhosted.org/packages/93/bd/fb25f3049957553d4ce0ba6ae480aa2f592a6985497fca590837d16c1be0/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:b165790117eea512d7f3fb22f1f6dad3d55a7189571993eb015591c1401276d1", size = 1668571, upload-time = "2026-06-07T21:09:21.458Z" },
{ url = "https://files.pythonhosted.org/packages/3f/22/7f73303d64dd567ff3addca90b556690ed1233a47b8f55d242fb90af3681/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ed09c7eb1c391271c2ed0314a51903e72a3acb653d5ccfc264cdf3ef11f8269d", size = 1881159, upload-time = "2026-06-07T21:09:23.813Z" },
{ url = "https://files.pythonhosted.org/packages/44/be/0474c5a8b5640e1e4aa1923430a91f4151be82e511373fe764189b89aef5/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:99abd37084b82f5830c635fddd0b4993b9742a66eb746dacf433c8590e8f9e3c", size = 1841409, upload-time = "2026-06-07T21:09:26.207Z" },
{ url = "https://files.pythonhosted.org/packages/7b/3c/bb4a7cba26956cb3da4553cc2056cf67be5b5ff6e6d8fa4fbdff73bfb7ae/aiohttp-3.14.1-cp314-cp314t-win32.whl", hash = "sha256:47ddf841cdecc810749921d25606dee45857d12d2ad5ddb7b5bd7eab12e4b365", size = 494166, upload-time = "2026-06-07T21:09:28.505Z" },
{ url = "https://files.pythonhosted.org/packages/8a/84/ec80c2c1f66a952555a9f86df6b33af65108a6febfa0471b69013a12f807/aiohttp-3.14.1-cp314-cp314t-win_amd64.whl", hash = "sha256:5e78b522b7a6e27e0b25d19b247b75039ac4c94f99823e3c9e53ae1603a9f7e9", size = 530255, upload-time = "2026-06-07T21:09:30.843Z" },
{ url = "https://files.pythonhosted.org/packages/2a/71/6e22be134a4061ada85a92951b842f2657f17d926b727f3f94c56ae963d6/aiohttp-3.14.1-cp314-cp314t-win_arm64.whl", hash = "sha256:90d53f1609c29ccc2193945ef732428382a28f78d0456ae4d3daf0d48b74f0f6", size = 469640, upload-time = "2026-06-07T21:09:33.028Z" },
{ url = "https://files.pythonhosted.org/packages/f8/5c/b3e4ff8ad43a8afef9602c5e90285936da1beaea8b029016b793891f03c3/aiohttp-3.14.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e568e14940c09955aa51f4e645b6daa18a581c5dcfcd73744dcc86a856e3ced3", size = 764250, upload-time = "2026-07-23T01:52:48.525Z" },
{ url = "https://files.pythonhosted.org/packages/0e/da/f1b384465e51449d844056b75070461da03a9a23e6c1747003695bf4172a/aiohttp-3.14.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:54cfcdee2770dac994417cbb0ee1f3eb0e7cb6b30c79bf44f2c02ff79ec5124a", size = 516281, upload-time = "2026-07-23T01:52:51.047Z" },
{ url = "https://files.pythonhosted.org/packages/b9/3f/01264f820ee2e3712a827892b1cd6ff80f3300c1fcbffbb45714a915d47a/aiohttp-3.14.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:21c016079415ed3fd676963e9793700a566d85dbbd6bfc564b9b2d209147dcc8", size = 514742, upload-time = "2026-07-23T01:52:53.779Z" },
{ url = "https://files.pythonhosted.org/packages/9e/8d/a71c6f2db52ac1ed142b133f7feddaa6b70539c3f4de24d7e226c95b794c/aiohttp-3.14.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d6088ec9894113802bddb3c09e974929aed2c7b3a8c456219b8aab4481f1a239", size = 1780613, upload-time = "2026-07-23T01:52:56.948Z" },
{ url = "https://files.pythonhosted.org/packages/a5/11/3dd9b3fb3a170f6ec9011b5291d876a6fab4086714c9e158600edf01b4fd/aiohttp-3.14.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:16ea7e24c309fb7c0bbd505d149abe4fe4dccfb8db911db7dbec0921bc889a6f", size = 1737688, upload-time = "2026-07-23T01:52:59.294Z" },
{ url = "https://files.pythonhosted.org/packages/6d/3e/834c26918be7d88068822b40e0db30fca50b5f4fe79104aa16a93f1d74e6/aiohttp-3.14.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56f355e79f71aef2a85c80305cc915f894b170dba76de5fe84f6351939b83c06", size = 1845742, upload-time = "2026-07-23T01:53:01.641Z" },
{ url = "https://files.pythonhosted.org/packages/cc/c9/49ab8572df7d66bc13d11e31f781292badb04180dd87ba98733066c6aed7/aiohttp-3.14.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:18c441d0a8fca6de8d1f546849b9f0ab20d435993e2c5b59562b2fae6be2f929", size = 1928412, upload-time = "2026-07-23T01:53:04.018Z" },
{ url = "https://files.pythonhosted.org/packages/a5/b9/2b8f0c0ce09c87a1daf80fd483431b56b1435d3f62789bc86f572e1245de/aiohttp-3.14.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:53e7b4ce82b54a8bcc71b3b67a5cbd177ca1d7f592cbc92cd38b7349f73482db", size = 1786220, upload-time = "2026-07-23T01:53:06.481Z" },
{ url = "https://files.pythonhosted.org/packages/85/00/9c45f81de11710460edfa1dc81317b6e882703b160926c879a9d20da9fcc/aiohttp-3.14.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f55119f7bf25f49ed210f6096090715da24f2943c62102448915fde3c62877ce", size = 1637231, upload-time = "2026-07-23T01:53:10.258Z" },
{ url = "https://files.pythonhosted.org/packages/19/ce/967d628e910756f3539c6107cb7844a1b69440dcb3029a5ee7871b09ab63/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9aa6e61fdf20105c4144e755bd586008ff450791d67b1c8146fdc15959c4d51c", size = 1753161, upload-time = "2026-07-23T01:53:13.817Z" },
{ url = "https://files.pythonhosted.org/packages/11/b2/0c3d4114f0aee4f580f5b3b4eb71b24d7a23b834ea506a4dfebe76513f35/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ccd4893707b3e2a13e39c90d43cf80edf2e4d0457935bcc103bf2346214c3f15", size = 1756356, upload-time = "2026-07-23T01:53:16.211Z" },
{ url = "https://files.pythonhosted.org/packages/63/5d/99e7d91c82f1399d1ae2a854e080bd1493fbc31e5e959dbc4ec33dac3bec/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b2466434105a4e03113c36ec775cc2ebe6676b62eae326fa670bb607ef788c1c", size = 1819846, upload-time = "2026-07-23T01:53:18.289Z" },
{ url = "https://files.pythonhosted.org/packages/ad/05/d5e1cb6480eeffd3f901d40a2c5e2d1e7effdc797837da3b490272699f13/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:ba59d59aba08ac02fc03b0c8983ccd5ee39a199d0552ce9e6d2b4845b34d59ae", size = 1628531, upload-time = "2026-07-23T01:53:23.86Z" },
{ url = "https://files.pythonhosted.org/packages/c9/90/b934682bcaefae18a9e04f3dff5b68522ba810906358ae5029b68110ea3b/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ed099d105449c4f9e84f24af203cd131349d4761d8813fa7e02c32e7128cd910", size = 1832712, upload-time = "2026-07-23T01:53:27.551Z" },
{ url = "https://files.pythonhosted.org/packages/21/df/6061679faaf81fac746e7307c7adb71e858071a5d34c27583afefc64f543/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:152516815ef926786a0b6ae2b8f1fd2e0c71582dee0b435636865316fd4891b7", size = 1775014, upload-time = "2026-07-23T01:53:30.223Z" },
{ url = "https://files.pythonhosted.org/packages/8a/1d/f854878bbc69b88faefe924b619a34a6f59ec05fd387c77690667eaa75eb/aiohttp-3.14.3-cp311-cp311-win32.whl", hash = "sha256:a4af35c443e0b1a1bd6a8af3f3485d7fda15c142751a00f3ff8090f0b93346fa", size = 456006, upload-time = "2026-07-23T01:53:34.97Z" },
{ url = "https://files.pythonhosted.org/packages/73/0c/2af9d1674baccd1dbd47282a93d660a22e57ef6167c856deb24b4214fbab/aiohttp-3.14.3-cp311-cp311-win_amd64.whl", hash = "sha256:e1e74298bab6ee0d6e749ed4fd1901c7e604bdda32c03d787a2cc71c46d0433d", size = 481069, upload-time = "2026-07-23T01:53:39.673Z" },
{ url = "https://files.pythonhosted.org/packages/8e/76/88401ff3fc95e85c5fc38d588f36f55e61ecb64343b2bc8d69326f453cc0/aiohttp-3.14.3-cp311-cp311-win_arm64.whl", hash = "sha256:03cd2bde3d7f085b64e549c985f4bb928cad7e8ecf5323bfca320db548d81b39", size = 453021, upload-time = "2026-07-23T01:53:43.749Z" },
{ url = "https://files.pythonhosted.org/packages/18/d4/eb96299230e20acf2efae207cb8d69051f1f68e357e5ea5e479bf6fb097a/aiohttp-3.14.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:39aded8c7f3b935b54aab1d8d73c70ec0ee2d3ec3b943e0e86611bc150ba47f5", size = 754690, upload-time = "2026-07-23T01:53:47.332Z" },
{ url = "https://files.pythonhosted.org/packages/88/11/e7a70a209eb9a067c0d3212b518a0134e3484f5178c7533878b6b514d469/aiohttp-3.14.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5bcb6ff3fdab1258a192679ff1a05d44f59626430aa05cd1a9d2447423599228", size = 509484, upload-time = "2026-07-23T01:53:51.159Z" },
{ url = "https://files.pythonhosted.org/packages/30/07/4bbc222cc8dbe31d4c3e8a5baad2286e4d42026ac0c570027b89afce6344/aiohttp-3.14.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:617105e2c3018ee38d0c8ce5ee3c84f621a6d8b9f723202aacaff28449ca91ee", size = 511949, upload-time = "2026-07-23T01:53:55.083Z" },
{ url = "https://files.pythonhosted.org/packages/54/b9/42e74c46b7b7c794b995bbc1f573fb48950c38b19d8600c62a6804ee2d67/aiohttp-3.14.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f631fe87a6f30df5fbe6d79640b25e4cffb38c31c7fb6f10871517b84b0f8c1a", size = 1765282, upload-time = "2026-07-23T01:53:59.662Z" },
{ url = "https://files.pythonhosted.org/packages/6b/ed/62bc4d74363ad346d518e0720363a949f63e2e23439a79eb5813d4d29bb3/aiohttp-3.14.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a94dbaae5ae27bd849c93570669bff91e0510f33a80805738e3de72a7be0447b", size = 1741511, upload-time = "2026-07-23T01:54:04.063Z" },
{ url = "https://files.pythonhosted.org/packages/d0/9f/181e8a8bc79e47d13c7fc4540bd7a3b729d9505609c61f392a8dd2fbfe55/aiohttp-3.14.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8f2f1c4c032c7cedd7d8da6f54c97b70266c6570c3108d3fdffee7188bb70529", size = 1810680, upload-time = "2026-07-23T01:54:09.882Z" },
{ url = "https://files.pythonhosted.org/packages/5c/9a/dec94d6ad694552fe3424e3f1928d7a606a5d9d9433a04e7ecdd9d38ae7f/aiohttp-3.14.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ea05e1f97ceea523942d9b2a7d7c0359d781d683d6b043f5943a602b14da4787", size = 1905646, upload-time = "2026-07-23T01:54:13.475Z" },
{ url = "https://files.pythonhosted.org/packages/52/b7/7cd31f29d6055bd711ae6e669367fba6f5ae9de463910a793e30556a8db7/aiohttp-3.14.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:543906c127fb1d929b95076db19b83fa2d46751006ff1e23b093aa5ac4d8db42", size = 1792122, upload-time = "2026-07-23T01:54:15.752Z" },
{ url = "https://files.pythonhosted.org/packages/66/73/10b1ef93afa61f4963c746257b70ced619cf31a4798671de5fdb2608501d/aiohttp-3.14.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0a5ff2dfbb9ce645fa5b8ef3e02c6c0b9cc3f6030ff863d0c51fffc50cb5541b", size = 1591127, upload-time = "2026-07-23T01:54:19.489Z" },
{ url = "https://files.pythonhosted.org/packages/49/ed/3b203fa6de1b338c14acdc06bf6ca9b043b7944f005966958c2ced932cde/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:041badb8f84396357c4d3ad26de6afd7a32b112f43d3c63045c0c8278cfd2043", size = 1725210, upload-time = "2026-07-23T01:54:24.129Z" },
{ url = "https://files.pythonhosted.org/packages/28/b7/1c2aab8c706436dcc28598452488ac9cd7c409da815237c28c27d58993e6/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:530125ee1163c4219af35dc3aa1206e541e7b31b6efc1a3f93b70a136f65d427", size = 1764848, upload-time = "2026-07-23T01:54:27.973Z" },
{ url = "https://files.pythonhosted.org/packages/54/50/94c28f08b131c4bf10984ea2c7a536c9920608bb2d6e7f95642c30cc87b7/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c8653fd547c93a61aadc612007790f5555cdd18946fa48cf45e26d8ea4ea473d", size = 1777102, upload-time = "2026-07-23T01:54:31.775Z" },
{ url = "https://files.pythonhosted.org/packages/13/d4/e7d09ba7d345fb2d74440fd2fa033c5e079fac05552927705986f41a364f/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:89176250f686cb9853c0fb7ead90e639e915b84a6f43eedc2a4e7ec21f1037f0", size = 1580205, upload-time = "2026-07-23T01:54:34.518Z" },
{ url = "https://files.pythonhosted.org/packages/a3/84/072a91d68e1e1eb587985b54baab94221277f877e8ef274fc213a0ceae28/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3a26434dafe408229ff3403458ca58de24fb51936504decac49ce6755f77e59d", size = 1797219, upload-time = "2026-07-23T01:54:36.995Z" },
{ url = "https://files.pythonhosted.org/packages/e0/eb/aad34e897e668424d6e995da5dff8a4a09af93363d3392488772957a63aa/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d1558173930a5a8d3069cee5c92fc91c87c4dbcb099debbb3622053717145a19", size = 1768629, upload-time = "2026-07-23T01:54:40.103Z" },
{ url = "https://files.pythonhosted.org/packages/b6/2b/6bb88ddba0fecd9122aa3ebcad25996cf6c083a4a7040dbb3a4f97972af6/aiohttp-3.14.3-cp312-cp312-win32.whl", hash = "sha256:16100ad3ab8d649fdfbee87602d9d2dcdca9df0b9eda8a1b5fdc0d41f96da559", size = 451481, upload-time = "2026-07-23T01:54:42.547Z" },
{ url = "https://files.pythonhosted.org/packages/76/9b/f2f8f108da17ecef2cc3efc424e8b7ad3782b1a8360f7b8eae8ced84f6ea/aiohttp-3.14.3-cp312-cp312-win_amd64.whl", hash = "sha256:33a2d7c28d33797a2e99923dffa63f83d908a19b6bf26cfe80fa790aa5e1a75a", size = 476845, upload-time = "2026-07-23T01:54:44.853Z" },
{ url = "https://files.pythonhosted.org/packages/3e/44/28dac80a8941b604f4da10ce21097614ca1bf905ce93dca28d8d7de9c1e7/aiohttp-3.14.3-cp312-cp312-win_arm64.whl", hash = "sha256:362a3fd481769cac1a824514bcd86fda51c65e8fe6e051099e008fddde6db17c", size = 448050, upload-time = "2026-07-23T01:54:47.087Z" },
{ url = "https://files.pythonhosted.org/packages/57/be/5afd201cc0ab139029aadb75392efe85a293403d9dd3a3226161c21ce00c/aiohttp-3.14.3-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:2e9878ae68e4a5f1c0abe4dd497dbc3d51946f5837b56759e2a02e78fa90ef86", size = 506269, upload-time = "2026-07-23T01:54:49.075Z" },
{ url = "https://files.pythonhosted.org/packages/22/09/dec8189d62b45ade009f6792a2264b942a90cb88aeaf181239933cd72c3c/aiohttp-3.14.3-cp313-cp313-android_21_x86_64.whl", hash = "sha256:f3d2669fe7dec7fc359ecdb5984b29b50d85d5d00f8c1cb61de4f4a24ee42627", size = 515166, upload-time = "2026-07-23T01:54:51.894Z" },
{ url = "https://files.pythonhosted.org/packages/28/24/2854869d29ed8a8b19d74f9ec6629515f7e04d02dd329d9d179201e58e47/aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:cc7cb243a68167172f48c1fd43cee91ec4b1d40cefd190edd43369d1a6bc9c82", size = 486263, upload-time = "2026-07-23T01:54:54.223Z" },
{ url = "https://files.pythonhosted.org/packages/d4/dd/57187c8be2a35aea65eaee3bd2c3dcbbcf0204f5106c89637e3610380cd1/aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:78253b573e6ffab5028924fc98bc281aae05445969982a10864bc360dea2016c", size = 492299, upload-time = "2026-07-23T01:54:56.236Z" },
{ url = "https://files.pythonhosted.org/packages/b9/11/06ae6ed8f0d414edf4068861e233d8fe23ee699bfd4b3ceb8663db948a62/aiohttp-3.14.3-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7041d52c3a7fa20c9e8c182b534704abb19502c8bdcbde7ab23bfda6f642394f", size = 502235, upload-time = "2026-07-23T01:54:58.377Z" },
{ url = "https://files.pythonhosted.org/packages/7e/a3/559639c34a345d2cf7c52dff6838119f2eaf29eb508227b5b83f573af813/aiohttp-3.14.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ac74facc01463f138b0da5580329cfcc82818dea5656e83ddcd11268fc12ff80", size = 750883, upload-time = "2026-07-23T01:55:00.65Z" },
{ url = "https://files.pythonhosted.org/packages/91/cd/41e131f13afd1e7b0172a9d9eda085ef90eb8439f41f0d279db81ed3ae60/aiohttp-3.14.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d6218d92e450824e9b4881f44e8c09f1853b490f9a64130801024a4793b1b3b0", size = 508473, upload-time = "2026-07-23T01:55:02.945Z" },
{ url = "https://files.pythonhosted.org/packages/bc/6b/e7f13410d391c6e55b4c007a8de024355389d7d459e3d64c42b2d33617e5/aiohttp-3.14.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:11fb37ef075669eee52ab1928fbf6e1741fada40409fa309ebde9607a962aebf", size = 509190, upload-time = "2026-07-23T01:55:05.173Z" },
{ url = "https://files.pythonhosted.org/packages/97/21/6464573e53d69672cc1eada3e5c5cb2d2efa82701e8305a0f2047a576967/aiohttp-3.14.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55bdcc472aafe2de4a253045cc128007a64f1e0264fb675791e132ea5edaa3bd", size = 1761478, upload-time = "2026-07-23T01:55:07.383Z" },
{ url = "https://files.pythonhosted.org/packages/1a/81/d217043a4c17fbce360905e3b2bdd20139ebc9a2de836d035d179c4da006/aiohttp-3.14.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c39846c3aad97a8530c89d7a3869a8f8e9e3762c6ac0504481e5c80948f7e807", size = 1735092, upload-time = "2026-07-23T01:55:09.803Z" },
{ url = "https://files.pythonhosted.org/packages/a1/66/e13a02d0eeb1a9a502402a977abb4e4abff9fe4051c26f80558c57a7c975/aiohttp-3.14.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5895ef58c4620afe02fa16044f023dc4dafec08158f9d08874a46a7dbc0341b8", size = 1800546, upload-time = "2026-07-23T01:55:12.012Z" },
{ url = "https://files.pythonhosted.org/packages/26/5e/57d42fca1d18cb5acc1cad945d017fabc5d6ae71d8a08ad66be8dc3ee544/aiohttp-3.14.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa9467a8113aa69d3d7c55a70ef0b7c636010a40993f3df9d9d0d73b3eb7ef24", size = 1895250, upload-time = "2026-07-23T01:55:14.357Z" },
{ url = "https://files.pythonhosted.org/packages/ca/1c/7da8d08e74d56f00070822f9638ff3f1c563f8ad87d1efa996c87bfc8644/aiohttp-3.14.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7d2deec16eeedf55f2c7cf75b521ea3856a5177e123844f8fd0f114ce252cb5", size = 1789289, upload-time = "2026-07-23T01:55:16.668Z" },
{ url = "https://files.pythonhosted.org/packages/cd/0f/cf16bcf56896981c1a0319f5d5db9337994b5165730c48a8fa07e9b34be6/aiohttp-3.14.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dd54d0e8717de95939766febac482ac0474d8ac3b048115f9f2b1d23a16e7db4", size = 1586706, upload-time = "2026-07-23T01:55:18.913Z" },
{ url = "https://files.pythonhosted.org/packages/fe/6f/76eac12a7f2480e1e304f842efdb07db33256b0d9165b866b6ef0806c202/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:df82f3787c940c94986b34222d59c9e38843fba85139f36e85255a82ad5355a9", size = 1724652, upload-time = "2026-07-23T01:55:21.296Z" },
{ url = "https://files.pythonhosted.org/packages/39/b6/19c8c592baeeb94b75f966547d40c02ac7590902306ec5863d5c027cf506/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:42a67efc36300d052fb4508a53e8b6901b9284b599ae63945c377569c5fcc1e1", size = 1756239, upload-time = "2026-07-23T01:55:23.705Z" },
{ url = "https://files.pythonhosted.org/packages/dc/c9/4e9383150296f97f873b680c4de8fb2cd88608fb9f48c79edcb111611abc/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7a75aa63cbf9b21cfaf60dc2657e19df2c2867d91707d653fee171ffeedd1371", size = 1769161, upload-time = "2026-07-23T01:55:26.082Z" },
{ url = "https://files.pythonhosted.org/packages/aa/1e/147bdc6cc5de5f3ab011be8bf5d6e786633249f22c20bae06f85e45f5387/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e92eb8acc45eb6a9f4935071a77edf5b85cc6f8dfad5cd99e97653c26593cdde", size = 1578759, upload-time = "2026-07-23T01:55:28.846Z" },
{ url = "https://files.pythonhosted.org/packages/fd/31/78388a9d6040ece2e11df62ea229a822cf5e52d238374b220ae9975b2623/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b014a6ed7cf912e787149fdc529166d3ceabac23f26efeea3158c9aba2354e7e", size = 1792025, upload-time = "2026-07-23T01:55:31.457Z" },
{ url = "https://files.pythonhosted.org/packages/03/51/a3d29fdf2c25d796746af8ad6fe56a45d6256c38b0a8a2ed752e1160b3a2/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3d4f72af88ac2474bb5bca640030320e3d38a0163a1d7533500e87be458eef71", size = 1768477, upload-time = "2026-07-23T01:55:33.87Z" },
{ url = "https://files.pythonhosted.org/packages/29/a6/442e18b5afeade534d877a2dc3c3e392aff8d49787890b0cf84790410267/aiohttp-3.14.3-cp313-cp313-win32.whl", hash = "sha256:5f08ec777f35ee70720233b8b9811d3bb5d728137f30ac91b7457709c3261ac0", size = 451069, upload-time = "2026-07-23T01:55:36.121Z" },
{ url = "https://files.pythonhosted.org/packages/9d/69/3d876ac02659f271cf7f6769f14a8e3de5b6e888ed8b5a7e998086a4cec8/aiohttp-3.14.3-cp313-cp313-win_amd64.whl", hash = "sha256:dff9461ec275f22135650d5ba4b4931a11f3958df7dfbb8db630000d4dee0883", size = 476518, upload-time = "2026-07-23T01:55:38.303Z" },
{ url = "https://files.pythonhosted.org/packages/b2/0e/50d6e6471cd31edce8b282bdec59375a3a69124d8a989a0b1313355cae52/aiohttp-3.14.3-cp313-cp313-win_arm64.whl", hash = "sha256:ddcac3c6b382e81f1dd0499199d4136b877beb4cb5ef770bbbfba56c4b8f55d2", size = 447676, upload-time = "2026-07-23T01:55:40.451Z" },
{ url = "https://files.pythonhosted.org/packages/c8/20/887fdcf832326571b370ffc347b3e70abe101096f3720126aac161b1d872/aiohttp-3.14.3-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:49f7325beb0f85ef4aef5f48f490269575f83e6e2acad00a1d80b807eb027062", size = 509067, upload-time = "2026-07-23T01:55:42.618Z" },
{ url = "https://files.pythonhosted.org/packages/ad/a3/92cec936f78cc4bf0fa5554ebe593b73459d94e3c62303e1902a4cccb6f7/aiohttp-3.14.3-cp314-cp314-android_24_x86_64.whl", hash = "sha256:e3be98a7c30b8c25d573dafba7171d66dfb05ee6a9070fc46535464ff97700a6", size = 514774, upload-time = "2026-07-23T01:55:44.937Z" },
{ url = "https://files.pythonhosted.org/packages/29/ba/2a0c38df3fc557620b6a5acd98364af050053b6285b4dc7ee74100c63c18/aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:614c61d478b83953e261d02bb2df750f17227cd33ef8002945bf5aebbde21919", size = 488134, upload-time = "2026-07-23T01:55:47.135Z" },
{ url = "https://files.pythonhosted.org/packages/48/d6/d51b7d4bf309af3693940d8ffd2b9ed0b682434ef85959b7c9c137f60cf8/aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:1caa7b0d05f3e3a36f87788c59e970a7ee1cefcfcbb924a9f138c4a6551c9cb7", size = 494201, upload-time = "2026-07-23T01:55:49.451Z" },
{ url = "https://files.pythonhosted.org/packages/3f/5a/8f624384e5f1efabb5229b94157eb966b021e97bdb188c62860c2ae243c2/aiohttp-3.14.3-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:dfa68deb2a443bdaa3ea5297b0699c1464f08aef3812b486d1348eee61b07dc0", size = 502766, upload-time = "2026-07-23T01:55:51.656Z" },
{ url = "https://files.pythonhosted.org/packages/a6/26/4ff0164370deec18fb19254ee4ab10b7a73304ac0c860b13f5f84663759b/aiohttp-3.14.3-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:e72ee89e28d907a18f46959b4eb0bb06701cc7f8cf4366e00029e2ccfaaf5924", size = 756557, upload-time = "2026-07-23T01:55:53.964Z" },
{ url = "https://files.pythonhosted.org/packages/97/a3/7056b86dc0d9ec709ea9777eae3b0161428f943372f8b98c01c11593b682/aiohttp-3.14.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ad4c8b7488d745d2ca4838ebd8ae5ba9b56341d30b1da43640e4ce87f9f49646", size = 510168, upload-time = "2026-07-23T01:55:56.22Z" },
{ url = "https://files.pythonhosted.org/packages/85/ed/0357a015892fd68058bf2d39d3fd1958e459b997a7db30aaa6aaa434ae96/aiohttp-3.14.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:db332af25642007330fca8be5c4d194caf2bea7a7fc84415aff3497af5dfee6b", size = 512957, upload-time = "2026-07-23T01:55:58.437Z" },
{ url = "https://files.pythonhosted.org/packages/47/d1/8aba53f15ccb2238405f5e9d30e2a8ca44f93878c26e7165ade00d374b1c/aiohttp-3.14.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25bd2708db6bdf6a6630dd37bdcdfcb47c4434d22ac69c64665b802910140b30", size = 1750149, upload-time = "2026-07-23T01:56:00.856Z" },
{ url = "https://files.pythonhosted.org/packages/49/bd/40c3fee327529284375c6701cbb0fa4600cc2e8432af1378f897e2ef7d3a/aiohttp-3.14.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:cef89a58e628c4efcac3275c2d68083f82426dcdc89c1492a6f654f9f7ea6ab9", size = 1707685, upload-time = "2026-07-23T01:56:03.371Z" },
{ url = "https://files.pythonhosted.org/packages/2a/a3/ca0cc6724cca8114b05694abd916060758c79894c3aa5b012cdadc1bc28e/aiohttp-3.14.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c23ec8ee9d5ab2f5421f9c7fffce208435607af27fd46d4a44e031954352838f", size = 1803911, upload-time = "2026-07-23T01:56:05.817Z" },
{ url = "https://files.pythonhosted.org/packages/95/b5/85b099c299c3ffd38ad9b3e43694c8a346934e4a30c88c4fd5a841234f77/aiohttp-3.14.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e2667f0bbe7eb6c74eae5e9691441ad186e5845ca3cff63230fc09c4e7514f5d", size = 1876929, upload-time = "2026-07-23T01:56:08.413Z" },
{ url = "https://files.pythonhosted.org/packages/d5/b7/1da684a04175473fa4cddbf9a2f572e79514c3fd27a74597f43057d4f3da/aiohttp-3.14.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18cb43369747b2ae007bd2655fb8e63a099c2ff1d207962943636dac989b3147", size = 1761112, upload-time = "2026-07-23T01:56:10.918Z" },
{ url = "https://files.pythonhosted.org/packages/d1/16/bc4b55e3e5cb175fd69c53c90d60d2f47797cb343da5106e23863dc4dba4/aiohttp-3.14.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d77640cc618c1d99fc4f8589c0f24a730adfa54eb1e57ef7bf0c8dfb78da898c", size = 1583500, upload-time = "2026-07-23T01:56:13.613Z" },
{ url = "https://files.pythonhosted.org/packages/2a/e8/13a9d957a1ee40837f46aa30f0f4c657e673ad86a2e6362a9f9be20d26d9/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:53e5179d8abb5710f8e83ba207c41c8d1261fcffd4616500e15ca2b7a33be10a", size = 1713940, upload-time = "2026-07-23T01:56:15.969Z" },
{ url = "https://files.pythonhosted.org/packages/38/05/d33c680c1bcf1c7e130f9cbfc1fc02fe8bb0c4af2a94a53dd5fb56131e5c/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:cd817772b2fcf2b8c0905795318485f9ec16eae60b29feb7f4c77085311637f0", size = 1724413, upload-time = "2026-07-23T01:56:18.591Z" },
{ url = "https://files.pythonhosted.org/packages/85/1d/af798d306f7a74b6a632dbcabcf62a4c91391b7582d2a8c6d7712e2cc54e/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4e3ac92d90e92773b2362d506068e9a948192bd553e743c5b2429e28527c8661", size = 1770748, upload-time = "2026-07-23T01:56:21.074Z" },
{ url = "https://files.pythonhosted.org/packages/a8/92/ad720d472556a995049206867765e9410969684f86ee09423ff9969044c1/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3f42e9b78301f11c8f861746175d8b9c1ccef713fcad9eab396e2f6db8ed4a22", size = 1577564, upload-time = "2026-07-23T01:56:23.475Z" },
{ url = "https://files.pythonhosted.org/packages/60/ad/0ed7586cbef7a884e23a752fa2bb987a122e6a5dd50dab109258d0a95193/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9d9edccfe496b476db5f398d97b865e9a6752bcf8aec4eef8390ce20fb64bb41", size = 1782080, upload-time = "2026-07-23T01:56:25.994Z" },
{ url = "https://files.pythonhosted.org/packages/97/ea/dbaed0d73e8a69aad653b045dab451c67c2454bb731a37b45a86593e9422/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1c5ec8fb1bcc31a8466f74aaf26c345d5c386fa4bd08a3f0eb9c7a4a3fe8b5bf", size = 1745813, upload-time = "2026-07-23T01:56:28.604Z" },
{ url = "https://files.pythonhosted.org/packages/81/1b/6893d4bc57e434fc93a6c9217c637d967a0b651d989f6e3265179375754a/aiohttp-3.14.3-cp314-cp314-win32.whl", hash = "sha256:38901a84da3ce22249f6e860bf8f90d141bcab7da090cc398f8bb58c0e44b7da", size = 455872, upload-time = "2026-07-23T01:56:31.031Z" },
{ url = "https://files.pythonhosted.org/packages/f5/8b/c7baa1ba1eda4db6989baefe5de6d99834921b84ebd7918624febcb9f290/aiohttp-3.14.3-cp314-cp314-win_amd64.whl", hash = "sha256:8b3b60de05f3dcb6f6a00f818bb2ec781cee4de0645f59ccaf99b1d1823b6100", size = 481030, upload-time = "2026-07-23T01:56:33.365Z" },
{ url = "https://files.pythonhosted.org/packages/22/8c/c29d067df825a2df88ca432db848aa2fe8199598359cc06c12b09320cac9/aiohttp-3.14.3-cp314-cp314-win_arm64.whl", hash = "sha256:1576145bdceeb92382d899751e12743a3a5b8e460a841e3e50543859e54864dc", size = 453669, upload-time = "2026-07-23T01:56:35.731Z" },
{ url = "https://files.pythonhosted.org/packages/6a/a4/9c033beb355d39b6147980597ec9645e4729243f686ee4dc73945de72030/aiohttp-3.14.3-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:8800c996b01c2772a783e3e46f3e1abd5823029adca0df54231960de9bfefa5b", size = 791403, upload-time = "2026-07-23T01:56:37.972Z" },
{ url = "https://files.pythonhosted.org/packages/80/ca/87c32a0a7704583cfc49660bd817889bae5b830bf53b5dcb4e92145ac2da/aiohttp-3.14.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ebe8e504f058fe91223351cecd2d9d6946c9d241bb0250d898ffbdf584cc72b0", size = 526413, upload-time = "2026-07-23T01:56:40.523Z" },
{ url = "https://files.pythonhosted.org/packages/9e/d8/8ec0e471248c500acdce2be3f46db8fb62b5eb60efef072529cc85ee1d26/aiohttp-3.14.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:30402d03a7c0ff52bce290b57e564e9079fd9d0cb545c8aba73f86a103162d2e", size = 532135, upload-time = "2026-07-23T01:56:42.876Z" },
{ url = "https://files.pythonhosted.org/packages/fe/45/f8919fd936e8b79fcd9bda7b6d8e62613462a713f4f17987fd7c34399142/aiohttp-3.14.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9fc7b5bfec6573f3ae844f457fdde5adeb713f8b8e4a81ad64fc207b49383716", size = 1922742, upload-time = "2026-07-23T01:56:45.528Z" },
{ url = "https://files.pythonhosted.org/packages/f6/ec/9ca76b28a27525b0cc53e20842e0228b022f301ce1f436b7d814b4aaf2df/aiohttp-3.14.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8a5fd34f7f7410d1730d5c2ba873cacb2eed3fede366feb268a70ba22581ed8f", size = 1787371, upload-time = "2026-07-23T01:56:48.045Z" },
{ url = "https://files.pythonhosted.org/packages/b1/04/6acdbf17315f7b55f1937e3387acb89a3cddeb4995689553d064af8e92ab/aiohttp-3.14.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:270d3dace9ca2f10f0da5d8ebe519b7a310fc6112ed916e32df5866df0888553", size = 1912623, upload-time = "2026-07-23T01:56:50.605Z" },
{ url = "https://files.pythonhosted.org/packages/86/e6/438b0c79ca6f45eb9fd9817dd4c01a91919a38c0de5ee9e05e2b4dc0ece7/aiohttp-3.14.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3ae5b3a59436d089b5395d910121a390feed4d00578eb95a0fd1a329fe963100", size = 2005515, upload-time = "2026-07-23T01:56:53.153Z" },
{ url = "https://files.pythonhosted.org/packages/bb/6b/62cbd6577758699525f5c712d1ddef57d9875fbab0ae8d5f5a202fd598f8/aiohttp-3.14.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2498f0fe69ead802f9675beca44a7c21c62fdaa4ec5145ea1c3ad6edbee29f85", size = 1879906, upload-time = "2026-07-23T01:56:55.818Z" },
{ url = "https://files.pythonhosted.org/packages/00/95/18bcbf830a21dc3aae24d8f6b6feaf3db1d2090242d00a7868db2ffb0b67/aiohttp-3.14.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a0dc483c00da8b673abbb367eb6f8d8f4bcec30eb58529ea13cb42e7fd2dfa33", size = 1675849, upload-time = "2026-07-23T01:56:58.861Z" },
{ url = "https://files.pythonhosted.org/packages/a9/19/47f4968659c5e23606c3790c80fc624e691c153d036148449ee84d31b287/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c7d3a97c678d34fc5b59da671ee9cd630096ddc643e7b5a30d54a2a6f3574d3f", size = 1843496, upload-time = "2026-07-23T01:57:01.591Z" },
{ url = "https://files.pythonhosted.org/packages/64/af/38c33c4dd82fddcb4e56c4653b6f1072a8edbc6b7fa15809f14932c41e2d/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:f8fb78a83c9e5f741ca3a68cfb455c1f5bb83b4e7249a3848b3cd78d0a8563b0", size = 1827746, upload-time = "2026-07-23T01:57:05.131Z" },
{ url = "https://files.pythonhosted.org/packages/a1/9d/0537cda4885ac8f5b7053d164dd06312f4c483a4edcb8ee5b8aaf2a989bf/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:74ab5b6a9fb13e873e5a90946588baecaf488745e1db1a4a5c433f971f035098", size = 1853810, upload-time = "2026-07-23T01:57:08.043Z" },
{ url = "https://files.pythonhosted.org/packages/19/fe/26f9c5e6458385aa86497836b0dea6fb2f027827d63f37c7856cce9286ee/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd52f811e65f6fb634b1047159657c98f52b407f8efec907bcfc09da9a4c0a25", size = 1668895, upload-time = "2026-07-23T01:57:10.837Z" },
{ url = "https://files.pythonhosted.org/packages/ec/4c/618b1db9b9ba079b8875d2cdf78e7c4a3bf72903bd5850fee7dd9544600a/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:f0f177d1b195b9e06376cfd7d308d8a1b920909a609d03ac82a8c73bbb16d3b9", size = 1883833, upload-time = "2026-07-23T01:57:13.672Z" },
{ url = "https://files.pythonhosted.org/packages/94/c6/bd959bd1e4771f9fd944e9e436224c48c77b018b73b519b5aad346335bcc/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:498c6c623134f8e09a3c4e60bcd607a0b4590dd7dbf08dd40851b27cbb520ccb", size = 1844251, upload-time = "2026-07-23T01:57:16.593Z" },
{ url = "https://files.pythonhosted.org/packages/5e/19/08d41839658bdd44a0ed2480f3891705ecb487ce28c0dde62c9040c997e0/aiohttp-3.14.3-cp314-cp314t-win32.whl", hash = "sha256:b304db572b4368edd8dda8a2274f73156fe15558fca4a917cb8a09fc47af5963", size = 474180, upload-time = "2026-07-23T01:57:19.306Z" },
{ url = "https://files.pythonhosted.org/packages/99/5d/3cd6ef0a2b2851f7ab913b5b079334781bd50ff56a323e4454063377a080/aiohttp-3.14.3-cp314-cp314t-win_amd64.whl", hash = "sha256:b20032766aedf6261c7a566585a40867d092ac03a0d81592d5370ef9b054f99b", size = 500528, upload-time = "2026-07-23T01:57:21.762Z" },
{ url = "https://files.pythonhosted.org/packages/a4/37/cfd1ed540a4d318da025590d96b728e63713c09e9377950fc655dadeb856/aiohttp-3.14.3-cp314-cp314t-win_arm64.whl", hash = "sha256:2e1161602f45a54de2ce0905243a95f58cb42dcd378402f3697f5e0b21e9d2e7", size = 469280, upload-time = "2026-07-23T01:57:24.241Z" },
]
[[package]]
@@ -977,61 +977,58 @@ sdist = { url = "https://files.pythonhosted.org/packages/6b/b0/e595ce2a2527e169c
[[package]]
name = "cryptography"
version = "48.0.0"
version = "50.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9f/a9/db8f313fdcd85d767d4973515e1db101f9c71f95fced83233de224673757/cryptography-48.0.0.tar.gz", hash = "sha256:5c3932f4436d1cccb036cb0eaef46e6e2db91035166f1ad6505c3c9d5a635920", size = 832984, upload-time = "2026-05-04T22:59:38.133Z" }
sdist = { url = "https://files.pythonhosted.org/packages/de/41/6cbdcf9142d00fe82836fbb51e503e58088575cf7a0fe1dbff6695bf0840/cryptography-50.0.0.tar.gz", hash = "sha256:eeac2acb5a20ed25e0ad6d1df9891a520b78b404266b6d11778f25d5d691a6c9", size = 880201, upload-time = "2026-07-31T14:25:10.11Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/df/3d/01f6dd9190170a5a241e0e98c2d04be3664a9e6f5b9b872cde63aff1c3dd/cryptography-48.0.0-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:0c558d2cdffd8f4bbb30fc7134c74d2ca9a476f830bb053074498fbc86f41ed6", size = 8001587, upload-time = "2026-05-04T22:57:36.803Z" },
{ url = "https://files.pythonhosted.org/packages/b2/6e/e90527eef33f309beb811cf7c982c3aeffcce8e3edb178baa4ca3ae4a6fa/cryptography-48.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f5333311663ea94f75dd408665686aaf426563556bb5283554a3539177e03b8c", size = 4690433, upload-time = "2026-05-04T22:57:40.373Z" },
{ url = "https://files.pythonhosted.org/packages/90/04/673510ed51ddff56575f306cf1617d80411ee76831ccd3097599140efdfe/cryptography-48.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7995ef305d7165c3f11ae07f2517e5a4f1d5c18da1376a0a9ed496336b69e5f3", size = 4710620, upload-time = "2026-05-04T22:57:42.935Z" },
{ url = "https://files.pythonhosted.org/packages/14/d5/e9c4ef932c8d800490c34d8bd589d64a31d5890e27ec9e9ad532be893294/cryptography-48.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:40ba1f85eaa6959837b1d51c9767e230e14612eea4ef110ee8854ada22da1bf5", size = 4696283, upload-time = "2026-05-04T22:57:45.294Z" },
{ url = "https://files.pythonhosted.org/packages/0c/29/174b9dfb60b12d59ecfc6cfa04bc88c21b42a54f01b8aae09bb6e51e4c7f/cryptography-48.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:369a6348999f94bbd53435c894377b20ab95f25a9065c283570e70150d8abc3c", size = 5296573, upload-time = "2026-05-04T22:57:47.933Z" },
{ url = "https://files.pythonhosted.org/packages/95/38/0d29a6fd7d0d1373f0c0c88a04ba20e359b257753ac497564cd660fc1d55/cryptography-48.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a0e692c683f4df67815a2d258b324e66f4738bd7a96a218c826dce4f4bd05d8f", size = 4743677, upload-time = "2026-05-04T22:57:50.067Z" },
{ url = "https://files.pythonhosted.org/packages/30/be/eef653013d5c63b6a490529e0316f9ac14a37602965d4903efed1399f32b/cryptography-48.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:18349bbc56f4743c8b12dc32e2bccb2cf83ee8b69a3bba74ef8ae857e26b3d25", size = 4330808, upload-time = "2026-05-04T22:57:52.301Z" },
{ url = "https://files.pythonhosted.org/packages/84/9e/500463e87abb7a0a0f9f256ec21123ecde0a7b5541a15e840ea54551fd81/cryptography-48.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e8eac43dfca5c4cccc6dad9a80504436fca53bb9bc3100a2386d730fbe6b602", size = 4695941, upload-time = "2026-05-04T22:57:54.603Z" },
{ url = "https://files.pythonhosted.org/packages/e3/dc/7303087450c2ec9e7fbb750e17c2abfbc658f23cbd0e54009509b7cc4091/cryptography-48.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9ccdac7d40688ecb5a3b4a604b8a88c8002e3442d6c60aead1db2a89a041560c", size = 5252579, upload-time = "2026-05-04T22:57:57.207Z" },
{ url = "https://files.pythonhosted.org/packages/d0/c0/7101d3b7215edcdc90c45da544961fd8ed2d6448f77577460fa75a8443f7/cryptography-48.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:bd72e68b06bb1e96913f97dd4901119bc17f39d4586a5adf2d3e47bc2b9d58b5", size = 4743326, upload-time = "2026-05-04T22:57:59.535Z" },
{ url = "https://files.pythonhosted.org/packages/ac/d8/5b833bad13016f562ab9d063d68199a4bd121d18458e439515601d3357ec/cryptography-48.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:59baa2cb386c4f0b9905bd6eb4c2a79a69a128408fd31d32ca4d7102d4156321", size = 4826672, upload-time = "2026-05-04T22:58:01.996Z" },
{ url = "https://files.pythonhosted.org/packages/98/e1/7074eb8bf3c135558c73fc2bcf0f5633f912e6fb87e868a55c454080ef09/cryptography-48.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9249e3cd978541d665967ac2cb2787fd6a62bddf1e75b3e347a594d7dacf4f74", size = 4972574, upload-time = "2026-05-04T22:58:03.968Z" },
{ url = "https://files.pythonhosted.org/packages/04/70/e5a1b41d325f797f39427aa44ef8baf0be500065ab6d8e10369d850d4a4f/cryptography-48.0.0-cp311-abi3-win32.whl", hash = "sha256:9c459db21422be75e2809370b829a87eb37f74cd785fc4aa9ea1e5f43b47cda4", size = 3294868, upload-time = "2026-05-04T22:58:06.467Z" },
{ url = "https://files.pythonhosted.org/packages/f4/ac/8ac51b4a5fc5932eb7ee5c517ba7dc8cd834f0048962b6b352f00f41ebf9/cryptography-48.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:5b012212e08b8dd5edc78ef54da83dd9892fd9105323b3993eff6bea65dc21d7", size = 3817107, upload-time = "2026-05-04T22:58:08.845Z" },
{ url = "https://files.pythonhosted.org/packages/6b/84/70e3feea9feea87fd7cbe77efb2712ae1e3e6edf10749dc6e95f4e60e455/cryptography-48.0.0-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:3cb07a3ed6431663cd321ea8a000a1314c74211f823e4177fefa2255e057d1ec", size = 7986556, upload-time = "2026-05-04T22:58:11.172Z" },
{ url = "https://files.pythonhosted.org/packages/89/6e/18e07a618bb5442ba10cf4df16e99c071365528aa570dfcb8c02e25a303b/cryptography-48.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c7378637d7d88016fa6791c159f698b3d3eed28ebf844ac36b9dc04a14dae18", size = 4684776, upload-time = "2026-05-04T22:58:13.712Z" },
{ url = "https://files.pythonhosted.org/packages/be/6a/4ea3b4c6c6759794d5ee2103c304a5076dc4b19ae1f9fe47dba439e159e9/cryptography-48.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc90c0b39b2e3c65ef52c804b72e3c58f8a04ab2a1871272798e5f9572c17d20", size = 4698121, upload-time = "2026-05-04T22:58:16.448Z" },
{ url = "https://files.pythonhosted.org/packages/2f/59/6ff6ad6cae03bb887da2a5860b2c9805f8dac969ef01ce563336c49bd1d1/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:76341972e1eff8b4bea859f09c0d3e64b96ce931b084f9b9b7db8ef364c30eff", size = 4690042, upload-time = "2026-05-04T22:58:18.544Z" },
{ url = "https://files.pythonhosted.org/packages/ca/b4/fc334ed8cfd705aca282fe4d8f5ae64a8e0f74932e9feecb344610cf6e4d/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:55b7718303bf06a5753dcdccf2f3945cf18ad7bffde41b61226e4db31ab89a9c", size = 5282526, upload-time = "2026-05-04T22:58:20.75Z" },
{ url = "https://files.pythonhosted.org/packages/11/08/9f8c5386cc4cd90d8255c7cdd0f5baf459a08502a09de30dc51f553d38dc/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:a64697c641c7b1b2178e573cbc31c7c6684cd56883a478d75143dbb7118036db", size = 4733116, upload-time = "2026-05-04T22:58:23.627Z" },
{ url = "https://files.pythonhosted.org/packages/b8/77/99307d7574045699f8805aa500fa0fb83422d115b5400a064ddd306d7750/cryptography-48.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:561215ea3879cb1cbbf272867e2efda62476f240fb58c64de6b393ae19246741", size = 4316030, upload-time = "2026-05-04T22:58:25.581Z" },
{ url = "https://files.pythonhosted.org/packages/fd/36/a608b98337af3cb2aff4818e406649d30572b7031918b04c87d979495348/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ad64688338ed4bc1a6618076ba75fd7194a5f1797ac60b47afe926285adb3166", size = 4689640, upload-time = "2026-05-04T22:58:27.747Z" },
{ url = "https://files.pythonhosted.org/packages/dd/a6/825010a291b4438aecc1f568bc428189fc1175515223632477c07dc0a6df/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:906cbf0670286c6e0044156bc7d4af9cbb0ef6db9f73e52c3ec56ba6bdde5336", size = 5237657, upload-time = "2026-05-04T22:58:29.848Z" },
{ url = "https://files.pythonhosted.org/packages/b9/09/4e76a09b4caa29aad535ddc806f5d4c5d01885bd978bd984fbc6ca032cae/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:ea8990436d914540a40ab24b6a77c0969695ed52f4a4874c5137ccf7045a7057", size = 4732362, upload-time = "2026-05-04T22:58:32.009Z" },
{ url = "https://files.pythonhosted.org/packages/18/78/444fa04a77d0cb95f417dda20d450e13c56ba8e5220fc892a1658f44f882/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c18684a7f0cc9a3cb60328f496b8e3372def7c5d2df39ac267878b05565aaaae", size = 4819580, upload-time = "2026-05-04T22:58:34.254Z" },
{ url = "https://files.pythonhosted.org/packages/38/85/ea67067c70a1fd4be2c63d35eeed82658023021affccc7b17705f8527dd2/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9be5aafa5736574f8f15f262adc81b2a9869e2cfe9014d52a44633905b40d52c", size = 4963283, upload-time = "2026-05-04T22:58:36.376Z" },
{ url = "https://files.pythonhosted.org/packages/75/54/cc6d0f3deac3e81c7f847e8a189a12b6cdd65059b43dad25d4316abd849a/cryptography-48.0.0-cp314-cp314t-win32.whl", hash = "sha256:c17dfe85494deaeddc5ce251aebd1d60bbe6afc8b62071bb0b469431a000124f", size = 3270954, upload-time = "2026-05-04T22:58:38.791Z" },
{ url = "https://files.pythonhosted.org/packages/49/67/cc947e288c0758a4e5473d1dcb743037ab7785541265a969240b8885441a/cryptography-48.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27241b1dc9962e056062a8eef1991d02c3a24569c95975bd2322a8a52c6e5e12", size = 3797313, upload-time = "2026-05-04T22:58:40.746Z" },
{ url = "https://files.pythonhosted.org/packages/f2/63/61d4a4e1c6b6bab6ce1e213cd36a24c415d90e76d78c5eb8577c5541d2e8/cryptography-48.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:58d00498e8933e4a194f3076aee1b4a97dfec1a6da444535755822fe5d8b0b86", size = 7983482, upload-time = "2026-05-04T22:58:43.769Z" },
{ url = "https://files.pythonhosted.org/packages/d5/ac/f5b5995b87770c693e2596559ffafe195b4033a57f14a82268a2842953f3/cryptography-48.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:614d0949f4790582d2cc25553abd09dd723025f0c0e7c67376a1d77196743d6e", size = 4683266, upload-time = "2026-05-04T22:58:46.064Z" },
{ url = "https://files.pythonhosted.org/packages/ec/c6/8b14f67e18338fbc4adb76f66c001f5c3610b3e2d1837f268f47a347dbbb/cryptography-48.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ce4bfae76319a532a2dc68f82cc32f5676ee792a983187dac07183690e5c66f", size = 4696228, upload-time = "2026-05-04T22:58:48.22Z" },
{ url = "https://files.pythonhosted.org/packages/ea/73/f808fbae9514bd91b47875b003f13e284c8c6bdfd904b7944e803937eec1/cryptography-48.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:2eb992bbd4661238c5a397594c83f5b4dc2bc5b848c365c8f991b6780efcc5c7", size = 4689097, upload-time = "2026-05-04T22:58:50.9Z" },
{ url = "https://files.pythonhosted.org/packages/93/01/d86632d7d28db8ae83221995752eeb6639ffb374c2d22955648cf8d52797/cryptography-48.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:22a5cb272895dce158b2cacdfdc3debd299019659f42947dbdac6f32d68fe832", size = 5283582, upload-time = "2026-05-04T22:58:53.017Z" },
{ url = "https://files.pythonhosted.org/packages/02/e1/50edc7a50334807cc4791fc4a0ce7468b4a1416d9138eab358bfc9a3d70b/cryptography-48.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2b4d59804e8408e2fea7d1fbaf218e5ec984325221db76e6a241a9abd6cdd95c", size = 4730479, upload-time = "2026-05-04T22:58:55.611Z" },
{ url = "https://files.pythonhosted.org/packages/6f/af/99a582b1b1641ff5911ac559beb45097cf79efd4ead4657f578ef1af2d47/cryptography-48.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:984a20b0f62a26f48a3396c72e4bc34c66e356d356bf370053066b3b6d54634a", size = 4326481, upload-time = "2026-05-04T22:58:57.607Z" },
{ url = "https://files.pythonhosted.org/packages/90/ee/89aa26a06ef0a7d7611788ffd571a7c50e368cc6a4d5eef8b4884e866edb/cryptography-48.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5a5ed8fde7a1d09376ca0b40e68cd59c69fe23b1f9768bd5824f54681626032a", size = 4688713, upload-time = "2026-05-04T22:59:00.077Z" },
{ url = "https://files.pythonhosted.org/packages/70/ba/bcb1b0bb7a33d4c7c0c4d4c7874b4a62ae4f56113a5f4baefa362dfb1f0f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:8cd666227ef7af430aa5914a9910e0ddd703e75f039cef0825cd0da71b6b711a", size = 5238165, upload-time = "2026-05-04T22:59:02.317Z" },
{ url = "https://files.pythonhosted.org/packages/c9/70/ca4003b1ce5ca3dc3186ada51908c8a9b9ff7d5cab83cc0d43ee14ec144f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9071196d81abc88b3516ac8cdfad32e2b66dd4a5393a8e68a961e9161ddc6239", size = 4729947, upload-time = "2026-05-04T22:59:05.255Z" },
{ url = "https://files.pythonhosted.org/packages/44/a0/4ec7cf774207905aef1a8d11c3750d5a1db805eb380ee4e16df317870128/cryptography-48.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e2d54c8be6152856a36f0882ab231e70f8ec7f14e93cf87db8a2ed056bf160c", size = 4822059, upload-time = "2026-05-04T22:59:07.802Z" },
{ url = "https://files.pythonhosted.org/packages/1e/75/a2e55f99c16fcac7b5d6c1eb19ad8e00799854d6be5ca845f9259eae1681/cryptography-48.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a5da777e32ffed6f85a7b2b3f7c5cbc88c146bfcd0a1d7baf5fcc6c52ee35dd4", size = 4960575, upload-time = "2026-05-04T22:59:09.851Z" },
{ url = "https://files.pythonhosted.org/packages/b8/23/6e6f32143ab5d8b36ca848a502c4bcd477ae75b9e1677e3530d669062578/cryptography-48.0.0-cp39-abi3-win32.whl", hash = "sha256:77a2ccbbe917f6710e05ba9adaa25fb5075620bf3ea6fb751997875aff4ae4bd", size = 3279117, upload-time = "2026-05-04T22:59:12.019Z" },
{ url = "https://files.pythonhosted.org/packages/9d/9a/0fea98a70cf1749d41d738836f6349d97945f7c89433a259a6c2642eefeb/cryptography-48.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:16cd65b9330583e4619939b3a3843eec1e6e789744bb01e7c7e2e62e33c239c8", size = 3792100, upload-time = "2026-05-04T22:59:14.884Z" },
{ url = "https://files.pythonhosted.org/packages/be/d2/024b5e06be9d44cb021fb0e1a03d34d63989cf56a0fe62f3dfbab695b9b4/cryptography-48.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:84cf79f0dc8b36ac5da873481716e87aef31fcfa0444f9e1d8b4b2cece142855", size = 3950391, upload-time = "2026-05-04T22:59:17.415Z" },
{ url = "https://files.pythonhosted.org/packages/bc/17/3861e17c56fa0fd37491a14a8673fdb77c57fc5693cafe745ea8b06dba75/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:fdfef35d751d510fcef5252703621574364fec16418c4a1e5e1055248401054b", size = 4637126, upload-time = "2026-05-04T22:59:20.197Z" },
{ url = "https://files.pythonhosted.org/packages/f0/0a/7e226dbff530f21480727eb764973a7bff2b912f8e15cd4f129e71b56d1d/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:0890f502ddf7d9c6426129c3f49f5c0a39278ed7cd6322c8755ffca6ee675a13", size = 4667270, upload-time = "2026-05-04T22:59:22.647Z" },
{ url = "https://files.pythonhosted.org/packages/3b/f2/5a72274ca9f1b2a8b44a662ee0bf1b435909deb473d6f97bcd035bcdbc71/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:ecde28a596bead48b0cfd2a1b4416c3d43074c2d785e3a398d7ec1fc4d0f7fbb", size = 4636797, upload-time = "2026-05-04T22:59:24.912Z" },
{ url = "https://files.pythonhosted.org/packages/b4/e1/48cedb2fe63626e91ded1edad159e2a4fb8b6906c4425eb7749673077ce7/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:4defde8685ae324a9eb9d818717e93b4638ef67070ac9bc15b8ca85f63048355", size = 4666800, upload-time = "2026-05-04T22:59:27.474Z" },
{ url = "https://files.pythonhosted.org/packages/a2/ca/7e8365deec19afb2b2c7be7c1c0aa8f99633b54e90c570999acda93260fc/cryptography-48.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:db63bf618e5dea46c07de12e900fe1cdd2541e6dc9dbae772a70b7d4d4765f6a", size = 3739536, upload-time = "2026-05-04T22:59:29.61Z" },
{ url = "https://files.pythonhosted.org/packages/c5/5c/59086b4aac5e879d38ddbcf74e4be7ade89cebc3eb199a55da998c3bb46a/cryptography-50.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:031e2d5dd4bb9caa3ca9c82e5a197fd8ae680232cee62603d1a813f3f07e3d03", size = 4001252, upload-time = "2026-07-31T14:23:33.331Z" },
{ url = "https://files.pythonhosted.org/packages/57/ef/8f2df13c7216bcad3e1c74e07f6e193d93e998e114f524a53877c9af27ad/cryptography-50.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd9192b7b70c573d7f214eb1ae35e00d359f6f5e4b27c7e21e30de1fc6204645", size = 4719554, upload-time = "2026-07-31T14:23:35.611Z" },
{ url = "https://files.pythonhosted.org/packages/d9/41/029086c34d91052fc3b88bcc8056f709a7c915c7a23b235a54eb800b1c97/cryptography-50.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:06a32a980526a6ab9a4b9bf8f7385800791e2bb960903cb6b530e4817509a3b7", size = 4702130, upload-time = "2026-07-31T14:23:37.635Z" },
{ url = "https://files.pythonhosted.org/packages/7d/ff/b6ce0954962e7f7b969f850a883744197bb3910bdfd7b6da162eab7d9f68/cryptography-50.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a1b30560f2acc95aa8b2e06e716a13dbfc97314747b80d9707e307f77b40d6b3", size = 4725244, upload-time = "2026-07-31T14:23:39.471Z" },
{ url = "https://files.pythonhosted.org/packages/06/1e/63a1027cb7fec360a182208e1b7767d5aa1fe57be3d6aa856e69a321edc0/cryptography-50.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:8d89f3976b10b4ce31118de72329025f70d2c6ead14a8217c5514dd2c6d5a78f", size = 5342265, upload-time = "2026-07-31T14:23:41.286Z" },
{ url = "https://files.pythonhosted.org/packages/6b/72/a1116d683a6d7ece94590013882515de087edf9ef0e6292aae615a44df73/cryptography-50.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b42a28c1844fd9de8f3f7d540e36b66f3a9c83fceac7170ebc7a6a19edd9dcae", size = 4734609, upload-time = "2026-07-31T14:23:43.139Z" },
{ url = "https://files.pythonhosted.org/packages/15/37/36a9c479bbe49acea2636c7fd3360d20f7b7e079c300352011c44850b181/cryptography-50.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:900131fafd8aead39ac7dd3a7e833be754c17a95cfd91221636949fe4eb0aa8a", size = 4356517, upload-time = "2026-07-31T14:23:44.939Z" },
{ url = "https://files.pythonhosted.org/packages/32/98/8a151d64367204cbc63ec65d37502f1d9c53cf4bfc6ec3c532614dbec60d/cryptography-50.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:07949c449a1abcf60d1ee6e88956d89404c7df3c8258f46589e912988e551987", size = 4724529, upload-time = "2026-07-31T14:23:46.93Z" },
{ url = "https://files.pythonhosted.org/packages/22/f6/ec13b470172126464a86bf54d2294a46d29837fc51ba3e45d4047946fb5e/cryptography-50.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:f89831ef99dd7dd169ab06d63a831adb9e20a87aac6d380266bbda5823349169", size = 5299852, upload-time = "2026-07-31T14:23:48.851Z" },
{ url = "https://files.pythonhosted.org/packages/da/3a/f05e32c99d440c9bb891ea0e36c9091891e36be5a9a87ab2ee6ea20729f6/cryptography-50.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:82148ec5bddac30b51a5b3c1945075f896fa022cb93f8e4a01e9f6ee95292c5f", size = 4734462, upload-time = "2026-07-31T14:23:50.861Z" },
{ url = "https://files.pythonhosted.org/packages/ca/dc/bd72b26be8953f80625f63151efd38eee71c76ca6cf591c08ff34615a79e/cryptography-50.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1489e263a8048bb8b6a8bac662eb2d402ea5d2b7b4699b72f385f1e2772db105", size = 4852708, upload-time = "2026-07-31T14:23:52.715Z" },
{ url = "https://files.pythonhosted.org/packages/27/20/c930314a2ab476d15dec966ec87e2e9637bb02b06106b12c0396c57bb603/cryptography-50.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7cec5b856506da6defb290f30c9ee687d5f5e8cb0bd3f6459dde43b0b4fa40ef", size = 5004179, upload-time = "2026-07-31T14:23:54.887Z" },
{ url = "https://files.pythonhosted.org/packages/32/2e/c9db68a0c4bfa28e310707527c0ee3a2bd254104d2e02e68f368e197aa4c/cryptography-50.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:bd1c592e4d5974f0d08d4888e432157adba757c66da0246918e43677fafa2d30", size = 3840395, upload-time = "2026-07-31T14:23:56.677Z" },
{ url = "https://files.pythonhosted.org/packages/c3/fb/951032a3bf22a5697c83183fb6294a4843772947a70e616c57b3ff5f522e/cryptography-50.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:49e7d93abdbd2990caced757e5fade25302f719c3c8fb6e6fff2dde98999fc41", size = 3989258, upload-time = "2026-07-31T14:23:58.881Z" },
{ url = "https://files.pythonhosted.org/packages/d4/67/91eb047e69c5e845f2f14b8a2e4a1aab0f283cb885531e9e22c8adb176bc/cryptography-50.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:19736989797678c6af1e55cd49055cdbcb55d8f6b5583ac5335f933aba9101dc", size = 4700648, upload-time = "2026-07-31T14:24:00.702Z" },
{ url = "https://files.pythonhosted.org/packages/30/82/85f0f7425c856b9f96459411eb12e74ef72df9caf6f8f15bf23a33ff131f/cryptography-50.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80b63928fa35083b33966ce1efb70e5b9607181e49dcd1c22c8c005e319f667f", size = 4682442, upload-time = "2026-07-31T14:24:02.538Z" },
{ url = "https://files.pythonhosted.org/packages/1a/28/b555a365adff1cca2fbe7b9e487d68a40de6bc67ff2cb587473eb43de0e7/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:d58c3db7cd6eed54e6c06744db55456b65ebd7492ddeae9c1e93cfca7aa857d3", size = 4707596, upload-time = "2026-07-31T14:24:04.394Z" },
{ url = "https://files.pythonhosted.org/packages/72/d8/f52538140cc719df62a01cf87d1c7142318d235817109d6f4054d7c352d6/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:df2a58a472f332225671c35b0a830208b86d004f82baa8530fa3782c85646533", size = 5314552, upload-time = "2026-07-31T14:24:06.31Z" },
{ url = "https://files.pythonhosted.org/packages/38/14/6120e5bd7c5aa022ad15424ba4d5c5269d0d9448ed4d55e492ea91e3c1c4/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:11b74db56cdbe3cdee6e3f6982ecb70334fa10dce99ed58bf7894aaaa3b2a037", size = 4717113, upload-time = "2026-07-31T14:24:08.349Z" },
{ url = "https://files.pythonhosted.org/packages/fa/71/190bf38c3ee2e0f8efc9860ae100c9df4169742eef274b91e7aa1cb133b9/cryptography-50.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f59e38625469987d7ef6d495323c55e7db6c212eaf6112267e0d3b565a2e9c9f", size = 4338580, upload-time = "2026-07-31T14:24:10.227Z" },
{ url = "https://files.pythonhosted.org/packages/3a/63/504ccfbbe61fd8aa983f7f146399cdf034c72c2fc55f5b2dfdcdcdb20c99/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ecfed7367f965a0328cfbdd70da860f15441f002f613185668c6e6ebf5a0ac11", size = 4707038, upload-time = "2026-07-31T14:24:12.169Z" },
{ url = "https://files.pythonhosted.org/packages/01/77/2cf79bbfc4d12ca106437a6e170d6aaa01a373e93093118aaaef0e801bd4/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:9aa87839c383bdbab6ef865787a1fb877af8dd03464c4400322726feaaadfc6d", size = 5273110, upload-time = "2026-07-31T14:24:14.38Z" },
{ url = "https://files.pythonhosted.org/packages/e5/45/8aae2972c520145377ea3559a605a899bebe227bf070b33cdb445929a9b9/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:6ba6a53445bd3cfa809ef3ef5f1589aa6ba08784a1d962bf47d0940e871dab1c", size = 4716439, upload-time = "2026-07-31T14:24:16.415Z" },
{ url = "https://files.pythonhosted.org/packages/7b/20/4fe50b619a48c2525cc46e2dbc1ac490708d704be5d467bdaac6dc955682/cryptography-50.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3f5735ffe4996d28b809371756219f5354864902a3b9e7c0b9ee87041209fc9c", size = 4837383, upload-time = "2026-07-31T14:24:18.553Z" },
{ url = "https://files.pythonhosted.org/packages/92/91/3a31366e183343d3703f8995c095f5734676bd6938118047e50fcf279eb4/cryptography-50.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1b4a266766514614f8aa60416e71f2fc6e575d36e7bdc90f644fadb2f4b75b95", size = 4985772, upload-time = "2026-07-31T14:24:20.385Z" },
{ url = "https://files.pythonhosted.org/packages/74/9a/02ffe35b2853d121689871eb5dce862092562b3a1ed5cc98f1aaed441506/cryptography-50.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:12b9c6996425c76ea6c457ace4f3073e715b8c545add07cd1a8f3a4f90691269", size = 3816291, upload-time = "2026-07-31T14:24:22.125Z" },
{ url = "https://files.pythonhosted.org/packages/03/37/73d005be173aff344af30e9fd2a576575cb2391a7101d9cd3842e1fa8cce/cryptography-50.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ccdc4a71a4dabae05de219404f9f4abc38e3b58422177ff93d0da05967dafa07", size = 4036009, upload-time = "2026-07-31T14:24:24.122Z" },
{ url = "https://files.pythonhosted.org/packages/ff/c6/7a6202a534e32103a285b7834a120869557fe198d51d7cfe59754c8bda9c/cryptography-50.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:910e1d2668e7de9648f2bcee30e180db2a6b15c30f887d7c4c93ddf96e3992e3", size = 4745252, upload-time = "2026-07-31T14:24:26.118Z" },
{ url = "https://files.pythonhosted.org/packages/85/4f/0fa8c2f4428198f15d9ff8d63400e27afbf94ce833f6108da1eb3753f945/cryptography-50.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a91296cb61e8df6f86d0c19cc4068228da256bf59bf86049fbd821084565327f", size = 4728939, upload-time = "2026-07-31T14:24:27.994Z" },
{ url = "https://files.pythonhosted.org/packages/d1/63/54dd723490ba2dc09b299682c10b38db38f159728bcaae8c591b8af2f22d/cryptography-50.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e722f16708d854fe924790e051061f6704a472c3bac347b6fd88033ea8dd0dc5", size = 4748483, upload-time = "2026-07-31T14:24:30.254Z" },
{ url = "https://files.pythonhosted.org/packages/1d/dd/7c77d26285cc7f6991efce64a0f5b4f9383bfa5dd8c5033003eaf7db4cdb/cryptography-50.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d764dcf130c428ef66786f866dd750f53182bc608813489915e9fc106bb0c82f", size = 5367599, upload-time = "2026-07-31T14:24:32.457Z" },
{ url = "https://files.pythonhosted.org/packages/46/c9/f60aed34c013f317f92817b6c171c2d22a78270fa41109bd4b08af26b194/cryptography-50.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:105110f43a471dbd0060b9c9516cb8a6a79233631a04cc2ba16f28323ac6e025", size = 4762647, upload-time = "2026-07-31T14:24:34.599Z" },
{ url = "https://files.pythonhosted.org/packages/be/f3/f9a0173b139372c3a48ed98154b45cc6b9de17c789d5ab552e621c293609/cryptography-50.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:828743d939e9629bc267b8e2d08d8bb67cd4319c771a33d4b18b22dd8fb7440a", size = 4385197, upload-time = "2026-07-31T14:24:36.647Z" },
{ url = "https://files.pythonhosted.org/packages/d8/36/83bb81f6e569bc38e1e4a7bc80f29b46bb9601920bc455fc8e888f5d5742/cryptography-50.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:2a8183b489dc1f7f80f135780fadc1108f14b31b8a40411c7a5b17425f65f28b", size = 4748095, upload-time = "2026-07-31T14:24:39.493Z" },
{ url = "https://files.pythonhosted.org/packages/6b/16/d3008eff98c764979865834c3d386d4fd041b5f52e7f34fc29ac1a5eb515/cryptography-50.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6e7d61120573a7f2cd94cc095f9e81f6967c61ccdf194285aa143ecec8e0b708", size = 5325948, upload-time = "2026-07-31T14:24:41.556Z" },
{ url = "https://files.pythonhosted.org/packages/9c/f8/d97f9603efda3888187bfdb893f26c41be4735c10631d05d284ee6b047c4/cryptography-50.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:37fdb0d0111f1e2ff07139dfb79f1b49531f8e213c46f1163dd7642979b58c47", size = 4762400, upload-time = "2026-07-31T14:24:43.636Z" },
{ url = "https://files.pythonhosted.org/packages/64/a2/4615c8f7d81a00b1d6e6afe19f694e1543582349fb5f4076f6cb5dc36485/cryptography-50.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87f62a3d3b9888ed0fdde100ec06aa61ca9cd44bad9057d1dff9a516b5f5bb9", size = 4878208, upload-time = "2026-07-31T14:24:45.522Z" },
{ url = "https://files.pythonhosted.org/packages/d2/1a/efcfb02f91407149a0dacffffab791f7e19bf6385f63b3666dc8b5e5c9c8/cryptography-50.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:65c2c3add92b45fd0709db8594536aea39c2a67af0e27ffcf049c498501140b7", size = 5037050, upload-time = "2026-07-31T14:24:47.697Z" },
{ url = "https://files.pythonhosted.org/packages/57/30/4a22984d4f1bdfb8c054f07a92bc176b97a3134cc1d6c4b3bffb1f3688b4/cryptography-50.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:d24fead1d4d076e1bfb006dcec392074a3cd8d7b4fc8a595aa64073b2b7a96ba", size = 3874135, upload-time = "2026-07-31T14:24:50.085Z" },
{ url = "https://files.pythonhosted.org/packages/9d/3e/e54cde8c01631a5a8226ccd617eab9e57fd5cfdad90f1a9e6bb570794631/cryptography-50.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:5e34edd123674534acd70147f0ca331eaa2c74e6325fb2028c886aa26ba0b68c", size = 3963170, upload-time = "2026-07-31T14:24:51.968Z" },
{ url = "https://files.pythonhosted.org/packages/01/b6/0b9e125e90f3d2dcf599a218a899cda7326a3158cfa258723f0b398b08f6/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:8eb5e1172eb569ea8a872796576e6a67c276351728b6455d5beb01242b027c6a", size = 4692441, upload-time = "2026-07-31T14:24:53.743Z" },
{ url = "https://files.pythonhosted.org/packages/53/c9/a5151588710785a96d7bc4de27d4cd62f263bbbcb203cfe29df537eb6505/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:910d11e1a385c654bf738bf3e6b8e6ed5de0f5610fcae2be9e5b398d8081d20e", size = 4699810, upload-time = "2026-07-31T14:24:55.746Z" },
{ url = "https://files.pythonhosted.org/packages/c7/1a/15b92b25eb6ce3089cd49377ae990a0f3ad485a510f968aed1f19dbdcdf2/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:62598a8a57f815db4c6259a4e97d857dab56697e7de8e8ab02352ab74da1995d", size = 4691924, upload-time = "2026-07-31T14:24:58.082Z" },
{ url = "https://files.pythonhosted.org/packages/62/15/219075012ab13e8905f3cd572204f4acb4b111df787104346b9bc0cea789/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:07479a1cb08219ab719147e742e76090c9c773321959bb94946fffdd397a6437", size = 4699593, upload-time = "2026-07-31T14:24:59.951Z" },
{ url = "https://files.pythonhosted.org/packages/8e/b5/c2c5fce26f0ee40d21bafe7f191d29a34b35a65ac4fe8a1191d1983612e9/cryptography-50.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c99c003e088647b8a5b7c145d6f78c335f6348332b62e142d411c4b63d1460b9", size = 3813796, upload-time = "2026-07-31T14:25:02.298Z" },
]
[[package]]
@@ -1193,6 +1190,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" },
]
[[package]]
name = "defusedxml"
version = "0.7.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" },
]
[[package]]
name = "demucs"
version = "4.0.1"
@@ -2962,17 +2968,18 @@ wheels = [
[[package]]
name = "nltk"
version = "3.9.4"
version = "3.10.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "defusedxml" },
{ name = "joblib" },
{ name = "regex" },
{ name = "tqdm" },
]
sdist = { url = "https://files.pythonhosted.org/packages/74/a1/b3b4adf15585a5bc4c357adde150c01ebeeb642173ded4d871e89468767c/nltk-3.9.4.tar.gz", hash = "sha256:ed03bc098a40481310320808b2db712d95d13ca65b27372f8a403949c8b523d0", size = 2946864, upload-time = "2026-03-24T06:13:40.641Z" }
sdist = { url = "https://files.pythonhosted.org/packages/96/02/df4f105b28a7c16b0e41423bc09cf0f1b8a305df4ef0b10ca74a2e4c648c/nltk-3.10.0.tar.gz", hash = "sha256:4fbac1d98203cbcd1b5d94a2877fb822300072d80604a5e7fae49d2c5f84e8c1", size = 3089244, upload-time = "2026-07-08T02:39:13.562Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9d/91/04e965f8e717ba0ab4bdca5c112deeab11c9e750d94c4d4602f050295d39/nltk-3.9.4-py3-none-any.whl", hash = "sha256:f2fa301c3a12718ce4a0e9305c5675299da5ad9e26068218b69d692fda84828f", size = 1552087, upload-time = "2026-03-24T06:13:38.47Z" },
{ url = "https://files.pythonhosted.org/packages/6e/89/a0b0f35e2820d6a99d75ea1c11977ee6d5c9e6658eceb45b0c7620881faa/nltk-3.10.0-py3-none-any.whl", hash = "sha256:54ff84d4916d3ef127e8953bee0023f6a6b320b75d634a19e06ef056d3d244bf", size = 1716144, upload-time = "2026-07-08T02:39:09.753Z" },
]
[[package]]
@@ -4440,11 +4447,11 @@ wheels = [
[[package]]
name = "pypdf"
version = "6.13.2"
version = "6.15.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/99/0a/48fe05c6bb3aa4bb4d2a4079a383d33c0dfec1edf613a642f07d8b8b5c2e/pypdf-6.13.2.tar.gz", hash = "sha256:5a96a17dbdfbf9c2ab24c0a13fa0aba182be22ba6f283098712c16fc242f509f", size = 6479250, upload-time = "2026-06-10T16:42:34.5Z" }
sdist = { url = "https://files.pythonhosted.org/packages/17/17/ee75a92718ec7212de831e71454d702225aa5e474a805cce169806044453/pypdf-6.15.0.tar.gz", hash = "sha256:d39c4d955a76409284a905e2d65b40076d77ab76129e0faaeeb6612403ecfc79", size = 6993794, upload-time = "2026-08-06T13:06:49.929Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/17/378943705992f74e451a06de3401ce68e3213763c81e44d0614559c45599/pypdf-6.13.2-py3-none-any.whl", hash = "sha256:6eeb9e57693f29d41bd01255d02660cbbb41fd7fc818a982677389a35e4f2083", size = 346555, upload-time = "2026-06-10T16:42:32.37Z" },
{ url = "https://files.pythonhosted.org/packages/af/72/ce3067ac31e214a66388159f8462ddb8c13dd00170f24d555a1f1ae8ee91/pypdf-6.15.0-py3-none-any.whl", hash = "sha256:14e001d6504822cb1ca9c7ed9a69bccb320f59b320730f55af804361abe4d5ee", size = 378123, upload-time = "2026-08-06T13:06:47.709Z" },
]
[[package]]
@@ -6805,11 +6812,11 @@ wheels = [
[[package]]
name = "yt-dlp"
version = "2026.6.9"
version = "2026.7.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/88/a4/1b0979d28f87774bb67fbbc66bce44f9dd1aa0e547a99e22985fac945c33/yt_dlp-2026.6.9.tar.gz", hash = "sha256:d50fcb95f48d61bedde33e408c1881d4c279e51c31354a599ce09e96ba0f4b86", size = 3030590, upload-time = "2026-06-09T23:27:14.831Z" }
sdist = { url = "https://files.pythonhosted.org/packages/47/c5/9972af4b472b0d55badf841ebafd2f98944cb0ae0f46e11d01f363ea5b91/yt_dlp-2026.7.4.tar.gz", hash = "sha256:b094813404f87a9dd2186f00815231df32e5fd8a5403be0f807b3bb2d21a4432", size = 3049326, upload-time = "2026-07-04T22:42:14.837Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f3/ee/188a3dadf9dfdac713243521f919feca1cd091d4358c9ea7e8ebb710a7cc/yt_dlp-2026.6.9-py3-none-any.whl", hash = "sha256:442ba4c75724b9496144c8434b617962ee08d0ee7c26ec663848fe9b78d5a3e4", size = 3169035, upload-time = "2026-06-09T23:27:12.58Z" },
{ url = "https://files.pythonhosted.org/packages/f9/8a/cd4c9b02c10c563adfe78118310129641900e1cd6de888cfae2452072696/yt_dlp-2026.7.4-py3-none-any.whl", hash = "sha256:f11f2b11d5a8ac4059f9bdf29fa4407dc7c6bb00c5097e95ca22a7a9db518266", size = 3184705, upload-time = "2026-07-04T22:42:12.989Z" },
]
[[package]]