a-01a commited on
Commit
a358eab
·
verified ·
1 Parent(s): ac542f0

Deploy FaceCloak

Browse files
Files changed (46) hide show
  1. .gitattributes +3 -0
  2. README.md +98 -12
  3. app.py +5 -0
  4. facecloak/__init__.py +3 -0
  5. facecloak/__pycache__/__init__.cpython-312.pyc +0 -0
  6. facecloak/__pycache__/cloaking.cpython-312.pyc +0 -0
  7. facecloak/__pycache__/deploy.cpython-312.pyc +0 -0
  8. facecloak/__pycache__/environment.cpython-312.pyc +0 -0
  9. facecloak/__pycache__/errors.cpython-312.pyc +0 -0
  10. facecloak/__pycache__/interface.cpython-312.pyc +0 -0
  11. facecloak/__pycache__/models.cpython-312.pyc +0 -0
  12. facecloak/__pycache__/pipeline.cpython-312.pyc +0 -0
  13. facecloak/__pycache__/project.cpython-312.pyc +0 -0
  14. facecloak/cloaking.py +123 -0
  15. facecloak/deploy.py +121 -0
  16. facecloak/environment.py +97 -0
  17. facecloak/errors.py +7 -0
  18. facecloak/interface.py +229 -0
  19. facecloak/models.py +38 -0
  20. facecloak/pipeline.py +115 -0
  21. facecloak/project.py +47 -0
  22. main.py +5 -0
  23. pyproject.toml +28 -0
  24. requirements.txt +7 -0
  25. scripts/create_or_update_space.py +20 -0
  26. tests/__pycache__/conftest.cpython-312-pytest-9.0.3.pyc +0 -0
  27. tests/__pycache__/test_cloaking.cpython-312-pytest-9.0.3.pyc +0 -0
  28. tests/__pycache__/test_deploy.cpython-312-pytest-9.0.3.pyc +0 -0
  29. tests/__pycache__/test_environment.cpython-312-pytest-9.0.3.pyc +0 -0
  30. tests/__pycache__/test_integration.cpython-312-pytest-9.0.3.pyc +0 -0
  31. tests/__pycache__/test_interface.cpython-312-pytest-9.0.3.pyc +0 -0
  32. tests/__pycache__/test_pipeline.cpython-312-pytest-9.0.3.pyc +0 -0
  33. tests/__pycache__/test_project.cpython-312-pytest-9.0.3.pyc +0 -0
  34. tests/conftest.py +7 -0
  35. tests/fixtures/README.md +9 -0
  36. tests/fixtures/faces/george_a.jpg +3 -0
  37. tests/fixtures/faces/obama_a.jpg +3 -0
  38. tests/fixtures/faces/obama_b.jpg +3 -0
  39. tests/test_cloaking.py +48 -0
  40. tests/test_deploy.py +84 -0
  41. tests/test_environment.py +24 -0
  42. tests/test_integration.py +40 -0
  43. tests/test_interface.py +40 -0
  44. tests/test_pipeline.py +83 -0
  45. tests/test_project.py +17 -0
  46. uv.lock +1032 -0
.gitattributes CHANGED
@@ -33,3 +33,6 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ tests/fixtures/faces/george_a.jpg filter=lfs diff=lfs merge=lfs -text
37
+ tests/fixtures/faces/obama_a.jpg filter=lfs diff=lfs merge=lfs -text
38
+ tests/fixtures/faces/obama_b.jpg filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -1,12 +1,98 @@
1
- ---
2
- title: Facecloak
3
- emoji: 📉
4
- colorFrom: pink
5
- colorTo: pink
6
- sdk: gradio
7
- sdk_version: 6.12.0
8
- app_file: app.py
9
- pinned: false
10
- ---
11
-
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # FaceCloak
2
+
3
+ FaceCloak is a fully local biometric privacy project that uses white-box adversarial optimization to push a face image away from its original embedding while keeping the image visually unchanged for a human viewer.
4
+
5
+ Phases 2 and 3 are now complete in this repository. The app can detect and align a face with MTCNN, extract a 512-dimensional embedding with `InceptionResnetV1`, compare embeddings with cosine similarity, and run a white-box PGD attack that pushes the cloaked face away from its original identity in embedding space.
6
+
7
+ ## Phase 2 and 3 Deliverables
8
+
9
+ - Cached MTCNN face detection and alignment on CPU
10
+ - Frozen `InceptionResnetV1(pretrained="vggface2")` embedding extraction
11
+ - Cosine similarity scoring for same-person vs different-person checks
12
+ - L-infinity PGD cloaking over the aligned face tensor
13
+ - Reverse conversion from the standardized tensor back to a downloadable PIL image
14
+ - A Gradio app with comparison and cloaking workflows
15
+ - A programmatic Hugging Face Space deployment helper that reads `FACECLOAK_HF_TOKEN` from `.env`
16
+ - Expanded test coverage, including real-image integration checks
17
+
18
+ ## Quickstart
19
+
20
+ These commands keep Python and the `uv` cache inside the repository so the setup stays self-contained.
21
+
22
+ ```powershell
23
+ $env:UV_CACHE_DIR = "$PWD/.uv-cache"
24
+ $env:UV_PYTHON_INSTALL_DIR = "$PWD/.uv-python"
25
+ uv python install 3.12
26
+ uv sync
27
+ uv run python main.py
28
+ ```
29
+
30
+ The first model-backed run will download the pretrained FaceNet weights into `.torch-cache/`.
31
+
32
+ ## App Workflow
33
+
34
+ The Gradio app now exposes two tabs:
35
+
36
+ - `Cloak Face`: upload one portrait, align the primary face, run PGD, and inspect the cloaked face plus perturbation preview
37
+ - `Compare Faces`: upload two portraits and measure cosine similarity between their face embeddings
38
+
39
+ The cloaking engine works on the aligned face crop returned by MTCNN. The tensors entering FaceNet are in the facenet-pytorch standardized range produced by `fixed_image_standardization`, which is why the default perturbation budget is specified on the `[-1, 1]`-style scale instead of in raw `0-255` pixels.
40
+
41
+ ## PGD Objective
42
+
43
+ The PGD loop uses an ascent step of the form `delta += alpha * sign(grad)`. Because the step is ascent, the objective is defined as:
44
+
45
+ ```text
46
+ objective = -cosine_similarity(original_embedding, cloaked_embedding) - lambda * ||delta||²
47
+ ```
48
+
49
+ Maximizing that objective lowers cosine similarity while gently discouraging unnecessarily large perturbations. This is mathematically equivalent to minimizing cosine similarity with a descent update.
50
+
51
+ ## Run Tests
52
+
53
+ ```powershell
54
+ $env:UV_CACHE_DIR = "$PWD/.uv-cache"
55
+ $env:UV_PYTHON_INSTALL_DIR = "$PWD/.uv-python"
56
+ uv run pytest
57
+ ```
58
+
59
+ The suite includes real-image integration tests using public-domain portrait photos in `tests/fixtures/faces/`.
60
+
61
+ ## Hugging Face Space Notes
62
+
63
+ Hugging Face Spaces still require a root-level `app.py` and `requirements.txt`. This repository now includes both:
64
+
65
+ - `app.py` exposes the Gradio demo for Spaces
66
+ - `requirements.txt` contains only the direct runtime dependencies, pinned to the versions validated locally
67
+ - `uv.lock` remains the source of truth for fully reproducible local development
68
+
69
+ You can create or update the Space programmatically with the token stored in `.env`:
70
+
71
+ ```powershell
72
+ $env:UV_CACHE_DIR = "$PWD/.uv-cache"
73
+ $env:UV_PYTHON_INSTALL_DIR = "$PWD/.uv-python"
74
+ uv run python scripts/create_or_update_space.py
75
+ ```
76
+
77
+ The script will:
78
+
79
+ - read `FACECLOAK_HF_TOKEN` from the environment or `.env`
80
+ - determine the Hugging Face username with `whoami`
81
+ - create `username/facecloak` as a Gradio Space on `cpu-basic` hardware if it does not already exist
82
+ - upload the repository contents needed to build the Space
83
+
84
+ ## Benchmarks Verified Locally
85
+
86
+ Using the public-domain portrait fixtures bundled for integration tests:
87
+
88
+ - same-person pairs scored above `0.8`
89
+ - different-person pairs scored below `0.3`
90
+ - the default cloaking hyperparameters (`epsilon=0.03`, `num_steps=30`) drove original-vs-cloaked similarity sharply downward on CPU
91
+
92
+ ## Verified in Phase 1
93
+
94
+ - `torch` imports successfully
95
+ - `torch.cuda.is_available()` is `False`, which matches the intended CPU deployment target
96
+ - Basic tensor math works
97
+ - The Gradio app builds successfully
98
+ - The repository has automated tests for the environment scaffold
app.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ from facecloak.interface import APP_CSS, APP_THEME, demo
2
+
3
+
4
+ if __name__ == "__main__":
5
+ demo.launch(theme=APP_THEME, css=APP_CSS)
facecloak/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ """FaceCloak package."""
2
+
3
+ __version__ = "0.1.0"
facecloak/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (225 Bytes). View file
 
facecloak/__pycache__/cloaking.cpython-312.pyc ADDED
Binary file (6.28 kB). View file
 
facecloak/__pycache__/deploy.cpython-312.pyc ADDED
Binary file (4.95 kB). View file
 
facecloak/__pycache__/environment.cpython-312.pyc ADDED
Binary file (4.91 kB). View file
 
facecloak/__pycache__/errors.cpython-312.pyc ADDED
Binary file (542 Bytes). View file
 
facecloak/__pycache__/interface.cpython-312.pyc ADDED
Binary file (10.3 kB). View file
 
facecloak/__pycache__/models.cpython-312.pyc ADDED
Binary file (1.75 kB). View file
 
facecloak/__pycache__/pipeline.cpython-312.pyc ADDED
Binary file (6.71 kB). View file
 
facecloak/__pycache__/project.cpython-312.pyc ADDED
Binary file (1.74 kB). View file
 
facecloak/cloaking.py ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Projected Gradient Descent cloaking for aligned face tensors."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+ from PIL import Image
8
+ import torch
9
+ import torch.nn.functional as F
10
+
11
+ from facecloak.models import get_embedding_model
12
+ from facecloak.pipeline import (
13
+ DISPLAY_MAX,
14
+ DISPLAY_MIN,
15
+ _prepare_face_batch,
16
+ perturbation_preview_image,
17
+ standardized_tensor_to_pil,
18
+ )
19
+
20
+
21
+ @dataclass(frozen=True, slots=True)
22
+ class CloakHyperparameters:
23
+ epsilon: float = 0.03
24
+ alpha: float | None = None
25
+ num_steps: int = 30
26
+ l2_lambda: float = 0.01
27
+
28
+
29
+ @dataclass(slots=True)
30
+ class CloakResult:
31
+ original_face_image: Image.Image
32
+ cloaked_face_image: Image.Image
33
+ perturbation_preview: Image.Image
34
+ cloaked_face_tensor: torch.Tensor
35
+ delta_tensor: torch.Tensor
36
+ original_similarity: float
37
+ final_similarity: float
38
+ similarity_drop: float
39
+ loss_history: list[float]
40
+ similarity_history: list[float]
41
+ delta_l_inf: float
42
+ delta_rms: float
43
+ parameters: CloakHyperparameters
44
+
45
+
46
+ def cloak_face_tensor(
47
+ face_tensor: torch.Tensor,
48
+ *,
49
+ model: torch.nn.Module | None = None,
50
+ parameters: CloakHyperparameters | None = None,
51
+ ) -> CloakResult:
52
+ parameters = parameters or CloakHyperparameters()
53
+ if parameters.epsilon <= 0.0:
54
+ raise ValueError("epsilon must be positive.")
55
+ if parameters.num_steps <= 0:
56
+ raise ValueError("num_steps must be positive.")
57
+ if parameters.l2_lambda < 0.0:
58
+ raise ValueError("l2_lambda cannot be negative.")
59
+
60
+ alpha = parameters.alpha if parameters.alpha is not None else parameters.epsilon / 10.0
61
+ if alpha <= 0.0:
62
+ raise ValueError("alpha must be positive.")
63
+
64
+ model = model or get_embedding_model()
65
+ original_batch = _prepare_face_batch(face_tensor).to(
66
+ next(model.parameters()).device
67
+ ).detach()
68
+ original_embedding = model(original_batch).detach()
69
+ delta = torch.zeros_like(original_batch, requires_grad=True)
70
+
71
+ loss_history: list[float] = []
72
+ similarity_history: list[float] = []
73
+
74
+ for _ in range(parameters.num_steps):
75
+ if delta.grad is not None:
76
+ delta.grad.zero_()
77
+
78
+ cloaked_batch = torch.clamp(original_batch + delta, DISPLAY_MIN, DISPLAY_MAX)
79
+ cloaked_embedding = model(cloaked_batch)
80
+ cosine = F.cosine_similarity(original_embedding, cloaked_embedding, dim=1).mean()
81
+
82
+ # We use a PGD ascent step, so maximizing -cosine lowers the embedding similarity.
83
+ objective = -cosine - parameters.l2_lambda * delta.pow(2).mean()
84
+ objective.backward()
85
+
86
+ with torch.no_grad():
87
+ delta.add_(alpha * delta.grad.sign())
88
+ delta.clamp_(-parameters.epsilon, parameters.epsilon)
89
+ delta.copy_(
90
+ torch.clamp(original_batch + delta, DISPLAY_MIN, DISPLAY_MAX) - original_batch
91
+ )
92
+
93
+ loss_history.append(float(objective.detach().item()))
94
+ similarity_history.append(float(cosine.detach().item()))
95
+
96
+ final_batch = torch.clamp(original_batch + delta, DISPLAY_MIN, DISPLAY_MAX).detach()
97
+ final_embedding = model(final_batch).detach()
98
+ final_similarity = float(
99
+ F.cosine_similarity(original_embedding, final_embedding, dim=1).mean().item()
100
+ )
101
+ delta_cpu = delta.detach().cpu()
102
+ final_cpu = final_batch.detach().cpu()
103
+
104
+ return CloakResult(
105
+ original_face_image=standardized_tensor_to_pil(original_batch.cpu()),
106
+ cloaked_face_image=standardized_tensor_to_pil(final_cpu),
107
+ perturbation_preview=perturbation_preview_image(delta_cpu),
108
+ cloaked_face_tensor=final_cpu[0],
109
+ delta_tensor=delta_cpu[0],
110
+ original_similarity=1.0,
111
+ final_similarity=final_similarity,
112
+ similarity_drop=1.0 - final_similarity,
113
+ loss_history=loss_history,
114
+ similarity_history=similarity_history,
115
+ delta_l_inf=float(delta_cpu.abs().max().item()),
116
+ delta_rms=float(delta_cpu.pow(2).mean().sqrt().item()),
117
+ parameters=CloakHyperparameters(
118
+ epsilon=parameters.epsilon,
119
+ alpha=alpha,
120
+ num_steps=parameters.num_steps,
121
+ l2_lambda=parameters.l2_lambda,
122
+ ),
123
+ )
facecloak/deploy.py ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Programmatic Hugging Face Space creation and upload."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ import os
7
+ from pathlib import Path
8
+
9
+ from huggingface_hub import HfApi, SpaceHardware
10
+
11
+ from facecloak.errors import FaceCloakError
12
+ from facecloak.project import (
13
+ PROJECT_ROOT,
14
+ PROJECT_SLUG,
15
+ SPACE_UPLOAD_ALLOW_PATTERNS,
16
+ SPACE_URL_TEMPLATE,
17
+ )
18
+
19
+ HF_TOKEN_ENV_VAR = "FACECLOAK_HF_TOKEN"
20
+
21
+
22
+ @dataclass(frozen=True, slots=True)
23
+ class SpaceDeploymentResult:
24
+ repo_id: str
25
+ space_url: str
26
+ runtime_stage: str | None
27
+ hardware: str | None
28
+ commit_oid: str | None
29
+
30
+
31
+ def read_env_file(env_path: Path | None = None) -> dict[str, str]:
32
+ env_path = env_path or PROJECT_ROOT / ".env"
33
+ if not env_path.exists():
34
+ return {}
35
+
36
+ values: dict[str, str] = {}
37
+ for raw_line in env_path.read_text(encoding="utf-8").splitlines():
38
+ line = raw_line.strip()
39
+ if not line or line.startswith("#") or "=" not in line:
40
+ continue
41
+
42
+ key, raw_value = line.split("=", 1)
43
+ key = key.strip()
44
+ value = raw_value.strip().strip('"').strip("'")
45
+ values[key] = value
46
+
47
+ return values
48
+
49
+
50
+ def resolve_hf_token(env_path: Path | None = None) -> str:
51
+ token = os.environ.get(HF_TOKEN_ENV_VAR)
52
+ if token:
53
+ return token
54
+
55
+ token = read_env_file(env_path).get(HF_TOKEN_ENV_VAR)
56
+ if token:
57
+ return token
58
+
59
+ raise FaceCloakError(
60
+ f"{HF_TOKEN_ENV_VAR} was not found. Add it to the environment or to a local .env file."
61
+ )
62
+
63
+
64
+ def default_space_repo_id(api: HfApi, token: str) -> str:
65
+ whoami = api.whoami(token=token, cache=False)
66
+ username = whoami.get("name")
67
+ if not username:
68
+ raise FaceCloakError("Could not determine the Hugging Face username from the provided token.")
69
+ return f"{username}/{PROJECT_SLUG}"
70
+
71
+
72
+ def create_or_update_space(
73
+ *,
74
+ repo_id: str | None = None,
75
+ token: str | None = None,
76
+ api: HfApi | None = None,
77
+ folder_path: Path | None = None,
78
+ ) -> SpaceDeploymentResult:
79
+ api = api or HfApi()
80
+ token = token or resolve_hf_token()
81
+ repo_id = repo_id or default_space_repo_id(api, token)
82
+ folder_path = folder_path or PROJECT_ROOT
83
+
84
+ api.create_repo(
85
+ repo_id=repo_id,
86
+ token=token,
87
+ repo_type="space",
88
+ exist_ok=True,
89
+ space_sdk="gradio",
90
+ space_hardware=SpaceHardware.CPU_BASIC,
91
+ )
92
+ commit_info = api.upload_folder(
93
+ repo_id=repo_id,
94
+ repo_type="space",
95
+ folder_path=folder_path,
96
+ token=token,
97
+ allow_patterns=list(SPACE_UPLOAD_ALLOW_PATTERNS),
98
+ commit_message="Deploy FaceCloak",
99
+ )
100
+ runtime = api.get_space_runtime(repo_id, token=token)
101
+
102
+ return SpaceDeploymentResult(
103
+ repo_id=repo_id,
104
+ space_url=SPACE_URL_TEMPLATE.format(repo_id=repo_id),
105
+ runtime_stage=getattr(runtime, "stage", None),
106
+ hardware=getattr(runtime, "hardware", None),
107
+ commit_oid=getattr(commit_info, "oid", None),
108
+ )
109
+
110
+
111
+ def deployment_markdown(result: SpaceDeploymentResult) -> str:
112
+ return "\n".join(
113
+ [
114
+ "### Hugging Face Space",
115
+ f"- Repository: `{result.repo_id}`",
116
+ f"- URL: {result.space_url}",
117
+ f"- Runtime Stage: `{result.runtime_stage or 'unknown'}`",
118
+ f"- Hardware: `{result.hardware or 'unknown'}`",
119
+ f"- Commit OID: `{result.commit_oid or 'unknown'}`",
120
+ ]
121
+ )
facecloak/environment.py ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Runtime diagnostics for the local and Hugging Face Space environments."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from importlib.metadata import PackageNotFoundError, version
7
+ import platform
8
+
9
+ from facecloak.project import PHASE_LABEL, PHASE_SUMMARY, TORCH_CACHE_DIR
10
+
11
+
12
+ @dataclass(frozen=True, slots=True)
13
+ class RuntimeReport:
14
+ python_version: str
15
+ operating_system: str
16
+ torch_version: str
17
+ torchvision_version: str
18
+ facenet_pytorch_version: str
19
+ gradio_version: str
20
+ huggingface_hub_version: str
21
+ numpy_version: str
22
+ pillow_version: str
23
+ cuda_available: bool
24
+ device: str
25
+ tensor_sanity: tuple[int, ...]
26
+ torch_cache_dir: str
27
+ phase: str
28
+ status: str
29
+ notes: str
30
+
31
+
32
+ def _installed_version(distribution_name: str) -> str:
33
+ try:
34
+ return version(distribution_name)
35
+ except PackageNotFoundError:
36
+ return "not installed"
37
+
38
+
39
+ def collect_runtime_report() -> RuntimeReport:
40
+ import torch
41
+
42
+ tensor_sanity = tuple((torch.arange(3) + 1).tolist())
43
+ cuda_available = bool(torch.cuda.is_available())
44
+ device = "cuda" if cuda_available else "cpu"
45
+ status = "ready" if tensor_sanity == (1, 2, 3) else "check failed"
46
+
47
+ return RuntimeReport(
48
+ python_version=platform.python_version(),
49
+ operating_system=platform.platform(),
50
+ torch_version=torch.__version__,
51
+ torchvision_version=_installed_version("torchvision"),
52
+ facenet_pytorch_version=_installed_version("facenet-pytorch"),
53
+ gradio_version=_installed_version("gradio"),
54
+ huggingface_hub_version=_installed_version("huggingface-hub"),
55
+ numpy_version=_installed_version("numpy"),
56
+ pillow_version=_installed_version("pillow"),
57
+ cuda_available=cuda_available,
58
+ device=device,
59
+ tensor_sanity=tensor_sanity,
60
+ torch_cache_dir=str(TORCH_CACHE_DIR),
61
+ phase=PHASE_LABEL,
62
+ status=status,
63
+ notes="CPU execution remains the intended deployment target for the Hugging Face Space.",
64
+ )
65
+
66
+
67
+ def format_runtime_markdown(report: RuntimeReport) -> str:
68
+ cuda_label = "Yes" if report.cuda_available else "No"
69
+ tensor_label = ", ".join(str(value) for value in report.tensor_sanity)
70
+
71
+ return "\n".join(
72
+ [
73
+ "### Runtime Diagnostics",
74
+ f"- Phase: {report.phase}",
75
+ f"- Status: {report.status}",
76
+ f"- Python: `{report.python_version}`",
77
+ f"- Platform: `{report.operating_system}`",
78
+ f"- Torch: `{report.torch_version}`",
79
+ f"- Torchvision: `{report.torchvision_version}`",
80
+ f"- facenet-pytorch: `{report.facenet_pytorch_version}`",
81
+ f"- NumPy: `{report.numpy_version}`",
82
+ f"- Pillow: `{report.pillow_version}`",
83
+ f"- Gradio: `{report.gradio_version}`",
84
+ f"- huggingface-hub: `{report.huggingface_hub_version}`",
85
+ f"- CUDA Available: `{cuda_label}`",
86
+ f"- Active Device: `{report.device}`",
87
+ f"- Tensor Sanity: `[{tensor_label}]`",
88
+ f"- Torch Cache: `{report.torch_cache_dir}`",
89
+ f"- Notes: {report.notes}",
90
+ "",
91
+ PHASE_SUMMARY,
92
+ ]
93
+ )
94
+
95
+
96
+ def render_runtime_markdown() -> str:
97
+ return format_runtime_markdown(collect_runtime_report())
facecloak/errors.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ """User-facing exceptions for FaceCloak workflows."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ class FaceCloakError(RuntimeError):
7
+ """A readable error that can be surfaced directly in the UI."""
facecloak/interface.py ADDED
@@ -0,0 +1,229 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Gradio interface for the Phase 2 and 3 FaceCloak workflow."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import gradio as gr
6
+
7
+ from facecloak.cloaking import CloakHyperparameters, cloak_face_tensor
8
+ from facecloak.environment import render_runtime_markdown
9
+ from facecloak.errors import FaceCloakError
10
+ from facecloak.pipeline import (
11
+ cosine_similarity,
12
+ detect_primary_face,
13
+ extract_embedding_numpy,
14
+ )
15
+ from facecloak.project import (
16
+ PHASE_LABEL,
17
+ PHASE_STATUS,
18
+ PHASE_SUMMARY,
19
+ PROJECT_NAME,
20
+ PROJECT_TAGLINE,
21
+ )
22
+
23
+ APP_CSS = """
24
+ .gradio-container {
25
+ background:
26
+ radial-gradient(circle at top left, rgba(180, 214, 255, 0.35), transparent 32%),
27
+ radial-gradient(circle at top right, rgba(255, 212, 170, 0.30), transparent 28%),
28
+ linear-gradient(135deg, #f6f2eb 0%, #eef4fb 100%);
29
+ }
30
+ #hero {
31
+ padding-top: 0.75rem;
32
+ }
33
+ .panel {
34
+ border: 1px solid rgba(32, 52, 84, 0.10);
35
+ border-radius: 18px;
36
+ background: rgba(255, 255, 255, 0.82);
37
+ padding: 0.75rem 1rem;
38
+ box-shadow: 0 18px 45px rgba(50, 70, 105, 0.08);
39
+ }
40
+ """
41
+
42
+ APP_THEME = gr.themes.Base(
43
+ primary_hue="amber",
44
+ secondary_hue="blue",
45
+ neutral_hue="slate",
46
+ font=["Georgia", "Palatino Linotype", "serif"],
47
+ font_mono=["Consolas", "Courier New", "monospace"],
48
+ )
49
+
50
+
51
+ def hero_markdown() -> str:
52
+ return "\n".join(
53
+ [
54
+ f"# {PROJECT_NAME}",
55
+ f"## {PROJECT_TAGLINE}",
56
+ "",
57
+ "A self-contained, locally-run tool for biometric privacy preservation.",
58
+ "",
59
+ f"**{PHASE_LABEL} status:** {PHASE_STATUS}",
60
+ "",
61
+ PHASE_SUMMARY,
62
+ ]
63
+ )
64
+
65
+
66
+ def roadmap_markdown() -> str:
67
+ return "\n".join(
68
+ [
69
+ "### Current Workflow",
70
+ "1. Detect and align the primary face with MTCNN",
71
+ "2. Extract a 512-dimensional embedding with InceptionResnetV1",
72
+ "3. Score cosine similarity for verification",
73
+ "4. Run L-infinity PGD directly on the aligned face tensor",
74
+ "5. Return the cloaked aligned face and a perturbation preview",
75
+ ]
76
+ )
77
+
78
+
79
+ def _format_detection_probability(probability: float | None) -> str:
80
+ return "unknown" if probability is None else f"{probability:.4f}"
81
+
82
+
83
+ def compare_faces(image_a, image_b):
84
+ try:
85
+ if image_a is None or image_b is None:
86
+ raise FaceCloakError("Please provide both images before running a similarity check.")
87
+
88
+ detected_a = detect_primary_face(image_a)
89
+ detected_b = detect_primary_face(image_b)
90
+ embedding_a = extract_embedding_numpy(detected_a.tensor)
91
+ embedding_b = extract_embedding_numpy(detected_b.tensor)
92
+ similarity = cosine_similarity(embedding_a, embedding_b)
93
+ summary = "\n".join(
94
+ [
95
+ "### Similarity Result",
96
+ f"- Cosine Similarity: `{similarity:.4f}`",
97
+ f"- Image A Detection Confidence: `{_format_detection_probability(detected_a.probability)}`",
98
+ f"- Image B Detection Confidence: `{_format_detection_probability(detected_b.probability)}`",
99
+ "- Interpretation: values near `1.0` indicate the same person; values near `0` or below indicate unrelated identities.",
100
+ ]
101
+ )
102
+ return detected_a.image, detected_b.image, similarity, summary
103
+ except FaceCloakError as exc:
104
+ raise gr.Error(str(exc)) from exc
105
+
106
+
107
+ def cloak_face(image, epsilon, num_steps, l2_lambda):
108
+ try:
109
+ if image is None:
110
+ raise FaceCloakError("Please upload an image before running the cloaking attack.")
111
+
112
+ detected = detect_primary_face(image)
113
+ result = cloak_face_tensor(
114
+ detected.tensor,
115
+ parameters=CloakHyperparameters(
116
+ epsilon=float(epsilon),
117
+ num_steps=int(num_steps),
118
+ l2_lambda=float(l2_lambda),
119
+ ),
120
+ )
121
+ summary = "\n".join(
122
+ [
123
+ "### Cloaking Result",
124
+ f"- Detection Confidence: `{_format_detection_probability(detected.probability)}`",
125
+ f"- Original Similarity: `{result.original_similarity:.4f}`",
126
+ f"- Final Similarity: `{result.final_similarity:.4f}`",
127
+ f"- Similarity Drop: `{result.similarity_drop:.4f}`",
128
+ f"- Epsilon (L-inf): `{result.parameters.epsilon:.4f}`",
129
+ f"- Alpha: `{result.parameters.alpha:.4f}`",
130
+ f"- Steps: `{result.parameters.num_steps}`",
131
+ f"- Delta L-inf: `{result.delta_l_inf:.4f}`",
132
+ f"- Delta RMS: `{result.delta_rms:.4f}`",
133
+ ]
134
+ )
135
+ return (
136
+ result.original_face_image,
137
+ result.cloaked_face_image,
138
+ result.perturbation_preview,
139
+ result.final_similarity,
140
+ summary,
141
+ )
142
+ except FaceCloakError as exc:
143
+ raise gr.Error(str(exc)) from exc
144
+
145
+
146
+ def build_demo() -> gr.Blocks:
147
+ with gr.Blocks(title=PROJECT_NAME) as demo:
148
+ gr.Markdown(hero_markdown(), elem_id="hero")
149
+
150
+ with gr.Tab("Cloak Face"):
151
+ with gr.Row():
152
+ input_image = gr.Image(label="Input Portrait", type="pil")
153
+ aligned_face = gr.Image(label="Aligned Face", type="pil")
154
+ cloaked_face = gr.Image(label="Cloaked Face", type="pil")
155
+
156
+ with gr.Row():
157
+ perturbation = gr.Image(label="Perturbation Preview", type="pil")
158
+ with gr.Column():
159
+ epsilon = gr.Slider(
160
+ minimum=0.01,
161
+ maximum=0.05,
162
+ value=0.03,
163
+ step=0.005,
164
+ label="Epsilon (L-inf budget)",
165
+ )
166
+ num_steps = gr.Slider(
167
+ minimum=10,
168
+ maximum=60,
169
+ value=30,
170
+ step=5,
171
+ label="PGD Steps",
172
+ )
173
+ l2_lambda = gr.Slider(
174
+ minimum=0.0,
175
+ maximum=0.05,
176
+ value=0.01,
177
+ step=0.005,
178
+ label="L2 Regularization Weight",
179
+ )
180
+ cloak_button = gr.Button("Run Cloaking Attack", variant="primary")
181
+ final_similarity = gr.Number(
182
+ label="Final Original-vs-Cloaked Similarity",
183
+ precision=4,
184
+ )
185
+ cloak_summary = gr.Markdown(elem_classes=["panel"])
186
+
187
+ cloak_button.click(
188
+ fn=cloak_face,
189
+ inputs=[input_image, epsilon, num_steps, l2_lambda],
190
+ outputs=[
191
+ aligned_face,
192
+ cloaked_face,
193
+ perturbation,
194
+ final_similarity,
195
+ cloak_summary,
196
+ ],
197
+ )
198
+
199
+ with gr.Tab("Compare Faces"):
200
+ with gr.Row():
201
+ compare_image_a = gr.Image(label="Image A", type="pil")
202
+ compare_image_b = gr.Image(label="Image B", type="pil")
203
+ with gr.Row():
204
+ aligned_a = gr.Image(label="Aligned Face A", type="pil")
205
+ aligned_b = gr.Image(label="Aligned Face B", type="pil")
206
+ compare_button = gr.Button("Compare Embeddings", variant="secondary")
207
+ pair_similarity = gr.Number(label="Cosine Similarity", precision=4)
208
+ pair_summary = gr.Markdown(elem_classes=["panel"])
209
+ compare_button.click(
210
+ fn=compare_faces,
211
+ inputs=[compare_image_a, compare_image_b],
212
+ outputs=[aligned_a, aligned_b, pair_similarity, pair_summary],
213
+ )
214
+
215
+ with gr.Tab("Diagnostics"):
216
+ diagnostics = gr.Markdown(
217
+ value=render_runtime_markdown(),
218
+ elem_classes=["panel"],
219
+ )
220
+ refresh_button = gr.Button("Re-run environment check", variant="secondary")
221
+ refresh_button.click(fn=render_runtime_markdown, outputs=diagnostics)
222
+
223
+ with gr.Accordion("Pipeline Notes", open=False):
224
+ gr.Markdown(roadmap_markdown(), elem_classes=["panel"])
225
+
226
+ return demo
227
+
228
+
229
+ demo = build_demo()
facecloak/models.py ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Cached model factories for face detection and embedding extraction."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from functools import lru_cache
6
+ import os
7
+
8
+ from facenet_pytorch import InceptionResnetV1, MTCNN
9
+
10
+ from facecloak.project import TORCH_CACHE_DIR
11
+
12
+
13
+ def configure_torch_cache() -> None:
14
+ """Point Torch downloads at a repository-local cache."""
15
+
16
+ TORCH_CACHE_DIR.mkdir(parents=True, exist_ok=True)
17
+ os.environ.setdefault("TORCH_HOME", str(TORCH_CACHE_DIR))
18
+
19
+
20
+ @lru_cache(maxsize=1)
21
+ def get_face_detector() -> MTCNN:
22
+ configure_torch_cache()
23
+ return MTCNN(
24
+ image_size=160,
25
+ margin=20,
26
+ keep_all=False,
27
+ post_process=True,
28
+ device="cpu",
29
+ )
30
+
31
+
32
+ @lru_cache(maxsize=1)
33
+ def get_embedding_model() -> InceptionResnetV1:
34
+ configure_torch_cache()
35
+ model = InceptionResnetV1(pretrained="vggface2", device="cpu").eval()
36
+ for parameter in model.parameters():
37
+ parameter.requires_grad_(False)
38
+ return model
facecloak/pipeline.py ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Face detection, embedding extraction, and similarity helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Any
7
+
8
+ import numpy as np
9
+ from PIL import Image
10
+ import torch
11
+
12
+ from facecloak.errors import FaceCloakError
13
+ from facecloak.models import get_embedding_model, get_face_detector
14
+
15
+ DISPLAY_MIN = -1.0
16
+ DISPLAY_MAX = 1.0
17
+
18
+
19
+ @dataclass(slots=True)
20
+ class DetectedFace:
21
+ tensor: torch.Tensor
22
+ image: Image.Image
23
+ probability: float | None
24
+
25
+
26
+ def ensure_rgb(image: Image.Image) -> Image.Image:
27
+ return image if image.mode == "RGB" else image.convert("RGB")
28
+
29
+
30
+ def _model_device(model: Any) -> torch.device:
31
+ device = getattr(model, "device", None)
32
+ if isinstance(device, torch.device):
33
+ return device
34
+ return torch.device("cpu")
35
+
36
+
37
+ def _prepare_face_batch(face_tensor: torch.Tensor) -> torch.Tensor:
38
+ if face_tensor.ndim == 3:
39
+ batch = face_tensor.unsqueeze(0)
40
+ elif face_tensor.ndim == 4:
41
+ batch = face_tensor
42
+ else:
43
+ raise ValueError("Face tensors must have shape (3, H, W) or (N, 3, H, W).")
44
+
45
+ return batch.to(dtype=torch.float32)
46
+
47
+
48
+ def standardized_tensor_to_pil(face_tensor: torch.Tensor) -> Image.Image:
49
+ batch = _prepare_face_batch(face_tensor)
50
+ chw_tensor = batch[0].detach().cpu()
51
+ pixel_tensor = torch.clamp(chw_tensor * 128.0 + 127.5, 0.0, 255.0)
52
+ image_array = pixel_tensor.byte().permute(1, 2, 0).numpy()
53
+ return Image.fromarray(image_array, mode="RGB")
54
+
55
+
56
+ def perturbation_preview_image(delta_tensor: torch.Tensor) -> Image.Image:
57
+ batch = _prepare_face_batch(delta_tensor)
58
+ chw_tensor = batch[0].detach().cpu()
59
+ max_abs = float(chw_tensor.abs().max().item())
60
+ if max_abs == 0.0:
61
+ preview = torch.full_like(chw_tensor, 127.5)
62
+ else:
63
+ preview = (chw_tensor / max_abs) * 127.5 + 127.5
64
+ preview_array = torch.clamp(preview, 0.0, 255.0).byte().permute(1, 2, 0).numpy()
65
+ return Image.fromarray(preview_array, mode="RGB")
66
+
67
+
68
+ def detect_primary_face(image: Image.Image, detector: Any | None = None) -> DetectedFace:
69
+ detector = detector or get_face_detector()
70
+ rgb_image = ensure_rgb(image)
71
+ face_tensor, probability = detector(rgb_image, return_prob=True)
72
+
73
+ if face_tensor is None:
74
+ raise FaceCloakError(
75
+ "No face was detected. Please upload a clear image with one dominant, visible face."
76
+ )
77
+
78
+ return DetectedFace(
79
+ tensor=face_tensor.detach().cpu(),
80
+ image=standardized_tensor_to_pil(face_tensor),
81
+ probability=None if probability is None else float(probability),
82
+ )
83
+
84
+
85
+ def extract_embedding_tensor(
86
+ face_tensor: torch.Tensor,
87
+ model: Any | None = None,
88
+ ) -> torch.Tensor:
89
+ model = model or get_embedding_model()
90
+ batch = _prepare_face_batch(face_tensor).to(_model_device(model))
91
+ return model(batch)
92
+
93
+
94
+ def extract_embedding_numpy(
95
+ face_tensor: torch.Tensor,
96
+ model: Any | None = None,
97
+ ) -> np.ndarray:
98
+ with torch.no_grad():
99
+ embedding = extract_embedding_tensor(face_tensor, model=model)
100
+ return embedding[0].detach().cpu().numpy().astype(np.float32)
101
+
102
+
103
+ def cosine_similarity(first: np.ndarray, second: np.ndarray) -> float:
104
+ first_vector = np.asarray(first, dtype=np.float32).reshape(-1)
105
+ second_vector = np.asarray(second, dtype=np.float32).reshape(-1)
106
+
107
+ if first_vector.shape != second_vector.shape:
108
+ raise ValueError("Embedding vectors must have the same shape.")
109
+
110
+ first_norm = np.linalg.norm(first_vector)
111
+ second_norm = np.linalg.norm(second_vector)
112
+ if first_norm == 0.0 or second_norm == 0.0:
113
+ raise ValueError("Cosine similarity is undefined for zero-length vectors.")
114
+
115
+ return float(np.dot(first_vector / first_norm, second_vector / second_norm))
facecloak/project.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Project metadata and repository-level constants."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ PROJECT_NAME = "FaceCloak"
8
+ PROJECT_SLUG = "facecloak"
9
+ PROJECT_TAGLINE = "Adversarial Pixel Poisoning for Biometric Privacy Preservation"
10
+ PHASE_LABEL = "Phases 2-3"
11
+ PHASE_STATUS = "Complete"
12
+ PHASE_SUMMARY = (
13
+ "Face detection, embedding extraction, cosine similarity scoring, "
14
+ "and PGD-based face cloaking are now implemented."
15
+ )
16
+
17
+ PROJECT_ROOT = Path(__file__).resolve().parents[1]
18
+ TORCH_CACHE_DIR = PROJECT_ROOT / ".torch-cache"
19
+ SPACE_URL_TEMPLATE = "https://huggingface.co/spaces/{repo_id}"
20
+
21
+ PINNED_RUNTIME_DEPENDENCIES: tuple[tuple[str, str], ...] = (
22
+ ("torch", "2.2.2"),
23
+ ("torchvision", "0.17.2"),
24
+ ("facenet-pytorch", "2.6.0"),
25
+ ("pillow", "10.2.0"),
26
+ ("numpy", "1.26.4"),
27
+ ("gradio", "6.12.0"),
28
+ ("huggingface-hub", "1.10.2"),
29
+ )
30
+
31
+ SPACE_UPLOAD_ALLOW_PATTERNS: tuple[str, ...] = (
32
+ "app.py",
33
+ "main.py",
34
+ "README.md",
35
+ "requirements.txt",
36
+ "pyproject.toml",
37
+ "uv.lock",
38
+ "facecloak/**",
39
+ "scripts/**",
40
+ "tests/**",
41
+ )
42
+
43
+
44
+ def requirements_lines() -> list[str]:
45
+ """Return the direct runtime dependencies for Hugging Face Spaces."""
46
+
47
+ return [f"{name}=={version}" for name, version in PINNED_RUNTIME_DEPENDENCIES]
main.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ from facecloak.interface import APP_CSS, APP_THEME, demo
2
+
3
+
4
+ if __name__ == "__main__":
5
+ demo.launch(theme=APP_THEME, css=APP_CSS)
pyproject.toml ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [project]
2
+ name = "facecloak"
3
+ version = "0.1.0"
4
+ description = "Local adversarial pixel poisoning toolkit for biometric privacy preservation."
5
+ readme = "README.md"
6
+ requires-python = ">=3.12,<3.13"
7
+ dependencies = [
8
+ "facenet-pytorch==2.6.0",
9
+ "gradio==6.12.0",
10
+ "huggingface-hub==1.10.2",
11
+ "numpy==1.26.4",
12
+ "pillow==10.2.0",
13
+ "torch==2.2.2",
14
+ "torchvision==0.17.2",
15
+ ]
16
+
17
+ [dependency-groups]
18
+ dev = [
19
+ "pytest==9.0.3",
20
+ "pytest-cov==7.1.0",
21
+ ]
22
+
23
+ [tool.pytest.ini_options]
24
+ addopts = "-q -p no:cacheprovider"
25
+ testpaths = ["tests"]
26
+ markers = [
27
+ "integration: tests that exercise the real facenet-pytorch models and sample portraits",
28
+ ]
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ torch==2.2.2
2
+ torchvision==0.17.2
3
+ facenet-pytorch==2.6.0
4
+ pillow==10.2.0
5
+ numpy==1.26.4
6
+ gradio==6.12.0
7
+ huggingface-hub==1.10.2
scripts/create_or_update_space.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ PROJECT_ROOT = Path(__file__).resolve().parents[1]
7
+ if str(PROJECT_ROOT) not in sys.path:
8
+ sys.path.insert(0, str(PROJECT_ROOT))
9
+
10
+ from facecloak.deploy import create_or_update_space, deployment_markdown
11
+
12
+
13
+ def main() -> None:
14
+ repo_id = sys.argv[1] if len(sys.argv) > 1 else None
15
+ result = create_or_update_space(repo_id=repo_id)
16
+ print(deployment_markdown(result))
17
+
18
+
19
+ if __name__ == "__main__":
20
+ main()
tests/__pycache__/conftest.cpython-312-pytest-9.0.3.pyc ADDED
Binary file (647 Bytes). View file
 
tests/__pycache__/test_cloaking.cpython-312-pytest-9.0.3.pyc ADDED
Binary file (7.19 kB). View file
 
tests/__pycache__/test_deploy.cpython-312-pytest-9.0.3.pyc ADDED
Binary file (7.66 kB). View file
 
tests/__pycache__/test_environment.cpython-312-pytest-9.0.3.pyc ADDED
Binary file (8.17 kB). View file
 
tests/__pycache__/test_integration.cpython-312-pytest-9.0.3.pyc ADDED
Binary file (5.07 kB). View file
 
tests/__pycache__/test_interface.cpython-312-pytest-9.0.3.pyc ADDED
Binary file (7.65 kB). View file
 
tests/__pycache__/test_pipeline.cpython-312-pytest-9.0.3.pyc ADDED
Binary file (12.2 kB). View file
 
tests/__pycache__/test_project.cpython-312-pytest-9.0.3.pyc ADDED
Binary file (2.56 kB). View file
 
tests/conftest.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ import sys
2
+ from pathlib import Path
3
+
4
+ PROJECT_ROOT = Path(__file__).resolve().parents[1]
5
+
6
+ if str(PROJECT_ROOT) not in sys.path:
7
+ sys.path.insert(0, str(PROJECT_ROOT))
tests/fixtures/README.md ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ # Test Fixtures
2
+
3
+ The portrait images in `tests/fixtures/faces/` are public-domain source images downloaded from Wikimedia Commons so the embedding pipeline can be tested end to end.
4
+
5
+ - `obama_a.jpg`: Official portrait of Barack Obama (cropped)
6
+ - `obama_b.jpg`: Alternate official portrait of Barack Obama
7
+ - `george_a.jpg`: George W. Bush cropped portrait
8
+
9
+ These fixtures are used only for local verification of face detection, embedding extraction, and cloaking behavior.
tests/fixtures/faces/george_a.jpg ADDED

Git LFS Details

  • SHA256: d98a26332c2a9c29769d19c03adbff1d9bc28b5066433022850879f3aacddba5
  • Pointer size: 132 Bytes
  • Size of remote file: 2.36 MB
tests/fixtures/faces/obama_a.jpg ADDED

Git LFS Details

  • SHA256: 4ed7d19964316d9aa87fa1d6731651615f8f39a2140f3fec81f487776b43a691
  • Pointer size: 132 Bytes
  • Size of remote file: 1.05 MB
tests/fixtures/faces/obama_b.jpg ADDED

Git LFS Details

  • SHA256: c00719b14633924a844e3a174491411e7f7d106edb9a57d116518bd385f27b20
  • Pointer size: 131 Bytes
  • Size of remote file: 703 kB
tests/test_cloaking.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pytest
2
+ import torch
3
+ import torch.nn.functional as F
4
+
5
+ from facecloak.cloaking import CloakHyperparameters, cloak_face_tensor
6
+
7
+
8
+ class TinyEmbeddingModel(torch.nn.Module):
9
+ def __init__(self) -> None:
10
+ super().__init__()
11
+ self.device = torch.device("cpu")
12
+ self.scale = torch.nn.Parameter(torch.tensor(1.0), requires_grad=False)
13
+
14
+ def forward(self, batch: torch.Tensor) -> torch.Tensor:
15
+ flattened = batch.reshape(batch.shape[0], -1)
16
+ features = flattened[:, :4] * self.scale
17
+ return F.normalize(features, p=2, dim=1)
18
+
19
+
20
+ def test_cloak_face_tensor_reduces_similarity_and_respects_budget() -> None:
21
+ model = TinyEmbeddingModel()
22
+ face_tensor = torch.tensor(
23
+ [
24
+ [[0.9, 0.8], [0.7, 0.6]],
25
+ [[0.1, 0.2], [0.3, 0.4]],
26
+ [[0.5, 0.4], [0.3, 0.2]],
27
+ ],
28
+ dtype=torch.float32,
29
+ )
30
+ result = cloak_face_tensor(
31
+ face_tensor,
32
+ model=model,
33
+ parameters=CloakHyperparameters(epsilon=0.2, num_steps=10, l2_lambda=0.0),
34
+ )
35
+
36
+ assert result.final_similarity < result.original_similarity
37
+ assert result.delta_l_inf == pytest.approx(0.2, abs=1e-6)
38
+ assert len(result.loss_history) == 10
39
+ assert len(result.similarity_history) == 10
40
+
41
+
42
+ def test_cloak_face_tensor_validates_hyperparameters() -> None:
43
+ with pytest.raises(ValueError):
44
+ cloak_face_tensor(
45
+ torch.zeros(3, 2, 2),
46
+ model=TinyEmbeddingModel(),
47
+ parameters=CloakHyperparameters(epsilon=0.0),
48
+ )
tests/test_deploy.py ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+ import shutil
3
+ import uuid
4
+
5
+ from facecloak.deploy import (
6
+ HF_TOKEN_ENV_VAR,
7
+ create_or_update_space,
8
+ default_space_repo_id,
9
+ read_env_file,
10
+ )
11
+
12
+
13
+ class DummyRuntime:
14
+ stage = "BUILDING"
15
+ hardware = "cpu-basic"
16
+
17
+
18
+ class DummyCommit:
19
+ oid = "abc123"
20
+
21
+
22
+ class DummyApi:
23
+ def __init__(self) -> None:
24
+ self.created = None
25
+ self.uploaded = None
26
+
27
+ def whoami(self, token, cache=False):
28
+ return {"name": "example-user"}
29
+
30
+ def create_repo(self, **kwargs):
31
+ self.created = kwargs
32
+
33
+ def upload_folder(self, **kwargs):
34
+ self.uploaded = kwargs
35
+ return DummyCommit()
36
+
37
+ def get_space_runtime(self, repo_id, token=None):
38
+ return DummyRuntime()
39
+
40
+
41
+ def _local_temp_dir(name: str) -> Path:
42
+ path = Path("tests") / "_tmp" / f"{name}_{uuid.uuid4().hex}"
43
+ path.mkdir(parents=True, exist_ok=True)
44
+ return path
45
+
46
+
47
+ def test_read_env_file_parses_simple_key_values() -> None:
48
+ temp_dir = _local_temp_dir("env")
49
+ env_path = temp_dir / ".env"
50
+ env_path.write_text(f"{HF_TOKEN_ENV_VAR}=hf_test_token\nOTHER=value\n", encoding="utf-8")
51
+
52
+ values = read_env_file(env_path)
53
+
54
+ assert values[HF_TOKEN_ENV_VAR] == "hf_test_token"
55
+ assert values["OTHER"] == "value"
56
+ shutil.rmtree(temp_dir)
57
+
58
+
59
+ def test_default_space_repo_id_uses_hf_username() -> None:
60
+ repo_id = default_space_repo_id(DummyApi(), "hf_test_token")
61
+
62
+ assert repo_id == "example-user/facecloak"
63
+
64
+
65
+ def test_create_or_update_space_uses_space_repo_settings() -> None:
66
+ api = DummyApi()
67
+ temp_dir = _local_temp_dir("deploy")
68
+ folder_path = temp_dir / "repo"
69
+ folder_path.mkdir()
70
+ (folder_path / "app.py").write_text("print('ok')\n", encoding="utf-8")
71
+
72
+ result = create_or_update_space(
73
+ api=api,
74
+ token="hf_test_token",
75
+ repo_id="example-user/facecloak",
76
+ folder_path=folder_path,
77
+ )
78
+
79
+ assert api.created["repo_type"] == "space"
80
+ assert api.created["space_sdk"] == "gradio"
81
+ assert api.uploaded["repo_type"] == "space"
82
+ assert result.repo_id == "example-user/facecloak"
83
+ assert result.runtime_stage == "BUILDING"
84
+ shutil.rmtree(temp_dir)
tests/test_environment.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from facecloak.environment import collect_runtime_report, render_runtime_markdown
2
+ from facecloak.project import PHASE_LABEL
3
+
4
+
5
+ def test_collect_runtime_report_returns_expected_values() -> None:
6
+ report = collect_runtime_report()
7
+
8
+ assert report.phase == PHASE_LABEL
9
+ assert report.status == "ready"
10
+ assert report.tensor_sanity == (1, 2, 3)
11
+ assert report.device == ("cuda" if report.cuda_available else "cpu")
12
+ assert report.torch_version.startswith("2.2.2")
13
+ assert report.facenet_pytorch_version == "2.6.0"
14
+ assert report.gradio_version == "6.12.0"
15
+ assert report.huggingface_hub_version == "1.10.2"
16
+
17
+
18
+ def test_render_runtime_markdown_contains_environment_summary() -> None:
19
+ markdown = render_runtime_markdown()
20
+
21
+ assert "Runtime Diagnostics" in markdown
22
+ assert "CUDA Available" in markdown
23
+ assert "Torch Cache" in markdown
24
+ assert "PGD-based face cloaking" in markdown
tests/test_integration.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+
3
+ from PIL import Image
4
+ import pytest
5
+
6
+ from facecloak.cloaking import CloakHyperparameters, cloak_face_tensor
7
+ from facecloak.pipeline import cosine_similarity, detect_primary_face, extract_embedding_numpy
8
+
9
+ FIXTURE_DIR = Path("tests/fixtures/faces")
10
+
11
+
12
+ @pytest.mark.integration
13
+ def test_real_face_similarity_pipeline_matches_expected_ranges() -> None:
14
+ obama_a = Image.open(FIXTURE_DIR / "obama_a.jpg").convert("RGB")
15
+ obama_b = Image.open(FIXTURE_DIR / "obama_b.jpg").convert("RGB")
16
+ george_a = Image.open(FIXTURE_DIR / "george_a.jpg").convert("RGB")
17
+
18
+ obama_a_embedding = extract_embedding_numpy(detect_primary_face(obama_a).tensor)
19
+ obama_b_embedding = extract_embedding_numpy(detect_primary_face(obama_b).tensor)
20
+ george_embedding = extract_embedding_numpy(detect_primary_face(george_a).tensor)
21
+
22
+ same_score = cosine_similarity(obama_a_embedding, obama_b_embedding)
23
+ different_score = cosine_similarity(obama_a_embedding, george_embedding)
24
+
25
+ assert same_score > 0.8
26
+ assert different_score < 0.3
27
+
28
+
29
+ @pytest.mark.integration
30
+ def test_real_face_cloaking_substantially_lowers_similarity() -> None:
31
+ obama_a = Image.open(FIXTURE_DIR / "obama_a.jpg").convert("RGB")
32
+ detected = detect_primary_face(obama_a)
33
+
34
+ result = cloak_face_tensor(
35
+ detected.tensor,
36
+ parameters=CloakHyperparameters(epsilon=0.03, num_steps=15, l2_lambda=0.01),
37
+ )
38
+
39
+ assert result.final_similarity < 0.35
40
+ assert result.delta_l_inf <= 0.03 + 1e-6
tests/test_interface.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ from PIL import Image
3
+
4
+ from facecloak.interface import build_demo, hero_markdown, roadmap_markdown
5
+ from facecloak.project import PROJECT_NAME, PROJECT_TAGLINE
6
+
7
+
8
+ def test_hero_markdown_contains_project_identity() -> None:
9
+ text = hero_markdown()
10
+
11
+ assert PROJECT_NAME in text
12
+ assert PROJECT_TAGLINE in text
13
+ assert "Phases 2-3 status" in text
14
+
15
+
16
+ def test_roadmap_markdown_mentions_current_pipeline() -> None:
17
+ text = roadmap_markdown()
18
+
19
+ assert "Detect and align the primary face with MTCNN" in text
20
+ assert "Extract a 512-dimensional embedding" in text
21
+ assert "Run L-infinity PGD" in text
22
+
23
+
24
+ def test_build_demo_returns_gradio_blocks_with_project_title() -> None:
25
+ demo = build_demo()
26
+
27
+ assert isinstance(demo, gr.Blocks)
28
+ assert demo.title == PROJECT_NAME
29
+
30
+
31
+ def test_compare_faces_requires_both_images() -> None:
32
+ from facecloak.interface import compare_faces
33
+
34
+ blank = Image.new("RGB", (16, 16), "white")
35
+ try:
36
+ compare_faces(blank, None)
37
+ except gr.Error as exc:
38
+ assert "Please provide both images" in str(exc)
39
+ else:
40
+ raise AssertionError("compare_faces should reject missing inputs.")
tests/test_pipeline.py ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ from PIL import Image
3
+ import pytest
4
+ import torch
5
+
6
+ from facecloak.errors import FaceCloakError
7
+ from facecloak.pipeline import (
8
+ cosine_similarity,
9
+ detect_primary_face,
10
+ extract_embedding_tensor,
11
+ perturbation_preview_image,
12
+ standardized_tensor_to_pil,
13
+ )
14
+
15
+
16
+ class DummyDetector:
17
+ def __call__(self, _image, return_prob=False):
18
+ face_tensor = torch.zeros(3, 160, 160)
19
+ probability = 0.97
20
+ if return_prob:
21
+ return face_tensor, probability
22
+ return face_tensor
23
+
24
+
25
+ class MissingFaceDetector:
26
+ def __call__(self, _image, return_prob=False):
27
+ if return_prob:
28
+ return None, None
29
+ return None
30
+
31
+
32
+ class DummyEmbeddingModel(torch.nn.Module):
33
+ def __init__(self) -> None:
34
+ super().__init__()
35
+ self.device = torch.device("cpu")
36
+ self.projection = torch.nn.Linear(3 * 4 * 4, 4, bias=False)
37
+ with torch.no_grad():
38
+ self.projection.weight.copy_(torch.eye(4, 3 * 4 * 4))
39
+
40
+ def forward(self, batch: torch.Tensor) -> torch.Tensor:
41
+ flat = batch.reshape(batch.shape[0], -1)
42
+ return self.projection(flat)
43
+
44
+
45
+ def test_detect_primary_face_returns_probability_and_image() -> None:
46
+ detected = detect_primary_face(Image.new("RGB", (64, 64), "white"), detector=DummyDetector())
47
+
48
+ assert detected.tensor.shape == (3, 160, 160)
49
+ assert detected.probability == pytest.approx(0.97)
50
+ assert detected.image.size == (160, 160)
51
+
52
+
53
+ def test_detect_primary_face_raises_readable_error_when_no_face_found() -> None:
54
+ with pytest.raises(FaceCloakError):
55
+ detect_primary_face(Image.new("RGB", (64, 64), "white"), detector=MissingFaceDetector())
56
+
57
+
58
+ def test_extract_embedding_tensor_preserves_gradients() -> None:
59
+ face_tensor = torch.ones(3, 4, 4, requires_grad=True)
60
+ model = DummyEmbeddingModel()
61
+
62
+ embedding = extract_embedding_tensor(face_tensor, model=model)
63
+ embedding.sum().backward()
64
+
65
+ assert embedding.shape == (1, 4)
66
+ assert face_tensor.grad is not None
67
+
68
+
69
+ def test_cosine_similarity_behaves_for_identical_and_opposite_vectors() -> None:
70
+ same = cosine_similarity(np.array([1.0, 2.0]), np.array([1.0, 2.0]))
71
+ opposite = cosine_similarity(np.array([1.0, 0.0]), np.array([-1.0, 0.0]))
72
+
73
+ assert same == pytest.approx(1.0)
74
+ assert opposite == pytest.approx(-1.0)
75
+
76
+
77
+ def test_tensor_visualization_helpers_return_pil_images() -> None:
78
+ face_tensor = torch.zeros(3, 160, 160)
79
+ face_image = standardized_tensor_to_pil(face_tensor)
80
+ delta_preview = perturbation_preview_image(torch.zeros(3, 160, 160))
81
+
82
+ assert face_image.size == (160, 160)
83
+ assert delta_preview.size == (160, 160)
tests/test_project.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+
3
+ from facecloak.project import PHASE_LABEL, requirements_lines
4
+
5
+
6
+ def test_requirements_file_matches_pinned_runtime_dependencies() -> None:
7
+ requirements = [
8
+ line.strip()
9
+ for line in Path("requirements.txt").read_text(encoding="utf-8").splitlines()
10
+ if line.strip()
11
+ ]
12
+
13
+ assert requirements == requirements_lines()
14
+
15
+
16
+ def test_phase_label_tracks_current_delivery() -> None:
17
+ assert PHASE_LABEL == "Phases 2-3"
uv.lock ADDED
@@ -0,0 +1,1032 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version = 1
2
+ revision = 3
3
+ requires-python = "==3.12.*"
4
+ resolution-markers = [
5
+ "sys_platform == 'win32'",
6
+ "sys_platform == 'emscripten'",
7
+ "sys_platform != 'emscripten' and sys_platform != 'win32'",
8
+ ]
9
+
10
+ [[package]]
11
+ name = "annotated-doc"
12
+ version = "0.0.4"
13
+ source = { registry = "https://pypi.org/simple" }
14
+ sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" }
15
+ wheels = [
16
+ { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" },
17
+ ]
18
+
19
+ [[package]]
20
+ name = "annotated-types"
21
+ version = "0.7.0"
22
+ source = { registry = "https://pypi.org/simple" }
23
+ sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
24
+ wheels = [
25
+ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
26
+ ]
27
+
28
+ [[package]]
29
+ name = "anyio"
30
+ version = "4.13.0"
31
+ source = { registry = "https://pypi.org/simple" }
32
+ dependencies = [
33
+ { name = "idna" },
34
+ { name = "typing-extensions" },
35
+ ]
36
+ sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" }
37
+ wheels = [
38
+ { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" },
39
+ ]
40
+
41
+ [[package]]
42
+ name = "brotli"
43
+ version = "1.2.0"
44
+ source = { registry = "https://pypi.org/simple" }
45
+ sdist = { url = "https://files.pythonhosted.org/packages/f7/16/c92ca344d646e71a43b8bb353f0a6490d7f6e06210f8554c8f874e454285/brotli-1.2.0.tar.gz", hash = "sha256:e310f77e41941c13340a95976fe66a8a95b01e783d430eeaf7a2f87e0a57dd0a", size = 7388632, upload-time = "2025-11-05T18:39:42.86Z" }
46
+ wheels = [
47
+ { url = "https://files.pythonhosted.org/packages/11/ee/b0a11ab2315c69bb9b45a2aaed022499c9c24a205c3a49c3513b541a7967/brotli-1.2.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:35d382625778834a7f3061b15423919aa03e4f5da34ac8e02c074e4b75ab4f84", size = 861543, upload-time = "2025-11-05T18:38:24.183Z" },
48
+ { url = "https://files.pythonhosted.org/packages/e1/2f/29c1459513cd35828e25531ebfcbf3e92a5e49f560b1777a9af7203eb46e/brotli-1.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7a61c06b334bd99bc5ae84f1eeb36bfe01400264b3c352f968c6e30a10f9d08b", size = 444288, upload-time = "2025-11-05T18:38:25.139Z" },
49
+ { url = "https://files.pythonhosted.org/packages/3d/6f/feba03130d5fceadfa3a1bb102cb14650798c848b1df2a808356f939bb16/brotli-1.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:acec55bb7c90f1dfc476126f9711a8e81c9af7fb617409a9ee2953115343f08d", size = 1528071, upload-time = "2025-11-05T18:38:26.081Z" },
50
+ { url = "https://files.pythonhosted.org/packages/2b/38/f3abb554eee089bd15471057ba85f47e53a44a462cfce265d9bf7088eb09/brotli-1.2.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:260d3692396e1895c5034f204f0db022c056f9e2ac841593a4cf9426e2a3faca", size = 1626913, upload-time = "2025-11-05T18:38:27.284Z" },
51
+ { url = "https://files.pythonhosted.org/packages/03/a7/03aa61fbc3c5cbf99b44d158665f9b0dd3d8059be16c460208d9e385c837/brotli-1.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:072e7624b1fc4d601036ab3f4f27942ef772887e876beff0301d261210bca97f", size = 1419762, upload-time = "2025-11-05T18:38:28.295Z" },
52
+ { url = "https://files.pythonhosted.org/packages/21/1b/0374a89ee27d152a5069c356c96b93afd1b94eae83f1e004b57eb6ce2f10/brotli-1.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adedc4a67e15327dfdd04884873c6d5a01d3e3b6f61406f99b1ed4865a2f6d28", size = 1484494, upload-time = "2025-11-05T18:38:29.29Z" },
53
+ { url = "https://files.pythonhosted.org/packages/cf/57/69d4fe84a67aef4f524dcd075c6eee868d7850e85bf01d778a857d8dbe0a/brotli-1.2.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7a47ce5c2288702e09dc22a44d0ee6152f2c7eda97b3c8482d826a1f3cfc7da7", size = 1593302, upload-time = "2025-11-05T18:38:30.639Z" },
54
+ { url = "https://files.pythonhosted.org/packages/d5/3b/39e13ce78a8e9a621c5df3aeb5fd181fcc8caba8c48a194cd629771f6828/brotli-1.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:af43b8711a8264bb4e7d6d9a6d004c3a2019c04c01127a868709ec29962b6036", size = 1487913, upload-time = "2025-11-05T18:38:31.618Z" },
55
+ { url = "https://files.pythonhosted.org/packages/62/28/4d00cb9bd76a6357a66fcd54b4b6d70288385584063f4b07884c1e7286ac/brotli-1.2.0-cp312-cp312-win32.whl", hash = "sha256:e99befa0b48f3cd293dafeacdd0d191804d105d279e0b387a32054c1180f3161", size = 334362, upload-time = "2025-11-05T18:38:32.939Z" },
56
+ { url = "https://files.pythonhosted.org/packages/1c/4e/bc1dcac9498859d5e353c9b153627a3752868a9d5f05ce8dedd81a2354ab/brotli-1.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:b35c13ce241abdd44cb8ca70683f20c0c079728a36a996297adb5334adfc1c44", size = 369115, upload-time = "2025-11-05T18:38:33.765Z" },
57
+ ]
58
+
59
+ [[package]]
60
+ name = "certifi"
61
+ version = "2026.2.25"
62
+ source = { registry = "https://pypi.org/simple" }
63
+ sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" }
64
+ wheels = [
65
+ { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" },
66
+ ]
67
+
68
+ [[package]]
69
+ name = "charset-normalizer"
70
+ version = "3.4.7"
71
+ source = { registry = "https://pypi.org/simple" }
72
+ sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" }
73
+ wheels = [
74
+ { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" },
75
+ { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" },
76
+ { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" },
77
+ { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" },
78
+ { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" },
79
+ { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" },
80
+ { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" },
81
+ { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" },
82
+ { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" },
83
+ { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" },
84
+ { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" },
85
+ { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" },
86
+ { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" },
87
+ { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" },
88
+ { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" },
89
+ { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" },
90
+ { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" },
91
+ ]
92
+
93
+ [[package]]
94
+ name = "click"
95
+ version = "8.3.2"
96
+ source = { registry = "https://pypi.org/simple" }
97
+ dependencies = [
98
+ { name = "colorama", marker = "sys_platform == 'win32'" },
99
+ ]
100
+ sdist = { url = "https://files.pythonhosted.org/packages/57/75/31212c6bf2503fdf920d87fee5d7a86a2e3bcf444984126f13d8e4016804/click-8.3.2.tar.gz", hash = "sha256:14162b8b3b3550a7d479eafa77dfd3c38d9dc8951f6f69c78913a8f9a7540fd5", size = 302856, upload-time = "2026-04-03T19:14:45.118Z" }
101
+ wheels = [
102
+ { url = "https://files.pythonhosted.org/packages/e4/20/71885d8b97d4f3dde17b1fdb92dbd4908b00541c5a3379787137285f602e/click-8.3.2-py3-none-any.whl", hash = "sha256:1924d2c27c5653561cd2cae4548d1406039cb79b858b747cfea24924bbc1616d", size = 108379, upload-time = "2026-04-03T19:14:43.505Z" },
103
+ ]
104
+
105
+ [[package]]
106
+ name = "colorama"
107
+ version = "0.4.6"
108
+ source = { registry = "https://pypi.org/simple" }
109
+ sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
110
+ wheels = [
111
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
112
+ ]
113
+
114
+ [[package]]
115
+ name = "coverage"
116
+ version = "7.13.5"
117
+ source = { registry = "https://pypi.org/simple" }
118
+ sdist = { url = "https://files.pythonhosted.org/packages/9d/e0/70553e3000e345daff267cec284ce4cbf3fc141b6da229ac52775b5428f1/coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179", size = 915967, upload-time = "2026-03-17T10:33:18.341Z" }
119
+ wheels = [
120
+ { url = "https://files.pythonhosted.org/packages/a0/c3/a396306ba7db865bf96fc1fb3b7fd29bcbf3d829df642e77b13555163cd6/coverage-7.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:460cf0114c5016fa841214ff5564aa4864f11948da9440bc97e21ad1f4ba1e01", size = 219554, upload-time = "2026-03-17T10:30:42.208Z" },
121
+ { url = "https://files.pythonhosted.org/packages/a6/16/a68a19e5384e93f811dccc51034b1fd0b865841c390e3c931dcc4699e035/coverage-7.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e223ce4b4ed47f065bfb123687686512e37629be25cc63728557ae7db261422", size = 219908, upload-time = "2026-03-17T10:30:43.906Z" },
122
+ { url = "https://files.pythonhosted.org/packages/29/72/20b917c6793af3a5ceb7fb9c50033f3ec7865f2911a1416b34a7cfa0813b/coverage-7.13.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e3370441f4513c6252bf042b9c36d22491142385049243253c7e48398a15a9f", size = 251419, upload-time = "2026-03-17T10:30:45.545Z" },
123
+ { url = "https://files.pythonhosted.org/packages/8c/49/cd14b789536ac6a4778c453c6a2338bc0a2fb60c5a5a41b4008328b9acc1/coverage-7.13.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:03ccc709a17a1de074fb1d11f217342fb0d2b1582ed544f554fc9fc3f07e95f5", size = 254159, upload-time = "2026-03-17T10:30:47.204Z" },
124
+ { url = "https://files.pythonhosted.org/packages/9d/00/7b0edcfe64e2ed4c0340dac14a52ad0f4c9bd0b8b5e531af7d55b703db7c/coverage-7.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f4818d065964db3c1c66dc0fbdac5ac692ecbc875555e13374fdbe7eedb4376", size = 255270, upload-time = "2026-03-17T10:30:48.812Z" },
125
+ { url = "https://files.pythonhosted.org/packages/93/89/7ffc4ba0f5d0a55c1e84ea7cee39c9fc06af7b170513d83fbf3bbefce280/coverage-7.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:012d5319e66e9d5a218834642d6c35d265515a62f01157a45bcc036ecf947256", size = 257538, upload-time = "2026-03-17T10:30:50.77Z" },
126
+ { url = "https://files.pythonhosted.org/packages/81/bd/73ddf85f93f7e6fa83e77ccecb6162d9415c79007b4bc124008a4995e4a7/coverage-7.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8dd02af98971bdb956363e4827d34425cb3df19ee550ef92855b0acb9c7ce51c", size = 251821, upload-time = "2026-03-17T10:30:52.5Z" },
127
+ { url = "https://files.pythonhosted.org/packages/a0/81/278aff4e8dec4926a0bcb9486320752811f543a3ce5b602cc7a29978d073/coverage-7.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f08fd75c50a760c7eb068ae823777268daaf16a80b918fa58eea888f8e3919f5", size = 253191, upload-time = "2026-03-17T10:30:54.543Z" },
128
+ { url = "https://files.pythonhosted.org/packages/70/ee/fe1621488e2e0a58d7e94c4800f0d96f79671553488d401a612bebae324b/coverage-7.13.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:843ea8643cf967d1ac7e8ecd4bb00c99135adf4816c0c0593fdcc47b597fcf09", size = 251337, upload-time = "2026-03-17T10:30:56.663Z" },
129
+ { url = "https://files.pythonhosted.org/packages/37/a6/f79fb37aa104b562207cc23cb5711ab6793608e246cae1e93f26b2236ed9/coverage-7.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9d44d7aa963820b1b971dbecd90bfe5fe8f81cff79787eb6cca15750bd2f79b9", size = 255404, upload-time = "2026-03-17T10:30:58.427Z" },
130
+ { url = "https://files.pythonhosted.org/packages/75/f0/ed15262a58ec81ce457ceb717b7f78752a1713556b19081b76e90896e8d4/coverage-7.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7132bed4bd7b836200c591410ae7d97bf7ae8be6fc87d160b2bd881df929e7bf", size = 250903, upload-time = "2026-03-17T10:31:00.093Z" },
131
+ { url = "https://files.pythonhosted.org/packages/0f/e9/9129958f20e7e9d4d56d51d42ccf708d15cac355ff4ac6e736e97a9393d2/coverage-7.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a698e363641b98843c517817db75373c83254781426e94ada3197cabbc2c919c", size = 252780, upload-time = "2026-03-17T10:31:01.916Z" },
132
+ { url = "https://files.pythonhosted.org/packages/a4/d7/0ad9b15812d81272db94379fe4c6df8fd17781cc7671fdfa30c76ba5ff7b/coverage-7.13.5-cp312-cp312-win32.whl", hash = "sha256:bdba0a6b8812e8c7df002d908a9a2ea3c36e92611b5708633c50869e6d922fdf", size = 222093, upload-time = "2026-03-17T10:31:03.642Z" },
133
+ { url = "https://files.pythonhosted.org/packages/29/3d/821a9a5799fac2556bcf0bd37a70d1d11fa9e49784b6d22e92e8b2f85f18/coverage-7.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:d2c87e0c473a10bffe991502eac389220533024c8082ec1ce849f4218dded810", size = 222900, upload-time = "2026-03-17T10:31:05.651Z" },
134
+ { url = "https://files.pythonhosted.org/packages/d4/fa/2238c2ad08e35cf4f020ea721f717e09ec3152aea75d191a7faf3ef009a8/coverage-7.13.5-cp312-cp312-win_arm64.whl", hash = "sha256:bf69236a9a81bdca3bff53796237aab096cdbf8d78a66ad61e992d9dac7eb2de", size = 221515, upload-time = "2026-03-17T10:31:07.293Z" },
135
+ { url = "https://files.pythonhosted.org/packages/9e/ee/a4cf96b8ce1e566ed238f0659ac2d3f007ed1d14b181bcb684e19561a69a/coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61", size = 211346, upload-time = "2026-03-17T10:33:15.691Z" },
136
+ ]
137
+
138
+ [[package]]
139
+ name = "facecloak"
140
+ version = "0.1.0"
141
+ source = { virtual = "." }
142
+ dependencies = [
143
+ { name = "facenet-pytorch" },
144
+ { name = "gradio" },
145
+ { name = "huggingface-hub" },
146
+ { name = "numpy" },
147
+ { name = "pillow" },
148
+ { name = "torch" },
149
+ { name = "torchvision" },
150
+ ]
151
+
152
+ [package.dev-dependencies]
153
+ dev = [
154
+ { name = "pytest" },
155
+ { name = "pytest-cov" },
156
+ ]
157
+
158
+ [package.metadata]
159
+ requires-dist = [
160
+ { name = "facenet-pytorch", specifier = "==2.6.0" },
161
+ { name = "gradio", specifier = "==6.12.0" },
162
+ { name = "huggingface-hub", specifier = "==1.10.2" },
163
+ { name = "numpy", specifier = "==1.26.4" },
164
+ { name = "pillow", specifier = "==10.2.0" },
165
+ { name = "torch", specifier = "==2.2.2" },
166
+ { name = "torchvision", specifier = "==0.17.2" },
167
+ ]
168
+
169
+ [package.metadata.requires-dev]
170
+ dev = [
171
+ { name = "pytest", specifier = "==9.0.3" },
172
+ { name = "pytest-cov", specifier = "==7.1.0" },
173
+ ]
174
+
175
+ [[package]]
176
+ name = "facenet-pytorch"
177
+ version = "2.6.0"
178
+ source = { registry = "https://pypi.org/simple" }
179
+ dependencies = [
180
+ { name = "numpy" },
181
+ { name = "pillow" },
182
+ { name = "requests" },
183
+ { name = "torch" },
184
+ { name = "torchvision" },
185
+ { name = "tqdm" },
186
+ ]
187
+ wheels = [
188
+ { url = "https://files.pythonhosted.org/packages/ed/2e/2d56386bc2f834cdc683743903852cf1428b4e5ee119f16cf808b589d3cd/facenet_pytorch-2.6.0-py3-none-any.whl", hash = "sha256:ecb82b27beb226d106f2219efe8f829b01b87a8595badd01545679bdb9f19cca", size = 1881930, upload-time = "2024-04-29T17:50:14.053Z" },
189
+ ]
190
+
191
+ [[package]]
192
+ name = "fastapi"
193
+ version = "0.135.3"
194
+ source = { registry = "https://pypi.org/simple" }
195
+ dependencies = [
196
+ { name = "annotated-doc" },
197
+ { name = "pydantic" },
198
+ { name = "starlette" },
199
+ { name = "typing-extensions" },
200
+ { name = "typing-inspection" },
201
+ ]
202
+ sdist = { url = "https://files.pythonhosted.org/packages/f7/e6/7adb4c5fa231e82c35b8f5741a9f2d055f520c29af5546fd70d3e8e1cd2e/fastapi-0.135.3.tar.gz", hash = "sha256:bd6d7caf1a2bdd8d676843cdcd2287729572a1ef524fc4d65c17ae002a1be654", size = 396524, upload-time = "2026-04-01T16:23:58.188Z" }
203
+ wheels = [
204
+ { url = "https://files.pythonhosted.org/packages/84/a4/5caa2de7f917a04ada20018eccf60d6cc6145b0199d55ca3711b0fc08312/fastapi-0.135.3-py3-none-any.whl", hash = "sha256:9b0f590c813acd13d0ab43dd8494138eb58e484bfac405db1f3187cfc5810d98", size = 117734, upload-time = "2026-04-01T16:23:59.328Z" },
205
+ ]
206
+
207
+ [[package]]
208
+ name = "filelock"
209
+ version = "3.28.0"
210
+ source = { registry = "https://pypi.org/simple" }
211
+ sdist = { url = "https://files.pythonhosted.org/packages/d6/17/6e8890271880903e3538660a21d63a6c1fea969ac71d0d6b608b78727fa9/filelock-3.28.0.tar.gz", hash = "sha256:4ed1010aae813c4ee8d9c660e4792475ee60c4a0ba76073ceaf862bd317e3ca6", size = 56474, upload-time = "2026-04-14T22:54:33.625Z" }
212
+ wheels = [
213
+ { url = "https://files.pythonhosted.org/packages/3b/21/2f728888c45033d34a417bfcd248ea2564c9e08ab1bfd301377cf05d5586/filelock-3.28.0-py3-none-any.whl", hash = "sha256:de9af6712788e7171df1b28b15eba2446c69721433fa427a9bee07b17820a9db", size = 39189, upload-time = "2026-04-14T22:54:32.037Z" },
214
+ ]
215
+
216
+ [[package]]
217
+ name = "fsspec"
218
+ version = "2026.3.0"
219
+ source = { registry = "https://pypi.org/simple" }
220
+ sdist = { url = "https://files.pythonhosted.org/packages/e1/cf/b50ddf667c15276a9ab15a70ef5f257564de271957933ffea49d2cdbcdfb/fsspec-2026.3.0.tar.gz", hash = "sha256:1ee6a0e28677557f8c2f994e3eea77db6392b4de9cd1f5d7a9e87a0ae9d01b41", size = 313547, upload-time = "2026-03-27T19:11:14.892Z" }
221
+ wheels = [
222
+ { url = "https://files.pythonhosted.org/packages/d5/1f/5f4a3cd9e4440e9d9bc78ad0a91a1c8d46b4d429d5239ebe6793c9fe5c41/fsspec-2026.3.0-py3-none-any.whl", hash = "sha256:d2ceafaad1b3457968ed14efa28798162f1638dbb5d2a6868a2db002a5ee39a4", size = 202595, upload-time = "2026-03-27T19:11:13.595Z" },
223
+ ]
224
+
225
+ [[package]]
226
+ name = "gradio"
227
+ version = "6.12.0"
228
+ source = { registry = "https://pypi.org/simple" }
229
+ dependencies = [
230
+ { name = "anyio" },
231
+ { name = "brotli" },
232
+ { name = "fastapi" },
233
+ { name = "gradio-client" },
234
+ { name = "groovy" },
235
+ { name = "hf-gradio" },
236
+ { name = "httpx" },
237
+ { name = "huggingface-hub" },
238
+ { name = "jinja2" },
239
+ { name = "markupsafe" },
240
+ { name = "numpy" },
241
+ { name = "orjson" },
242
+ { name = "packaging" },
243
+ { name = "pandas" },
244
+ { name = "pillow" },
245
+ { name = "pydantic" },
246
+ { name = "pydub" },
247
+ { name = "python-multipart" },
248
+ { name = "pytz" },
249
+ { name = "pyyaml" },
250
+ { name = "safehttpx" },
251
+ { name = "semantic-version" },
252
+ { name = "starlette" },
253
+ { name = "tomlkit" },
254
+ { name = "typer" },
255
+ { name = "typing-extensions" },
256
+ { name = "uvicorn" },
257
+ ]
258
+ sdist = { url = "https://files.pythonhosted.org/packages/9c/a3/d7b0f752d010a9ccf7ab938d787c912d7ebb43cd3424754e0266a53b4273/gradio-6.12.0.tar.gz", hash = "sha256:5a7808e7ef82cdcb711608275d6779d3368072f93777d649661ffae968af8107", size = 35955906, upload-time = "2026-04-10T20:57:02.335Z" }
259
+ wheels = [
260
+ { url = "https://files.pythonhosted.org/packages/dd/30/d7c61ec8c3f4ad337e20090d771c20d4b61e84aede8d92dc784ab32efa3c/gradio-6.12.0-py3-none-any.whl", hash = "sha256:5587f9ab7b8bfb757e0de9dffebadc63bb354dec5dde22fd7f0266df4b8bd271", size = 19633159, upload-time = "2026-04-10T20:56:59.084Z" },
261
+ ]
262
+
263
+ [[package]]
264
+ name = "gradio-client"
265
+ version = "2.4.1"
266
+ source = { registry = "https://pypi.org/simple" }
267
+ dependencies = [
268
+ { name = "fsspec" },
269
+ { name = "httpx" },
270
+ { name = "huggingface-hub" },
271
+ { name = "packaging" },
272
+ { name = "typing-extensions" },
273
+ ]
274
+ sdist = { url = "https://files.pythonhosted.org/packages/5f/5f/d480fb5e61cf47eaae6c4de2c84a5f130ebcaa2a0a4f36f9f494f53eea35/gradio_client-2.4.1.tar.gz", hash = "sha256:c08783eb3dbdfd6d567bfd912a432fd8ab37b9e633b03e794cc1a0fa105a6d31", size = 58611, upload-time = "2026-04-10T20:57:13.174Z" }
275
+ wheels = [
276
+ { url = "https://files.pythonhosted.org/packages/68/72/a4611ed242a7936d38bd5262c6071fff8dda86cab1c06172a1e372783772/gradio_client-2.4.1-py3-none-any.whl", hash = "sha256:e6cef91ddee06e6a10788c5119977a35aee987968e7a87b0c4716809172c33d4", size = 59563, upload-time = "2026-04-10T20:57:11.774Z" },
277
+ ]
278
+
279
+ [[package]]
280
+ name = "groovy"
281
+ version = "0.1.2"
282
+ source = { registry = "https://pypi.org/simple" }
283
+ sdist = { url = "https://files.pythonhosted.org/packages/52/36/bbdede67400277bef33d3ec0e6a31750da972c469f75966b4930c753218f/groovy-0.1.2.tar.gz", hash = "sha256:25c1dc09b3f9d7e292458aa762c6beb96ea037071bf5e917fc81fb78d2231083", size = 17325, upload-time = "2025-02-28T20:24:56.068Z" }
284
+ wheels = [
285
+ { url = "https://files.pythonhosted.org/packages/28/27/3d6dcadc8a3214d8522c1e7f6a19554e33659be44546d44a2f7572ac7d2a/groovy-0.1.2-py3-none-any.whl", hash = "sha256:7f7975bab18c729a257a8b1ae9dcd70b7cafb1720481beae47719af57c35fa64", size = 14090, upload-time = "2025-02-28T20:24:55.152Z" },
286
+ ]
287
+
288
+ [[package]]
289
+ name = "h11"
290
+ version = "0.16.0"
291
+ source = { registry = "https://pypi.org/simple" }
292
+ sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
293
+ wheels = [
294
+ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
295
+ ]
296
+
297
+ [[package]]
298
+ name = "hf-gradio"
299
+ version = "0.3.2"
300
+ source = { registry = "https://pypi.org/simple" }
301
+ dependencies = [
302
+ { name = "gradio-client" },
303
+ { name = "typer" },
304
+ ]
305
+ wheels = [
306
+ { url = "https://files.pythonhosted.org/packages/37/ce/c13bc8dbbf36d4b0b491fac4238e4b3a48d0e119d59b2b4d2b5d1be00b66/hf_gradio-0.3.2-py3-none-any.whl", hash = "sha256:107d9eca07d1d3eea52f9d8f8691890d964aed3873b439932d90197da38458a7", size = 4381, upload-time = "2026-04-15T20:32:10.412Z" },
307
+ ]
308
+
309
+ [[package]]
310
+ name = "hf-xet"
311
+ version = "1.4.3"
312
+ source = { registry = "https://pypi.org/simple" }
313
+ sdist = { url = "https://files.pythonhosted.org/packages/53/92/ec9ad04d0b5728dca387a45af7bc98fbb0d73b2118759f5f6038b61a57e8/hf_xet-1.4.3.tar.gz", hash = "sha256:8ddedb73c8c08928c793df2f3401ec26f95be7f7e516a7bee2fbb546f6676113", size = 670477, upload-time = "2026-03-31T22:40:07.874Z" }
314
+ wheels = [
315
+ { url = "https://files.pythonhosted.org/packages/ac/9f/9c23e4a447b8f83120798f9279d0297a4d1360bdbf59ef49ebec78fe2545/hf_xet-1.4.3-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:d0da85329eaf196e03e90b84c2d0aca53bd4573d097a75f99609e80775f98025", size = 3805048, upload-time = "2026-03-31T22:39:53.105Z" },
316
+ { url = "https://files.pythonhosted.org/packages/0b/f8/7aacb8e5f4a7899d39c787b5984e912e6c18b11be136ef13947d7a66d265/hf_xet-1.4.3-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:e23717ce4186b265f69afa66e6f0069fe7efbf331546f5c313d00e123dc84583", size = 3562178, upload-time = "2026-03-31T22:39:51.295Z" },
317
+ { url = "https://files.pythonhosted.org/packages/df/9a/a24b26dc8a65f0ecc0fe5be981a19e61e7ca963b85e062c083f3a9100529/hf_xet-1.4.3-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc360b70c815bf340ed56c7b8c63aacf11762a4b099b2fe2c9bd6d6068668c08", size = 4212320, upload-time = "2026-03-31T22:39:42.922Z" },
318
+ { url = "https://files.pythonhosted.org/packages/53/60/46d493db155d2ee2801b71fb1b0fd67696359047fdd8caee2c914cc50c79/hf_xet-1.4.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:39f2d2e9654cd9b4319885733993807aab6de9dfbd34c42f0b78338d6617421f", size = 3991546, upload-time = "2026-03-31T22:39:41.335Z" },
319
+ { url = "https://files.pythonhosted.org/packages/bc/f5/067363e1c96c6b17256910830d1b54099d06287e10f4ec6ec4e7e08371fc/hf_xet-1.4.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:49ad8a8cead2b56051aa84d7fce3e1335efe68df3cf6c058f22a65513885baac", size = 4193200, upload-time = "2026-03-31T22:40:01.936Z" },
320
+ { url = "https://files.pythonhosted.org/packages/42/4b/53951592882d9c23080c7644542fda34a3813104e9e11fa1a7d82d419cb8/hf_xet-1.4.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7716d62015477a70ea272d2d68cd7cad140f61c52ee452e133e139abfe2c17ba", size = 4429392, upload-time = "2026-03-31T22:40:03.492Z" },
321
+ { url = "https://files.pythonhosted.org/packages/8a/21/75a6c175b4e79662ad8e62f46a40ce341d8d6b206b06b4320d07d55b188c/hf_xet-1.4.3-cp37-abi3-win_amd64.whl", hash = "sha256:6b591fcad34e272a5b02607485e4f2a1334aebf1bc6d16ce8eb1eb8978ac2021", size = 3677359, upload-time = "2026-03-31T22:40:13.619Z" },
322
+ { url = "https://files.pythonhosted.org/packages/8a/7c/44314ecd0e89f8b2b51c9d9e5e7a60a9c1c82024ac471d415860557d3cd8/hf_xet-1.4.3-cp37-abi3-win_arm64.whl", hash = "sha256:7c2c7e20bcfcc946dc67187c203463f5e932e395845d098cc2a93f5b67ca0b47", size = 3533664, upload-time = "2026-03-31T22:40:12.152Z" },
323
+ ]
324
+
325
+ [[package]]
326
+ name = "httpcore"
327
+ version = "1.0.9"
328
+ source = { registry = "https://pypi.org/simple" }
329
+ dependencies = [
330
+ { name = "certifi" },
331
+ { name = "h11" },
332
+ ]
333
+ sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
334
+ wheels = [
335
+ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
336
+ ]
337
+
338
+ [[package]]
339
+ name = "httpx"
340
+ version = "0.28.1"
341
+ source = { registry = "https://pypi.org/simple" }
342
+ dependencies = [
343
+ { name = "anyio" },
344
+ { name = "certifi" },
345
+ { name = "httpcore" },
346
+ { name = "idna" },
347
+ ]
348
+ sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
349
+ wheels = [
350
+ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
351
+ ]
352
+
353
+ [[package]]
354
+ name = "huggingface-hub"
355
+ version = "1.10.2"
356
+ source = { registry = "https://pypi.org/simple" }
357
+ dependencies = [
358
+ { name = "filelock" },
359
+ { name = "fsspec" },
360
+ { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" },
361
+ { name = "httpx" },
362
+ { name = "packaging" },
363
+ { name = "pyyaml" },
364
+ { name = "tqdm" },
365
+ { name = "typer" },
366
+ { name = "typing-extensions" },
367
+ ]
368
+ sdist = { url = "https://files.pythonhosted.org/packages/0c/4d/00734890c7fcfe2c7ff04f1c1a167186c42b19e370a2dd8cfd8c34fc92c4/huggingface_hub-1.10.2.tar.gz", hash = "sha256:4b276f820483b709dc86a53bcb8183ea496b8d8447c9f7f88a115a12b498a95f", size = 758428, upload-time = "2026-04-14T10:42:28.498Z" }
369
+ wheels = [
370
+ { url = "https://files.pythonhosted.org/packages/5e/c9/4c1e1216b24bcab140c83acdf8bc89a846ea17cd8a06cd18e3fd308a297f/huggingface_hub-1.10.2-py3-none-any.whl", hash = "sha256:c26c908767cc711493978dc0b4f5747ba7841602997cc98bfd628450a28cf9bc", size = 642581, upload-time = "2026-04-14T10:42:26.563Z" },
371
+ ]
372
+
373
+ [[package]]
374
+ name = "idna"
375
+ version = "3.11"
376
+ source = { registry = "https://pypi.org/simple" }
377
+ sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
378
+ wheels = [
379
+ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
380
+ ]
381
+
382
+ [[package]]
383
+ name = "iniconfig"
384
+ version = "2.3.0"
385
+ source = { registry = "https://pypi.org/simple" }
386
+ sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
387
+ wheels = [
388
+ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
389
+ ]
390
+
391
+ [[package]]
392
+ name = "jinja2"
393
+ version = "3.1.6"
394
+ source = { registry = "https://pypi.org/simple" }
395
+ dependencies = [
396
+ { name = "markupsafe" },
397
+ ]
398
+ sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" }
399
+ wheels = [
400
+ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
401
+ ]
402
+
403
+ [[package]]
404
+ name = "markdown-it-py"
405
+ version = "4.0.0"
406
+ source = { registry = "https://pypi.org/simple" }
407
+ dependencies = [
408
+ { name = "mdurl" },
409
+ ]
410
+ sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" }
411
+ wheels = [
412
+ { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" },
413
+ ]
414
+
415
+ [[package]]
416
+ name = "markupsafe"
417
+ version = "3.0.3"
418
+ source = { registry = "https://pypi.org/simple" }
419
+ sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" }
420
+ wheels = [
421
+ { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" },
422
+ { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" },
423
+ { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" },
424
+ { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" },
425
+ { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" },
426
+ { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" },
427
+ { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" },
428
+ { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" },
429
+ { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" },
430
+ { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" },
431
+ { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" },
432
+ ]
433
+
434
+ [[package]]
435
+ name = "mdurl"
436
+ version = "0.1.2"
437
+ source = { registry = "https://pypi.org/simple" }
438
+ sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
439
+ wheels = [
440
+ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
441
+ ]
442
+
443
+ [[package]]
444
+ name = "mpmath"
445
+ version = "1.3.0"
446
+ source = { registry = "https://pypi.org/simple" }
447
+ sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" }
448
+ wheels = [
449
+ { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" },
450
+ ]
451
+
452
+ [[package]]
453
+ name = "networkx"
454
+ version = "3.6.1"
455
+ source = { registry = "https://pypi.org/simple" }
456
+ sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" }
457
+ wheels = [
458
+ { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" },
459
+ ]
460
+
461
+ [[package]]
462
+ name = "numpy"
463
+ version = "1.26.4"
464
+ source = { registry = "https://pypi.org/simple" }
465
+ sdist = { url = "https://files.pythonhosted.org/packages/65/6e/09db70a523a96d25e115e71cc56a6f9031e7b8cd166c1ac8438307c14058/numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010", size = 15786129, upload-time = "2024-02-06T00:26:44.495Z" }
466
+ wheels = [
467
+ { url = "https://files.pythonhosted.org/packages/95/12/8f2020a8e8b8383ac0177dc9570aad031a3beb12e38847f7129bacd96228/numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218", size = 20335901, upload-time = "2024-02-05T23:55:32.801Z" },
468
+ { url = "https://files.pythonhosted.org/packages/75/5b/ca6c8bd14007e5ca171c7c03102d17b4f4e0ceb53957e8c44343a9546dcc/numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b", size = 13685868, upload-time = "2024-02-05T23:55:56.28Z" },
469
+ { url = "https://files.pythonhosted.org/packages/79/f8/97f10e6755e2a7d027ca783f63044d5b1bc1ae7acb12afe6a9b4286eac17/numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b", size = 13925109, upload-time = "2024-02-05T23:56:20.368Z" },
470
+ { url = "https://files.pythonhosted.org/packages/0f/50/de23fde84e45f5c4fda2488c759b69990fd4512387a8632860f3ac9cd225/numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed", size = 17950613, upload-time = "2024-02-05T23:56:56.054Z" },
471
+ { url = "https://files.pythonhosted.org/packages/4c/0c/9c603826b6465e82591e05ca230dfc13376da512b25ccd0894709b054ed0/numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a", size = 13572172, upload-time = "2024-02-05T23:57:21.56Z" },
472
+ { url = "https://files.pythonhosted.org/packages/76/8c/2ba3902e1a0fc1c74962ea9bb33a534bb05984ad7ff9515bf8d07527cadd/numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0", size = 17786643, upload-time = "2024-02-05T23:57:56.585Z" },
473
+ { url = "https://files.pythonhosted.org/packages/28/4a/46d9e65106879492374999e76eb85f87b15328e06bd1550668f79f7b18c6/numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110", size = 5677803, upload-time = "2024-02-05T23:58:08.963Z" },
474
+ { url = "https://files.pythonhosted.org/packages/16/2e/86f24451c2d530c88daf997cb8d6ac622c1d40d19f5a031ed68a4b73a374/numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818", size = 15517754, upload-time = "2024-02-05T23:58:36.364Z" },
475
+ ]
476
+
477
+ [[package]]
478
+ name = "nvidia-cublas-cu12"
479
+ version = "12.1.3.1"
480
+ source = { registry = "https://pypi.org/simple" }
481
+ wheels = [
482
+ { url = "https://files.pythonhosted.org/packages/37/6d/121efd7382d5b0284239f4ab1fc1590d86d34ed4a4a2fdb13b30ca8e5740/nvidia_cublas_cu12-12.1.3.1-py3-none-manylinux1_x86_64.whl", hash = "sha256:ee53ccca76a6fc08fb9701aa95b6ceb242cdaab118c3bb152af4e579af792728", size = 410594774, upload-time = "2023-04-19T15:50:03.519Z" },
483
+ ]
484
+
485
+ [[package]]
486
+ name = "nvidia-cuda-cupti-cu12"
487
+ version = "12.1.105"
488
+ source = { registry = "https://pypi.org/simple" }
489
+ wheels = [
490
+ { url = "https://files.pythonhosted.org/packages/7e/00/6b218edd739ecfc60524e585ba8e6b00554dd908de2c9c66c1af3e44e18d/nvidia_cuda_cupti_cu12-12.1.105-py3-none-manylinux1_x86_64.whl", hash = "sha256:e54fde3983165c624cb79254ae9818a456eb6e87a7fd4d56a2352c24ee542d7e", size = 14109015, upload-time = "2023-04-19T15:47:32.502Z" },
491
+ ]
492
+
493
+ [[package]]
494
+ name = "nvidia-cuda-nvrtc-cu12"
495
+ version = "12.1.105"
496
+ source = { registry = "https://pypi.org/simple" }
497
+ wheels = [
498
+ { url = "https://files.pythonhosted.org/packages/b6/9f/c64c03f49d6fbc56196664d05dba14e3a561038a81a638eeb47f4d4cfd48/nvidia_cuda_nvrtc_cu12-12.1.105-py3-none-manylinux1_x86_64.whl", hash = "sha256:339b385f50c309763ca65456ec75e17bbefcbbf2893f462cb8b90584cd27a1c2", size = 23671734, upload-time = "2023-04-19T15:48:32.42Z" },
499
+ ]
500
+
501
+ [[package]]
502
+ name = "nvidia-cuda-runtime-cu12"
503
+ version = "12.1.105"
504
+ source = { registry = "https://pypi.org/simple" }
505
+ wheels = [
506
+ { url = "https://files.pythonhosted.org/packages/eb/d5/c68b1d2cdfcc59e72e8a5949a37ddb22ae6cade80cd4a57a84d4c8b55472/nvidia_cuda_runtime_cu12-12.1.105-py3-none-manylinux1_x86_64.whl", hash = "sha256:6e258468ddf5796e25f1dc591a31029fa317d97a0a94ed93468fc86301d61e40", size = 823596, upload-time = "2023-04-19T15:47:22.471Z" },
507
+ ]
508
+
509
+ [[package]]
510
+ name = "nvidia-cudnn-cu12"
511
+ version = "8.9.2.26"
512
+ source = { registry = "https://pypi.org/simple" }
513
+ dependencies = [
514
+ { name = "nvidia-cublas-cu12", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" },
515
+ ]
516
+ wheels = [
517
+ { url = "https://files.pythonhosted.org/packages/ff/74/a2e2be7fb83aaedec84f391f082cf765dfb635e7caa9b49065f73e4835d8/nvidia_cudnn_cu12-8.9.2.26-py3-none-manylinux1_x86_64.whl", hash = "sha256:5ccb288774fdfb07a7e7025ffec286971c06d8d7b4fb162525334616d7629ff9", size = 731725872, upload-time = "2023-06-01T19:24:57.328Z" },
518
+ ]
519
+
520
+ [[package]]
521
+ name = "nvidia-cufft-cu12"
522
+ version = "11.0.2.54"
523
+ source = { registry = "https://pypi.org/simple" }
524
+ wheels = [
525
+ { url = "https://files.pythonhosted.org/packages/86/94/eb540db023ce1d162e7bea9f8f5aa781d57c65aed513c33ee9a5123ead4d/nvidia_cufft_cu12-11.0.2.54-py3-none-manylinux1_x86_64.whl", hash = "sha256:794e3948a1aa71fd817c3775866943936774d1c14e7628c74f6f7417224cdf56", size = 121635161, upload-time = "2023-04-19T15:50:46Z" },
526
+ ]
527
+
528
+ [[package]]
529
+ name = "nvidia-curand-cu12"
530
+ version = "10.3.2.106"
531
+ source = { registry = "https://pypi.org/simple" }
532
+ wheels = [
533
+ { url = "https://files.pythonhosted.org/packages/44/31/4890b1c9abc496303412947fc7dcea3d14861720642b49e8ceed89636705/nvidia_curand_cu12-10.3.2.106-py3-none-manylinux1_x86_64.whl", hash = "sha256:9d264c5036dde4e64f1de8c50ae753237c12e0b1348738169cd0f8a536c0e1e0", size = 56467784, upload-time = "2023-04-19T15:51:04.804Z" },
534
+ ]
535
+
536
+ [[package]]
537
+ name = "nvidia-cusolver-cu12"
538
+ version = "11.4.5.107"
539
+ source = { registry = "https://pypi.org/simple" }
540
+ dependencies = [
541
+ { name = "nvidia-cublas-cu12", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" },
542
+ { name = "nvidia-cusparse-cu12", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" },
543
+ { name = "nvidia-nvjitlink-cu12", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" },
544
+ ]
545
+ wheels = [
546
+ { url = "https://files.pythonhosted.org/packages/bc/1d/8de1e5c67099015c834315e333911273a8c6aaba78923dd1d1e25fc5f217/nvidia_cusolver_cu12-11.4.5.107-py3-none-manylinux1_x86_64.whl", hash = "sha256:8a7ec542f0412294b15072fa7dab71d31334014a69f953004ea7a118206fe0dd", size = 124161928, upload-time = "2023-04-19T15:51:25.781Z" },
547
+ ]
548
+
549
+ [[package]]
550
+ name = "nvidia-cusparse-cu12"
551
+ version = "12.1.0.106"
552
+ source = { registry = "https://pypi.org/simple" }
553
+ dependencies = [
554
+ { name = "nvidia-nvjitlink-cu12", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" },
555
+ ]
556
+ wheels = [
557
+ { url = "https://files.pythonhosted.org/packages/65/5b/cfaeebf25cd9fdec14338ccb16f6b2c4c7fa9163aefcf057d86b9cc248bb/nvidia_cusparse_cu12-12.1.0.106-py3-none-manylinux1_x86_64.whl", hash = "sha256:f3b50f42cf363f86ab21f720998517a659a48131e8d538dc02f8768237bd884c", size = 195958278, upload-time = "2023-04-19T15:51:49.939Z" },
558
+ ]
559
+
560
+ [[package]]
561
+ name = "nvidia-nccl-cu12"
562
+ version = "2.19.3"
563
+ source = { registry = "https://pypi.org/simple" }
564
+ wheels = [
565
+ { url = "https://files.pythonhosted.org/packages/38/00/d0d4e48aef772ad5aebcf70b73028f88db6e5640b36c38e90445b7a57c45/nvidia_nccl_cu12-2.19.3-py3-none-manylinux1_x86_64.whl", hash = "sha256:a9734707a2c96443331c1e48c717024aa6678a0e2a4cb66b2c364d18cee6b48d", size = 165987969, upload-time = "2023-10-24T16:16:24.789Z" },
566
+ ]
567
+
568
+ [[package]]
569
+ name = "nvidia-nvjitlink-cu12"
570
+ version = "12.9.86"
571
+ source = { registry = "https://pypi.org/simple" }
572
+ wheels = [
573
+ { url = "https://files.pythonhosted.org/packages/46/0c/c75bbfb967457a0b7670b8ad267bfc4fffdf341c074e0a80db06c24ccfd4/nvidia_nvjitlink_cu12-12.9.86-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:e3f1171dbdc83c5932a45f0f4c99180a70de9bd2718c1ab77d14104f6d7147f9", size = 39748338, upload-time = "2025-06-05T20:10:25.613Z" },
574
+ ]
575
+
576
+ [[package]]
577
+ name = "nvidia-nvtx-cu12"
578
+ version = "12.1.105"
579
+ source = { registry = "https://pypi.org/simple" }
580
+ wheels = [
581
+ { url = "https://files.pythonhosted.org/packages/da/d3/8057f0587683ed2fcd4dbfbdfdfa807b9160b809976099d36b8f60d08f03/nvidia_nvtx_cu12-12.1.105-py3-none-manylinux1_x86_64.whl", hash = "sha256:dc21cf308ca5691e7c04d962e213f8a4aa9bbfa23d95412f452254c2caeb09e5", size = 99138, upload-time = "2023-04-19T15:48:43.556Z" },
582
+ ]
583
+
584
+ [[package]]
585
+ name = "orjson"
586
+ version = "3.11.8"
587
+ source = { registry = "https://pypi.org/simple" }
588
+ sdist = { url = "https://files.pythonhosted.org/packages/9d/1b/2024d06792d0779f9dbc51531b61c24f76c75b9f4ce05e6f3377a1814cea/orjson-3.11.8.tar.gz", hash = "sha256:96163d9cdc5a202703e9ad1b9ae757d5f0ca62f4fa0cc93d1f27b0e180cc404e", size = 5603832, upload-time = "2026-03-31T16:16:27.878Z" }
589
+ wheels = [
590
+ { url = "https://files.pythonhosted.org/packages/01/f6/8d58b32ab32d9215973a1688aebd098252ee8af1766c0e4e36e7831f0295/orjson-3.11.8-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:1cd0b77e77c95758f8e1100139844e99f3ccc87e71e6fc8e1c027e55807c549f", size = 229233, upload-time = "2026-03-31T16:15:12.762Z" },
591
+ { url = "https://files.pythonhosted.org/packages/a9/8b/2ffe35e71f6b92622e8ea4607bf33ecf7dfb51b3619dcfabfd36cbe2d0a5/orjson-3.11.8-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:6a3d159d5ffa0e3961f353c4b036540996bf8b9697ccc38261c0eac1fd3347a6", size = 128772, upload-time = "2026-03-31T16:15:14.237Z" },
592
+ { url = "https://files.pythonhosted.org/packages/27/d2/1f8682ae50d5c6897a563cb96bc106da8c9cb5b7b6e81a52e4cc086679b9/orjson-3.11.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76070a76e9c5ae661e2d9848f216980d8d533e0f8143e6ed462807b242e3c5e8", size = 131946, upload-time = "2026-03-31T16:15:15.607Z" },
593
+ { url = "https://files.pythonhosted.org/packages/52/4b/5500f76f0eece84226e0689cb48dcde081104c2fa6e2483d17ca13685ffb/orjson-3.11.8-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:54153d21520a71a4c82a0dbb4523e468941d549d221dc173de0f019678cf3813", size = 130368, upload-time = "2026-03-31T16:15:17.066Z" },
594
+ { url = "https://files.pythonhosted.org/packages/da/4e/58b927e08fbe9840e6c920d9e299b051ea667463b1f39a56e668669f8508/orjson-3.11.8-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:469ac2125611b7c5741a0b3798cd9e5786cbad6345f9f400c77212be89563bec", size = 135540, upload-time = "2026-03-31T16:15:18.404Z" },
595
+ { url = "https://files.pythonhosted.org/packages/56/7c/ba7cb871cba1bcd5cd02ee34f98d894c6cea96353ad87466e5aef2429c60/orjson-3.11.8-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:14778ffd0f6896aa613951a7fbf4690229aa7a543cb2bfbe9f358e08aafa9546", size = 146877, upload-time = "2026-03-31T16:15:19.833Z" },
596
+ { url = "https://files.pythonhosted.org/packages/0b/5d/eb9c25fc1386696c6a342cd361c306452c75e0b55e86ad602dd4827a7fd7/orjson-3.11.8-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea56a955056a6d6c550cf18b3348656a9d9a4f02e2d0c02cabf3c73f1055d506", size = 132837, upload-time = "2026-03-31T16:15:21.282Z" },
597
+ { url = "https://files.pythonhosted.org/packages/37/87/5ddeb7fc1fbd9004aeccab08426f34c81a5b4c25c7061281862b015fce2b/orjson-3.11.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53a0f57e59a530d18a142f4d4ba6dfc708dc5fdedce45e98ff06b44930a2a48f", size = 133624, upload-time = "2026-03-31T16:15:22.641Z" },
598
+ { url = "https://files.pythonhosted.org/packages/22/09/90048793db94ee4b2fcec4ac8e5ddb077367637d6650be896b3494b79bb7/orjson-3.11.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9b48e274f8824567d74e2158199e269597edf00823a1b12b63d48462bbf5123e", size = 141904, upload-time = "2026-03-31T16:15:24.435Z" },
599
+ { url = "https://files.pythonhosted.org/packages/c0/cf/eb284847487821a5d415e54149a6449ba9bfc5872ce63ab7be41b8ec401c/orjson-3.11.8-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:3f262401086a3960586af06c054609365e98407151f5ea24a62893a40d80dbbb", size = 423742, upload-time = "2026-03-31T16:15:26.155Z" },
600
+ { url = "https://files.pythonhosted.org/packages/44/09/e12423d327071c851c13e76936f144a96adacfc037394dec35ac3fc8d1e8/orjson-3.11.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8e8c6218b614badf8e229b697865df4301afa74b791b6c9ade01d19a9953a942", size = 147806, upload-time = "2026-03-31T16:15:27.909Z" },
601
+ { url = "https://files.pythonhosted.org/packages/b3/6d/37c2589ba864e582ffe7611643314785c6afb1f83c701654ef05daa8fcc7/orjson-3.11.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:093d489fa039ddade2db541097dbb484999fcc65fc2b0ff9819141e2ab364f25", size = 136485, upload-time = "2026-03-31T16:15:29.749Z" },
602
+ { url = "https://files.pythonhosted.org/packages/be/c9/135194a02ab76b04ed9a10f68624b7ebd238bbe55548878b11ff15a0f352/orjson-3.11.8-cp312-cp312-win32.whl", hash = "sha256:e0950ed1bcb9893f4293fd5c5a7ee10934fbf82c4101c70be360db23ce24b7d2", size = 131966, upload-time = "2026-03-31T16:15:31.687Z" },
603
+ { url = "https://files.pythonhosted.org/packages/ed/9a/9796f8fbe3cf30ce9cb696748dbb535e5c87be4bf4fe2e9ca498ef1fa8cf/orjson-3.11.8-cp312-cp312-win_amd64.whl", hash = "sha256:3cf17c141617b88ced4536b2135c552490f07799f6ad565948ea07bef0dcb9a6", size = 127441, upload-time = "2026-03-31T16:15:33.333Z" },
604
+ { url = "https://files.pythonhosted.org/packages/cc/47/5aaf54524a7a4a0dd09dd778f3fa65dd2108290615b652e23d944152bc8e/orjson-3.11.8-cp312-cp312-win_arm64.whl", hash = "sha256:48854463b0572cc87dac7d981aa72ed8bf6deedc0511853dc76b8bbd5482d36d", size = 127364, upload-time = "2026-03-31T16:15:34.748Z" },
605
+ ]
606
+
607
+ [[package]]
608
+ name = "packaging"
609
+ version = "26.1"
610
+ source = { registry = "https://pypi.org/simple" }
611
+ sdist = { url = "https://files.pythonhosted.org/packages/df/de/0d2b39fb4af88a0258f3bac87dfcbb48e73fbdea4a2ed0e2213f9a4c2f9a/packaging-26.1.tar.gz", hash = "sha256:f042152b681c4bfac5cae2742a55e103d27ab2ec0f3d88037136b6bfe7c9c5de", size = 215519, upload-time = "2026-04-14T21:12:49.362Z" }
612
+ wheels = [
613
+ { url = "https://files.pythonhosted.org/packages/7a/c2/920ef838e2f0028c8262f16101ec09ebd5969864e5a64c4c05fad0617c56/packaging-26.1-py3-none-any.whl", hash = "sha256:5d9c0669c6285e491e0ced2eee587eaf67b670d94a19e94e3984a481aba6802f", size = 95831, upload-time = "2026-04-14T21:12:47.56Z" },
614
+ ]
615
+
616
+ [[package]]
617
+ name = "pandas"
618
+ version = "3.0.2"
619
+ source = { registry = "https://pypi.org/simple" }
620
+ dependencies = [
621
+ { name = "numpy" },
622
+ { name = "python-dateutil" },
623
+ { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" },
624
+ ]
625
+ sdist = { url = "https://files.pythonhosted.org/packages/da/99/b342345300f13440fe9fe385c3c481e2d9a595ee3bab4d3219247ac94e9a/pandas-3.0.2.tar.gz", hash = "sha256:f4753e73e34c8d83221ba58f232433fca2748be8b18dbca02d242ed153945043", size = 4645855, upload-time = "2026-03-31T06:48:30.816Z" }
626
+ wheels = [
627
+ { url = "https://files.pythonhosted.org/packages/f3/b0/c20bd4d6d3f736e6bd6b55794e9cd0a617b858eaad27c8f410ea05d953b7/pandas-3.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:232a70ebb568c0c4d2db4584f338c1577d81e3af63292208d615907b698a0f18", size = 10347921, upload-time = "2026-03-31T06:46:33.36Z" },
628
+ { url = "https://files.pythonhosted.org/packages/35/d0/4831af68ce30cc2d03c697bea8450e3225a835ef497d0d70f31b8cdde965/pandas-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:970762605cff1ca0d3f71ed4f3a769ea8f85fc8e6348f6e110b8fea7e6eb5a14", size = 9888127, upload-time = "2026-03-31T06:46:36.253Z" },
629
+ { url = "https://files.pythonhosted.org/packages/61/a9/16ea9346e1fc4a96e2896242d9bc674764fb9049b0044c0132502f7a771e/pandas-3.0.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aff4e6f4d722e0652707d7bcb190c445fe58428500c6d16005b02401764b1b3d", size = 10399577, upload-time = "2026-03-31T06:46:39.224Z" },
630
+ { url = "https://files.pythonhosted.org/packages/c4/a8/3a61a721472959ab0ce865ef05d10b0d6bfe27ce8801c99f33d4fa996e65/pandas-3.0.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef8b27695c3d3dc78403c9a7d5e59a62d5464a7e1123b4e0042763f7104dc74f", size = 10880030, upload-time = "2026-03-31T06:46:42.412Z" },
631
+ { url = "https://files.pythonhosted.org/packages/da/65/7225c0ea4d6ce9cb2160a7fb7f39804871049f016e74782e5dade4d14109/pandas-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f8d68083e49e16b84734eb1a4dcae4259a75c90fb6e2251ab9a00b61120c06ab", size = 11409468, upload-time = "2026-03-31T06:46:45.2Z" },
632
+ { url = "https://files.pythonhosted.org/packages/fa/5b/46e7c76032639f2132359b5cf4c785dd8cf9aea5ea64699eac752f02b9db/pandas-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:32cc41f310ebd4a296d93515fcac312216adfedb1894e879303987b8f1e2b97d", size = 11936381, upload-time = "2026-03-31T06:46:48.293Z" },
633
+ { url = "https://files.pythonhosted.org/packages/7b/8b/721a9cff6fa6a91b162eb51019c6243b82b3226c71bb6c8ef4a9bd65cbc6/pandas-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:a4785e1d6547d8427c5208b748ae2efb64659a21bd82bf440d4262d02bfa02a4", size = 9744993, upload-time = "2026-03-31T06:46:51.488Z" },
634
+ { url = "https://files.pythonhosted.org/packages/d5/18/7f0bd34ae27b28159aa80f2a6799f47fda34f7fb938a76e20c7b7fe3b200/pandas-3.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:08504503f7101300107ecdc8df73658e4347586db5cfdadabc1592e9d7e7a0fd", size = 9056118, upload-time = "2026-03-31T06:46:54.548Z" },
635
+ ]
636
+
637
+ [[package]]
638
+ name = "pillow"
639
+ version = "10.2.0"
640
+ source = { registry = "https://pypi.org/simple" }
641
+ sdist = { url = "https://files.pythonhosted.org/packages/f8/3e/32cbd0129a28686621434cbf17bb64bf1458bfb838f1f668262fefce145c/pillow-10.2.0.tar.gz", hash = "sha256:e87f0b2c78157e12d7686b27d63c070fd65d994e8ddae6f328e0dcf4a0cd007e", size = 46212712, upload-time = "2024-01-02T09:16:59.702Z" }
642
+ wheels = [
643
+ { url = "https://files.pythonhosted.org/packages/37/d5/2c00228ace73a7855a52053a92fdd6cea9b22393fbf3961125c11829dcd2/pillow-10.2.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:1b5e1b74d1bd1b78bc3477528919414874748dd363e6272efd5abf7654e68bef", size = 3517780, upload-time = "2024-01-02T09:15:41.495Z" },
644
+ { url = "https://files.pythonhosted.org/packages/9d/a0/28756da34d6b58c3c5f6c1d5589e4e8f4e73472b55875524ae9d6e7e98fe/pillow-10.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0eae2073305f451d8ecacb5474997c08569fb4eb4ac231ffa4ad7d342fdc25ac", size = 3317920, upload-time = "2024-01-02T09:15:44.116Z" },
645
+ { url = "https://files.pythonhosted.org/packages/ab/72/e6a8887c0ce6c94cd0b74fef495a81f4ea4c742242de4bc1943abbd21f92/pillow-10.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7c2286c23cd350b80d2fc9d424fc797575fb16f854b831d16fd47ceec078f2c", size = 4308358, upload-time = "2024-01-02T09:33:09.603Z" },
646
+ { url = "https://files.pythonhosted.org/packages/a8/2f/86cf1dc4b0530e4c3e96edd0338dcc4809c2622d9d45460029a71a831473/pillow-10.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e23412b5c41e58cec602f1135c57dfcf15482013ce6e5f093a86db69646a5aa", size = 4422007, upload-time = "2024-01-02T09:15:46.355Z" },
647
+ { url = "https://files.pythonhosted.org/packages/00/43/1ca3313b56ef623de0afebfe3d7a6e9c07e1a76c50ce191302018907b2b5/pillow-10.2.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:52a50aa3fb3acb9cf7213573ef55d31d6eca37f5709c69e6858fe3bc04a5c2a2", size = 4333841, upload-time = "2024-01-02T09:33:14.842Z" },
648
+ { url = "https://files.pythonhosted.org/packages/5c/c6/5b6b1f7362267494a423b45af684d604491565e81436e3ebeefee68f78fd/pillow-10.2.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:127cee571038f252a552760076407f9cff79761c3d436a12af6000cd182a9d04", size = 4502101, upload-time = "2024-01-02T09:15:48.416Z" },
649
+ { url = "https://files.pythonhosted.org/packages/e6/c5/37e72d74c248adf133a2dd56890cf8632e2e46562e5fa70414445bbd3ae6/pillow-10.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:8d12251f02d69d8310b046e82572ed486685c38f02176bd08baf216746eb947f", size = 4542122, upload-time = "2024-01-02T09:33:19.012Z" },
650
+ { url = "https://files.pythonhosted.org/packages/fa/93/79979b8ab99da2958bf6fef1be745c344c4e727f07d1429c49c015e21db2/pillow-10.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:54f1852cd531aa981bc0965b7d609f5f6cc8ce8c41b1139f6ed6b3c54ab82bfb", size = 4611042, upload-time = "2024-01-02T09:15:50.616Z" },
651
+ { url = "https://files.pythonhosted.org/packages/ce/a7/11a539c1e12dfb9d67c35e5d3d99c7a6853face9083e6483360f4d9cd1d8/pillow-10.2.0-cp312-cp312-win32.whl", hash = "sha256:257d8788df5ca62c980314053197f4d46eefedf4e6175bc9412f14412ec4ea2f", size = 2290438, upload-time = "2024-01-02T09:15:53.219Z" },
652
+ { url = "https://files.pythonhosted.org/packages/51/07/7e9266a59bb267b56c1f432f6416653b9a78dda771c57740d064a8aa2a44/pillow-10.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:154e939c5f0053a383de4fd3d3da48d9427a7e985f58af8e94d0b3c9fcfcf4f9", size = 2621845, upload-time = "2024-01-02T09:15:55.293Z" },
653
+ { url = "https://files.pythonhosted.org/packages/a0/61/6cff8a8dbbac3d7fb7adb435b60737a7d0b0849f53e3af38f2c94d988da6/pillow-10.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:f379abd2f1e3dddb2b61bc67977a6b5a0a3f7485538bcc6f39ec76163891ee48", size = 2229322, upload-time = "2024-01-02T09:15:57.475Z" },
654
+ ]
655
+
656
+ [[package]]
657
+ name = "pluggy"
658
+ version = "1.6.0"
659
+ source = { registry = "https://pypi.org/simple" }
660
+ sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
661
+ wheels = [
662
+ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
663
+ ]
664
+
665
+ [[package]]
666
+ name = "pydantic"
667
+ version = "2.13.1"
668
+ source = { registry = "https://pypi.org/simple" }
669
+ dependencies = [
670
+ { name = "annotated-types" },
671
+ { name = "pydantic-core" },
672
+ { name = "typing-extensions" },
673
+ { name = "typing-inspection" },
674
+ ]
675
+ sdist = { url = "https://files.pythonhosted.org/packages/f3/6b/1353beb3d1cd5cf61cdec5b6f87a9872399de3bc5cae0b7ce07ff4de2ab0/pydantic-2.13.1.tar.gz", hash = "sha256:a0f829b279ddd1e39291133fe2539d2aa46cc6b150c1706a270ff0879e3774d2", size = 843746, upload-time = "2026-04-15T14:57:19.398Z" }
676
+ wheels = [
677
+ { url = "https://files.pythonhosted.org/packages/81/5a/2225f4c176dbfed0d809e848b50ef08f70e61daa667b7fa14b0d311ae44d/pydantic-2.13.1-py3-none-any.whl", hash = "sha256:9557ecc2806faaf6037f85b1fbd963d01e30511c48085f0d573650fdeaad378a", size = 471917, upload-time = "2026-04-15T14:57:17.277Z" },
678
+ ]
679
+
680
+ [[package]]
681
+ name = "pydantic-core"
682
+ version = "2.46.1"
683
+ source = { registry = "https://pypi.org/simple" }
684
+ dependencies = [
685
+ { name = "typing-extensions" },
686
+ ]
687
+ sdist = { url = "https://files.pythonhosted.org/packages/a1/93/f97a86a7eb28faa1d038af2fd5d6166418b4433659108a4c311b57128b2d/pydantic_core-2.46.1.tar.gz", hash = "sha256:d408153772d9f298098fb5d620f045bdf0f017af0d5cb6e309ef8c205540caa4", size = 471230, upload-time = "2026-04-15T14:49:34.52Z" }
688
+ wheels = [
689
+ { url = "https://files.pythonhosted.org/packages/ce/fb/caaa8ee23861c170f07dbd58fc2be3a2c02a32637693cbb23eef02e84808/pydantic_core-2.46.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ae8c8c5eb4c796944f3166f2f0dab6c761c2c2cc5bd20e5f692128be8600b9a4", size = 2119472, upload-time = "2026-04-15T14:49:45.946Z" },
690
+ { url = "https://files.pythonhosted.org/packages/fa/61/bcffaa52894489ff89e5e1cdde67429914bf083c0db7296bef153020f786/pydantic_core-2.46.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:daba6f5f5b986aa0682623a1a4f8d1ecb0ec00ce09cfa9ca71a3b742bc383e3a", size = 1951230, upload-time = "2026-04-15T14:52:27.646Z" },
691
+ { url = "https://files.pythonhosted.org/packages/f8/95/80d2f43a2a1a1e3220fd329d614aa5a39e0a75d24353a3aaf226e605f1c2/pydantic_core-2.46.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0265f3a2460539ecc97817a80c7a23c458dd84191229b655522a2674f701f14e", size = 1976394, upload-time = "2026-04-15T14:50:32.742Z" },
692
+ { url = "https://files.pythonhosted.org/packages/8d/31/2c5b1a207926b5fc1961a2d11da940129bc3841c36cc4df03014195b2966/pydantic_core-2.46.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb16c0156c4b4e94aa3719138cc43c53d30ff21126b6a3af63786dcc0757b56e", size = 2068455, upload-time = "2026-04-15T14:50:01.286Z" },
693
+ { url = "https://files.pythonhosted.org/packages/7d/36/c6aa07274359a51ac62895895325ce90107e811c6cea39d2617a99ef10d7/pydantic_core-2.46.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b42d80fad8e4b283e1e4138f1142f0d038c46d137aad2f9824ad9086080dd41", size = 2239049, upload-time = "2026-04-15T14:53:02.216Z" },
694
+ { url = "https://files.pythonhosted.org/packages/0a/3f/77cdd0db8bddc714842dfd93f737c863751cf02001c993341504f6b0cd53/pydantic_core-2.46.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9cced85896d5b795293bc36b7e2fb0347a36c828551b50cbba510510d928548c", size = 2318681, upload-time = "2026-04-15T14:50:04.539Z" },
695
+ { url = "https://files.pythonhosted.org/packages/a1/a3/09d929a40e6727274b0b500ad06e1b3f35d4f4665ae1c8ba65acbb17e9b5/pydantic_core-2.46.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a641cb1e74b44c418adaf9f5f450670dbec53511f030d8cde8d8accb66edc363", size = 2096527, upload-time = "2026-04-15T14:53:14.766Z" },
696
+ { url = "https://files.pythonhosted.org/packages/89/ae/544c3a82456ebc254a9fcbe2715bab76c70acf9d291aaea24391147943e4/pydantic_core-2.46.1-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:191e7a122ab14eb12415fe3f92610fc06c7f1d2b4b9101d24d490d447ac92506", size = 2170407, upload-time = "2026-04-15T14:51:27.138Z" },
697
+ { url = "https://files.pythonhosted.org/packages/9d/ce/0dfd881c7af4c522f47b325707bd9a2cdcf4f40e4f2fd30df0e9a3e8d393/pydantic_core-2.46.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4fe4ff660f7938b5d92f21529ce331b011aa35e481ab64b7cd03f52384e544bb", size = 2188578, upload-time = "2026-04-15T14:50:39.655Z" },
698
+ { url = "https://files.pythonhosted.org/packages/a1/e9/980ea2a6d5114dd1a62ecc5f56feb3d34555f33bd11043f042e5f7f0724a/pydantic_core-2.46.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:18fcea085b3adc3868d8d19606da52d7a52d8bccd8e28652b0778dbe5e6a6660", size = 2188959, upload-time = "2026-04-15T14:52:42.243Z" },
699
+ { url = "https://files.pythonhosted.org/packages/e7/f1/595e0f50f4bfc56cde2fe558f2b0978f29f2865da894c6226231e17464a5/pydantic_core-2.46.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:e8e589e7c9466e022d79e13c5764c2239b2e5a7993ba727822b021234f89b56b", size = 2339973, upload-time = "2026-04-15T14:52:10.642Z" },
700
+ { url = "https://files.pythonhosted.org/packages/49/44/be9f979a6ab6b8c36865ccd92c3a38a760c66055e1f384665f35525134c4/pydantic_core-2.46.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f78eb3d4027963bdc9baccd177f02a98bf8714bc51fe17153d8b51218918b5bc", size = 2385228, upload-time = "2026-04-15T14:51:00.77Z" },
701
+ { url = "https://files.pythonhosted.org/packages/5b/d4/c826cd711787d240219f01d0d3ca116cb55516b8b95277820aa9c85e1882/pydantic_core-2.46.1-cp312-cp312-win32.whl", hash = "sha256:54fe30c20cab03844dc63bdc6ddca67f74a2eb8482df69c1e5f68396856241be", size = 1978828, upload-time = "2026-04-15T14:50:29.362Z" },
702
+ { url = "https://files.pythonhosted.org/packages/22/05/8a1fcf8181be4c7a9cfc34e5fbf2d9c3866edc9dfd3c48d5401806e0a523/pydantic_core-2.46.1-cp312-cp312-win_amd64.whl", hash = "sha256:aea4e22ed4c53f2774221435e39969a54d2e783f4aee902cdd6c8011415de893", size = 2070015, upload-time = "2026-04-15T14:49:47.301Z" },
703
+ { url = "https://files.pythonhosted.org/packages/61/d5/fea36ad2882b99c174ef4ffbc7ea6523f6abe26060fbc1f77d6441670232/pydantic_core-2.46.1-cp312-cp312-win_arm64.whl", hash = "sha256:f76fb49c34b4d66aa6e552ce9e852ea97a3a06301a9f01ae82f23e449e3a55f8", size = 2030176, upload-time = "2026-04-15T14:50:47.307Z" },
704
+ { url = "https://files.pythonhosted.org/packages/f4/97/95de673a1356a88b2efdaa120eb6af357a81555c35f6809a7a1423ff7aef/pydantic_core-2.46.1-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:5f9107a24a4bc00293434dfa95cf8968751ad0dd703b26ea83a75a56f7326041", size = 2107564, upload-time = "2026-04-15T14:50:49.14Z" },
705
+ { url = "https://files.pythonhosted.org/packages/00/fc/a7c16d85211ea9accddc693b7d049f20b0c06440d9264d1e1c074394ee6c/pydantic_core-2.46.1-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:2b1801ba99876984d0a03362782819238141c4d0f3f67f69093663691332fc35", size = 1939925, upload-time = "2026-04-15T14:50:36.188Z" },
706
+ { url = "https://files.pythonhosted.org/packages/2e/23/87841169d77820ddabeb81d82002c95dcb82163846666d74f5bdeeaec750/pydantic_core-2.46.1-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7fd82a91a20ed6d54fa8c91e7a98255b1ff45bf09b051bfe7fe04eb411e232e", size = 1995313, upload-time = "2026-04-15T14:50:22.538Z" },
707
+ { url = "https://files.pythonhosted.org/packages/ea/96/b46609359a354fa9cd336fc5d93334f1c358b756cc81e4b397347a88fa6f/pydantic_core-2.46.1-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f135bf07c92c93def97008bc4496d16934da9efefd7204e5f22a2c92523cb1f", size = 2151197, upload-time = "2026-04-15T14:51:22.925Z" },
708
+ ]
709
+
710
+ [[package]]
711
+ name = "pydub"
712
+ version = "0.25.1"
713
+ source = { registry = "https://pypi.org/simple" }
714
+ sdist = { url = "https://files.pythonhosted.org/packages/fe/9a/e6bca0eed82db26562c73b5076539a4a08d3cffd19c3cc5913a3e61145fd/pydub-0.25.1.tar.gz", hash = "sha256:980a33ce9949cab2a569606b65674d748ecbca4f0796887fd6f46173a7b0d30f", size = 38326, upload-time = "2021-03-10T02:09:54.659Z" }
715
+ wheels = [
716
+ { url = "https://files.pythonhosted.org/packages/a6/53/d78dc063216e62fc55f6b2eebb447f6a4b0a59f55c8406376f76bf959b08/pydub-0.25.1-py2.py3-none-any.whl", hash = "sha256:65617e33033874b59d87db603aa1ed450633288aefead953b30bded59cb599a6", size = 32327, upload-time = "2021-03-10T02:09:53.503Z" },
717
+ ]
718
+
719
+ [[package]]
720
+ name = "pygments"
721
+ version = "2.20.0"
722
+ source = { registry = "https://pypi.org/simple" }
723
+ sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
724
+ wheels = [
725
+ { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
726
+ ]
727
+
728
+ [[package]]
729
+ name = "pytest"
730
+ version = "9.0.3"
731
+ source = { registry = "https://pypi.org/simple" }
732
+ dependencies = [
733
+ { name = "colorama", marker = "sys_platform == 'win32'" },
734
+ { name = "iniconfig" },
735
+ { name = "packaging" },
736
+ { name = "pluggy" },
737
+ { name = "pygments" },
738
+ ]
739
+ sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" }
740
+ wheels = [
741
+ { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" },
742
+ ]
743
+
744
+ [[package]]
745
+ name = "pytest-cov"
746
+ version = "7.1.0"
747
+ source = { registry = "https://pypi.org/simple" }
748
+ dependencies = [
749
+ { name = "coverage" },
750
+ { name = "pluggy" },
751
+ { name = "pytest" },
752
+ ]
753
+ sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" }
754
+ wheels = [
755
+ { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" },
756
+ ]
757
+
758
+ [[package]]
759
+ name = "python-dateutil"
760
+ version = "2.9.0.post0"
761
+ source = { registry = "https://pypi.org/simple" }
762
+ dependencies = [
763
+ { name = "six" },
764
+ ]
765
+ sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
766
+ wheels = [
767
+ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
768
+ ]
769
+
770
+ [[package]]
771
+ name = "python-multipart"
772
+ version = "0.0.26"
773
+ source = { registry = "https://pypi.org/simple" }
774
+ sdist = { url = "https://files.pythonhosted.org/packages/88/71/b145a380824a960ebd60e1014256dbb7d2253f2316ff2d73dfd8928ec2c3/python_multipart-0.0.26.tar.gz", hash = "sha256:08fadc45918cd615e26846437f50c5d6d23304da32c341f289a617127b081f17", size = 43501, upload-time = "2026-04-10T14:09:59.473Z" }
775
+ wheels = [
776
+ { url = "https://files.pythonhosted.org/packages/9a/22/f1925cdda983ab66fc8ec6ec8014b959262747e58bdca26a4e3d1da29d56/python_multipart-0.0.26-py3-none-any.whl", hash = "sha256:c0b169f8c4484c13b0dcf2ef0ec3a4adb255c4b7d18d8e420477d2b1dd03f185", size = 28847, upload-time = "2026-04-10T14:09:58.131Z" },
777
+ ]
778
+
779
+ [[package]]
780
+ name = "pytz"
781
+ version = "2026.1.post1"
782
+ source = { registry = "https://pypi.org/simple" }
783
+ sdist = { url = "https://files.pythonhosted.org/packages/56/db/b8721d71d945e6a8ac63c0fc900b2067181dbb50805958d4d4661cf7d277/pytz-2026.1.post1.tar.gz", hash = "sha256:3378dde6a0c3d26719182142c56e60c7f9af7e968076f31aae569d72a0358ee1", size = 321088, upload-time = "2026-03-03T07:47:50.683Z" }
784
+ wheels = [
785
+ { url = "https://files.pythonhosted.org/packages/10/99/781fe0c827be2742bcc775efefccb3b048a3a9c6ce9aec0cbf4a101677e5/pytz-2026.1.post1-py2.py3-none-any.whl", hash = "sha256:f2fd16142fda348286a75e1a524be810bb05d444e5a081f37f7affc635035f7a", size = 510489, upload-time = "2026-03-03T07:47:49.167Z" },
786
+ ]
787
+
788
+ [[package]]
789
+ name = "pyyaml"
790
+ version = "6.0.3"
791
+ source = { registry = "https://pypi.org/simple" }
792
+ sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
793
+ wheels = [
794
+ { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" },
795
+ { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" },
796
+ { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" },
797
+ { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" },
798
+ { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" },
799
+ { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" },
800
+ { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" },
801
+ { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" },
802
+ { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" },
803
+ { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" },
804
+ ]
805
+
806
+ [[package]]
807
+ name = "requests"
808
+ version = "2.33.1"
809
+ source = { registry = "https://pypi.org/simple" }
810
+ dependencies = [
811
+ { name = "certifi" },
812
+ { name = "charset-normalizer" },
813
+ { name = "idna" },
814
+ { name = "urllib3" },
815
+ ]
816
+ sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" }
817
+ wheels = [
818
+ { url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" },
819
+ ]
820
+
821
+ [[package]]
822
+ name = "rich"
823
+ version = "15.0.0"
824
+ source = { registry = "https://pypi.org/simple" }
825
+ dependencies = [
826
+ { name = "markdown-it-py" },
827
+ { name = "pygments" },
828
+ ]
829
+ sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" }
830
+ wheels = [
831
+ { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" },
832
+ ]
833
+
834
+ [[package]]
835
+ name = "safehttpx"
836
+ version = "0.1.7"
837
+ source = { registry = "https://pypi.org/simple" }
838
+ dependencies = [
839
+ { name = "httpx" },
840
+ ]
841
+ sdist = { url = "https://files.pythonhosted.org/packages/89/d1/4282284d9cf1ee873607a46442da977fc3c985059315ab23610be31d5885/safehttpx-0.1.7.tar.gz", hash = "sha256:db201c0978c41eddb8bb480f3eee59dd67304fdd91646035e9d9a720049a9d23", size = 10385, upload-time = "2025-10-24T18:30:09.783Z" }
842
+ wheels = [
843
+ { url = "https://files.pythonhosted.org/packages/2e/a3/0f0b7d78e2f1eb9e8e1afbff1d2bff8d60144aee17aca51c065b516743dd/safehttpx-0.1.7-py3-none-any.whl", hash = "sha256:c4f4a162db6993464d7ca3d7cc4af0ffc6515a606dfd220b9f82c6945d869cde", size = 8959, upload-time = "2025-10-24T18:30:08.733Z" },
844
+ ]
845
+
846
+ [[package]]
847
+ name = "semantic-version"
848
+ version = "2.10.0"
849
+ source = { registry = "https://pypi.org/simple" }
850
+ sdist = { url = "https://files.pythonhosted.org/packages/7d/31/f2289ce78b9b473d582568c234e104d2a342fd658cc288a7553d83bb8595/semantic_version-2.10.0.tar.gz", hash = "sha256:bdabb6d336998cbb378d4b9db3a4b56a1e3235701dc05ea2690d9a997ed5041c", size = 52289, upload-time = "2022-05-26T13:35:23.454Z" }
851
+ wheels = [
852
+ { url = "https://files.pythonhosted.org/packages/6a/23/8146aad7d88f4fcb3a6218f41a60f6c2d4e3a72de72da1825dc7c8f7877c/semantic_version-2.10.0-py2.py3-none-any.whl", hash = "sha256:de78a3b8e0feda74cabc54aab2da702113e33ac9d9eb9d2389bcf1f58b7d9177", size = 15552, upload-time = "2022-05-26T13:35:21.206Z" },
853
+ ]
854
+
855
+ [[package]]
856
+ name = "shellingham"
857
+ version = "1.5.4"
858
+ source = { registry = "https://pypi.org/simple" }
859
+ sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" }
860
+ wheels = [
861
+ { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" },
862
+ ]
863
+
864
+ [[package]]
865
+ name = "six"
866
+ version = "1.17.0"
867
+ source = { registry = "https://pypi.org/simple" }
868
+ sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
869
+ wheels = [
870
+ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
871
+ ]
872
+
873
+ [[package]]
874
+ name = "starlette"
875
+ version = "1.0.0"
876
+ source = { registry = "https://pypi.org/simple" }
877
+ dependencies = [
878
+ { name = "anyio" },
879
+ { name = "typing-extensions" },
880
+ ]
881
+ sdist = { url = "https://files.pythonhosted.org/packages/81/69/17425771797c36cded50b7fe44e850315d039f28b15901ab44839e70b593/starlette-1.0.0.tar.gz", hash = "sha256:6a4beaf1f81bb472fd19ea9b918b50dc3a77a6f2e190a12954b25e6ed5eea149", size = 2655289, upload-time = "2026-03-22T18:29:46.779Z" }
882
+ wheels = [
883
+ { url = "https://files.pythonhosted.org/packages/0b/c9/584bc9651441b4ba60cc4d557d8a547b5aff901af35bda3a4ee30c819b82/starlette-1.0.0-py3-none-any.whl", hash = "sha256:d3ec55e0bb321692d275455ddfd3df75fff145d009685eb40dc91fc66b03d38b", size = 72651, upload-time = "2026-03-22T18:29:45.111Z" },
884
+ ]
885
+
886
+ [[package]]
887
+ name = "sympy"
888
+ version = "1.14.0"
889
+ source = { registry = "https://pypi.org/simple" }
890
+ dependencies = [
891
+ { name = "mpmath" },
892
+ ]
893
+ sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" }
894
+ wheels = [
895
+ { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" },
896
+ ]
897
+
898
+ [[package]]
899
+ name = "tomlkit"
900
+ version = "0.14.0"
901
+ source = { registry = "https://pypi.org/simple" }
902
+ sdist = { url = "https://files.pythonhosted.org/packages/c3/af/14b24e41977adb296d6bd1fb59402cf7d60ce364f90c890bd2ec65c43b5a/tomlkit-0.14.0.tar.gz", hash = "sha256:cf00efca415dbd57575befb1f6634c4f42d2d87dbba376128adb42c121b87064", size = 187167, upload-time = "2026-01-13T01:14:53.304Z" }
903
+ wheels = [
904
+ { url = "https://files.pythonhosted.org/packages/b5/11/87d6d29fb5d237229d67973a6c9e06e048f01cf4994dee194ab0ea841814/tomlkit-0.14.0-py3-none-any.whl", hash = "sha256:592064ed85b40fa213469f81ac584f67a4f2992509a7c3ea2d632208623a3680", size = 39310, upload-time = "2026-01-13T01:14:51.965Z" },
905
+ ]
906
+
907
+ [[package]]
908
+ name = "torch"
909
+ version = "2.2.2"
910
+ source = { registry = "https://pypi.org/simple" }
911
+ dependencies = [
912
+ { name = "filelock" },
913
+ { name = "fsspec" },
914
+ { name = "jinja2" },
915
+ { name = "networkx" },
916
+ { name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
917
+ { name = "nvidia-cuda-cupti-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
918
+ { name = "nvidia-cuda-nvrtc-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
919
+ { name = "nvidia-cuda-runtime-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
920
+ { name = "nvidia-cudnn-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
921
+ { name = "nvidia-cufft-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
922
+ { name = "nvidia-curand-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
923
+ { name = "nvidia-cusolver-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
924
+ { name = "nvidia-cusparse-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
925
+ { name = "nvidia-nccl-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
926
+ { name = "nvidia-nvtx-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
927
+ { name = "sympy" },
928
+ { name = "typing-extensions" },
929
+ ]
930
+ wheels = [
931
+ { url = "https://files.pythonhosted.org/packages/4c/0c/d8f77363a7a3350c96e6c9db4ffb101d1c0487cc0b8cdaae1e4bfb2800ad/torch-2.2.2-cp312-cp312-manylinux1_x86_64.whl", hash = "sha256:cf12cdb66c9c940227ad647bc9cf5dba7e8640772ae10dfe7569a0c1e2a28aca", size = 755466713, upload-time = "2024-03-27T21:08:48.868Z" },
932
+ { url = "https://files.pythonhosted.org/packages/05/9b/e5c0df26435f3d55b6699e1c61f07652b8c8a3ac5058a75d0e991f92c2b0/torch-2.2.2-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:89ddac2a8c1fb6569b90890955de0c34e1724f87431cacff4c1979b5f769203c", size = 86515814, upload-time = "2024-03-27T21:09:07.247Z" },
933
+ { url = "https://files.pythonhosted.org/packages/72/ce/beca89dcdcf4323880d3b959ef457a4c61a95483af250e6892fec9174162/torch-2.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:451331406b760f4b1ab298ddd536486ab3cfb1312614cfe0532133535be60bea", size = 198528804, upload-time = "2024-03-27T21:09:14.691Z" },
934
+ { url = "https://files.pythonhosted.org/packages/79/78/29dcab24a344ffd9ee9549ec0ab2c7885c13df61cde4c65836ee275efaeb/torch-2.2.2-cp312-none-macosx_10_9_x86_64.whl", hash = "sha256:eb4d6e9d3663e26cd27dc3ad266b34445a16b54908e74725adb241aa56987533", size = 150797270, upload-time = "2024-03-27T21:08:29.623Z" },
935
+ { url = "https://files.pythonhosted.org/packages/4a/0e/e4e033371a7cba9da0db5ccb507a9174e41b9c29189a932d01f2f61ecfc0/torch-2.2.2-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:bf9558da7d2bf7463390b3b2a61a6a3dbb0b45b161ee1dd5ec640bf579d479fc", size = 59678388, upload-time = "2024-03-27T21:08:35.869Z" },
936
+ ]
937
+
938
+ [[package]]
939
+ name = "torchvision"
940
+ version = "0.17.2"
941
+ source = { registry = "https://pypi.org/simple" }
942
+ dependencies = [
943
+ { name = "numpy" },
944
+ { name = "pillow" },
945
+ { name = "torch" },
946
+ ]
947
+ wheels = [
948
+ { url = "https://files.pythonhosted.org/packages/ff/b6/a056fb68cae15e8aec4f854f78d4787086d77efa5468a29d5b744eee2a2b/torchvision-0.17.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:14fd1d4a033c325bdba2d03a69c3450cab6d3a625f85cc375781d9237ca5d04d", size = 1666430, upload-time = "2024-03-27T21:11:29.158Z" },
949
+ { url = "https://files.pythonhosted.org/packages/58/12/0be3c13b2694ce2d103d259a4c0692884d52b0b445387101d96965d5b060/torchvision-0.17.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9c3acbebbe379af112b62b535820174277b1f3eed30df264a4e458d58ee4e5b2", size = 1571152, upload-time = "2024-03-27T21:11:27.241Z" },
950
+ { url = "https://files.pythonhosted.org/packages/1c/e9/830390c704f1471c33faebe964c3ca99113e43ffc3f6653d3188ca04077c/torchvision-0.17.2-cp312-cp312-manylinux1_x86_64.whl", hash = "sha256:77d680adf6ce367166a186d2c7fda3a73807ab9a03b2c31a03fa8812c8c5335b", size = 6915847, upload-time = "2024-03-27T21:11:14.359Z" },
951
+ { url = "https://files.pythonhosted.org/packages/52/89/9af25236f7bc31fe74f88bde03bbd63c284d0aefa6d19bd92cc37433470c/torchvision-0.17.2-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:f1c9ab3152cfb27f83aca072cac93a3a4c4e4ab0261cf0f2d516b9868a4e96f3", size = 14008843, upload-time = "2024-03-27T21:10:59.268Z" },
952
+ { url = "https://files.pythonhosted.org/packages/fd/d1/8da7f30169f56764f0ef9ed961a32f300a2d782b6c1bc8b391c3014092f8/torchvision-0.17.2-cp312-cp312-win_amd64.whl", hash = "sha256:3f784381419f3ed3f2ec2aa42fb4aeec5bf4135e298d1631e41c926e6f1a0dff", size = 1165531, upload-time = "2024-03-27T21:11:22.555Z" },
953
+ ]
954
+
955
+ [[package]]
956
+ name = "tqdm"
957
+ version = "4.67.3"
958
+ source = { registry = "https://pypi.org/simple" }
959
+ dependencies = [
960
+ { name = "colorama", marker = "sys_platform == 'win32'" },
961
+ ]
962
+ sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" }
963
+ wheels = [
964
+ { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" },
965
+ ]
966
+
967
+ [[package]]
968
+ name = "typer"
969
+ version = "0.24.1"
970
+ source = { registry = "https://pypi.org/simple" }
971
+ dependencies = [
972
+ { name = "annotated-doc" },
973
+ { name = "click" },
974
+ { name = "rich" },
975
+ { name = "shellingham" },
976
+ ]
977
+ sdist = { url = "https://files.pythonhosted.org/packages/f5/24/cb09efec5cc954f7f9b930bf8279447d24618bb6758d4f6adf2574c41780/typer-0.24.1.tar.gz", hash = "sha256:e39b4732d65fbdcde189ae76cf7cd48aeae72919dea1fdfc16593be016256b45", size = 118613, upload-time = "2026-02-21T16:54:40.609Z" }
978
+ wheels = [
979
+ { url = "https://files.pythonhosted.org/packages/4a/91/48db081e7a63bb37284f9fbcefda7c44c277b18b0e13fbc36ea2335b71e6/typer-0.24.1-py3-none-any.whl", hash = "sha256:112c1f0ce578bfb4cab9ffdabc68f031416ebcc216536611ba21f04e9aa84c9e", size = 56085, upload-time = "2026-02-21T16:54:41.616Z" },
980
+ ]
981
+
982
+ [[package]]
983
+ name = "typing-extensions"
984
+ version = "4.15.0"
985
+ source = { registry = "https://pypi.org/simple" }
986
+ sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
987
+ wheels = [
988
+ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
989
+ ]
990
+
991
+ [[package]]
992
+ name = "typing-inspection"
993
+ version = "0.4.2"
994
+ source = { registry = "https://pypi.org/simple" }
995
+ dependencies = [
996
+ { name = "typing-extensions" },
997
+ ]
998
+ sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
999
+ wheels = [
1000
+ { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
1001
+ ]
1002
+
1003
+ [[package]]
1004
+ name = "tzdata"
1005
+ version = "2026.1"
1006
+ source = { registry = "https://pypi.org/simple" }
1007
+ sdist = { url = "https://files.pythonhosted.org/packages/19/f5/cd531b2d15a671a40c0f66cf06bc3570a12cd56eef98960068ebbad1bf5a/tzdata-2026.1.tar.gz", hash = "sha256:67658a1903c75917309e753fdc349ac0efd8c27db7a0cb406a25be4840f87f98", size = 197639, upload-time = "2026-04-03T11:25:22.002Z" }
1008
+ wheels = [
1009
+ { url = "https://files.pythonhosted.org/packages/b0/70/d460bd685a170790ec89317e9bd33047988e4bce507b831f5db771e142de/tzdata-2026.1-py2.py3-none-any.whl", hash = "sha256:4b1d2be7ac37ceafd7327b961aa3a54e467efbdb563a23655fbfe0d39cfc42a9", size = 348952, upload-time = "2026-04-03T11:25:20.313Z" },
1010
+ ]
1011
+
1012
+ [[package]]
1013
+ name = "urllib3"
1014
+ version = "2.6.3"
1015
+ source = { registry = "https://pypi.org/simple" }
1016
+ sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" }
1017
+ wheels = [
1018
+ { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" },
1019
+ ]
1020
+
1021
+ [[package]]
1022
+ name = "uvicorn"
1023
+ version = "0.44.0"
1024
+ source = { registry = "https://pypi.org/simple" }
1025
+ dependencies = [
1026
+ { name = "click" },
1027
+ { name = "h11" },
1028
+ ]
1029
+ sdist = { url = "https://files.pythonhosted.org/packages/5e/da/6eee1ff8b6cbeed47eeb5229749168e81eb4b7b999a1a15a7176e51410c9/uvicorn-0.44.0.tar.gz", hash = "sha256:6c942071b68f07e178264b9152f1f16dfac5da85880c4ce06366a96d70d4f31e", size = 86947, upload-time = "2026-04-06T09:23:22.826Z" }
1030
+ wheels = [
1031
+ { url = "https://files.pythonhosted.org/packages/b7/23/a5bbd9600dd607411fa644c06ff4951bec3a4d82c4b852374024359c19c0/uvicorn-0.44.0-py3-none-any.whl", hash = "sha256:ce937c99a2cc70279556967274414c087888e8cec9f9c94644dfca11bd3ced89", size = 69425, upload-time = "2026-04-06T09:23:21.524Z" },
1032
+ ]