From 8ca2df88da1c9049bb1866f7844a9ae7e5bd53e5 Mon Sep 17 00:00:00 2001 From: philipqueen Date: Sun, 9 Aug 2026 11:47:18 -0600 Subject: [PATCH 01/16] start with rearch plans --- .gitignore | 3 +- docs/architecture/00-known-issues.md | 32 +++++ docs/architecture/01-package-layout.md | 89 +++++++++++++ docs/architecture/02-core-library.md | 163 +++++++++++++++++++++++ docs/architecture/03-api-design.md | 73 ++++++++++ docs/architecture/04-frontend.md | 67 ++++++++++ docs/architecture/05-testing-strategy.md | 51 +++++++ docs/architecture/README.md | 72 ++++++++++ 8 files changed, 549 insertions(+), 1 deletion(-) create mode 100644 docs/architecture/00-known-issues.md create mode 100644 docs/architecture/01-package-layout.md create mode 100644 docs/architecture/02-core-library.md create mode 100644 docs/architecture/03-api-design.md create mode 100644 docs/architecture/04-frontend.md create mode 100644 docs/architecture/05-testing-strategy.md create mode 100644 docs/architecture/README.md diff --git a/.gitignore b/.gitignore index 0011c96..8ed3a70 100644 --- a/.gitignore +++ b/.gitignore @@ -153,4 +153,5 @@ cython_debug/ .vscode/ -.DS_Store \ No newline at end of file +.DS_Store +CLAUDE.md diff --git a/docs/architecture/00-known-issues.md b/docs/architecture/00-known-issues.md new file mode 100644 index 0000000..42c5797 --- /dev/null +++ b/docs/architecture/00-known-issues.md @@ -0,0 +1,32 @@ +# Known Issues in the Current Architecture + +This is the authoritative list of bugs and design smells found in the current `skelly_synchronize` codebase during the rearchitecture review. Every item has a stable ID (`KI-##`) so other design docs, PRs, and code comments can reference it directly (e.g. "resolves KI-07"). The goal is to make sure none of these are silently reproduced in the rewrite — each row states where it gets resolved. + +| ID | Area | Current behavior | Fix required | Resolved in | +|----|------|-------------------|---------------|-------------| +| KI-01 | Core data model | `video_info_dict`, `audio_signal_dict`, `lag_dict` are untyped, stringly-keyed dicts (e.g. `"video pathstring"`, `"camera name"`). Duration/fps keys are silently absent unless probed with the `"ffmpeg"` handler, causing latent `KeyError`s downstream. | Replace with typed Pydantic models with required fields validated at construction time. | [02-core-library.md](02-core-library.md) | +| KI-02 | Lag semantics | Audio-path lag values are normalized via `normalize_lag_dictionary` (max − value, latest-starting video gets lag 0); brightness-path lag values are **not** normalized (raw "seconds until brightness event"). Both are consumed identically downstream — works today, but the equivalence is implicit and undocumented. | Define one explicit `LagResult` contract ("seconds to trim from the front, normalized so the minimum lag is 0") that both algorithms must satisfy before returning. | [02-core-library.md](02-core-library.md) | +| KI-03 | Backend selection | Video probing (duration/fps) is hard-coded to always use the ffmpeg backend regardless of the caller's `video_handler` argument; only trimming actually honors the parameter, despite the function signature implying otherwise. | Make this a deliberate, documented policy inside the `VideoBackend` interface rather than an accidental hard-coding. | [02-core-library.md](02-core-library.md) | +| KI-04 | Orchestration | `synchronize_videos_from_audio` and `synchronize_videos_from_brightness` duplicate almost all orchestration logic: folder setup, fps-normalization retry block, frame-count verification logging, TOML debug dump. | Extract a shared pipeline of composable stages used by both sync methods. | [02-core-library.md](02-core-library.md) | +| KI-05 | Framerate normalization | `normalize_framerates` returns a path; callers are responsible for manually re-running video discovery/probing against the new folder, duplicated in both entry points. | Encapsulate the re-probe inside the normalization pipeline stage itself. | [02-core-library.md](02-core-library.md) | +| KI-06 | Trimming performance | The deffcode trim path checks `frame_number in frame_list` (a Python list) once per decoded frame — an O(n) scan per frame, making trimming O(n²) in frame count. | Use a `set[int]` or contiguous-range check for O(1) membership tests. | [02-core-library.md](02-core-library.md) | +| KI-07 | Side effects | `find_brightness_across_frames` writes a `.npy` sidecar file to disk as a side effect of what looks like a pure computation/getter. | Separate pure computation from persistence; make saving explicit and opt-in (debug stage only). | [02-core-library.md](02-core-library.md) | +| KI-08 | Brightness detection | `find_first_brightness_change` falls back to `argmax` of an all-`False` boolean array (index 0) as an implicit "no crossing found" sentinel — fragile if a real crossing could ever legitimately occur at frame 0. | Return an explicit `BrightnessEvent | None` instead of relying on an index-0 sentinel. | [02-core-library.md](02-core-library.md) | +| KI-09 | Debug plots | The brightness debug plot's x-axis is labeled "Time (s)" but the values plotted are raw frame indices (never divided by fps). | Fix the axis to either show correctly-scaled time or correctly label frame index. | [02-core-library.md](02-core-library.md) | +| KI-10 | Debug plots | `if Path(...).exists:` (missing parentheses) is used in the brightness entry point's debug-plot branch selection — a bound method is always truthy, so this branch is always taken regardless of whether normalization actually ran. | Fix the call; add static typing/linting that would catch this class of bug. | [02-core-library.md](02-core-library.md), [05-testing-strategy.md](05-testing-strategy.md) | +| KI-11 | Audio memory usage | `extract_audio_files` loads each full audio signal into memory via librosa with no streaming — long recordings are memory-heavy. | Accepted as a documented v1 limitation (local single-user tool, recordings are minutes not hours); revisit streaming if needed later. | [02-core-library.md](02-core-library.md) | +| KI-12 | Audio redundancy | `trim_audio_files` reloads each `.wav` from disk even though the signal was already loaded once in `extract_audio_files` and discarded. | Reuse the already-loaded in-memory signal instead of reloading. | [02-core-library.md](02-core-library.md) | +| KI-13 | Correlation reference | `find_cross_correlation_lags` picks `next(iter(audio_signal_dict))` as the reference signal — implicitly whichever camera sorts first alphabetically from file discovery. | Make reference-camera selection an explicit, deterministic, documented strategy. | [02-core-library.md](02-core-library.md) | +| KI-14 | Correlation confidence | `cross_correlate` returns only the arg-max lag with no confidence/peak-sharpness diagnostic — a poor or ambiguous correlation peak silently produces a lag with no way to flag it. | Add a confidence score to `LagResult`, surfaced through to the API job result. | [02-core-library.md](02-core-library.md), [03-api-design.md](03-api-design.md) | +| KI-15 | Scattered constants | `standard_audio_sample_rate = 44100` is hard-coded in `normalize_framerates.py` rather than living in the central constants module alongside other naming/path conventions. | Consolidate all magic constants into one config module. | [01-package-layout.md](01-package-layout.md) | +| KI-16 | Output format | Trimmed/normalized video output is always forced to `.mp4` regardless of the input container format. | Keep as a documented, deliberate v1 decision (simplifies backend code); revisit later if needed. | [02-core-library.md](02-core-library.md) | +| KI-17 | Layering | Production code (`skelly_synchronize.py`) imports helpers from `skelly_synchronize/tests/utilities/*` — test code is a runtime dependency of the library. | Promote these helpers into the core library proper. | [01-package-layout.md](01-package-layout.md) | +| KI-18 | Logging | `configure_logging` mutates the global root logger (`logging.getLogger("")`) — a library/application logging anti-pattern; fine only as long as it's called exactly once from an application entry point. | Library modules use `logging.getLogger(__name__)` only; a single `configure_logging()` call lives in the CLI/API entry points, never inside `core`. | [01-package-layout.md](01-package-layout.md) | +| KI-19 | Error handling | ffmpeg/ffprobe subprocess failures raise generic `RuntimeError`s with just a return code — captured stderr is logged but not included in the exception message; no timeouts or retries on subprocess calls. | Introduce a structured exception hierarchy that carries captured stderr and supports timeouts. | [02-core-library.md](02-core-library.md) | +| KI-20 | Packaging | `sys.path.insert` hacks appear in `__init__.py`, `__main__.py`, and test files instead of relying on proper package imports. | Remove entirely; rely on a correct editable/installed package layout. | [01-package-layout.md](01-package-layout.md) | +| KI-21 | Naming convention | `name_synced_video` strips a `"raw_"` prefix via a hard-coded `filename[4:]` slice, assuming the prefix is always exactly 4 characters. | Derive the new name via `str.removeprefix`/a config-driven naming scheme instead of a magic-length slice. | [01-package-layout.md](01-package-layout.md) | +| KI-22 | GUI responsiveness | The PySide6 GUI runs sync jobs synchronously on the Qt UI thread, freezing the window for the whole job; there is no progress bar or error dialog — the README documents the freeze as expected behavior. | Replace entirely with an async job model (API) and a live progress UI (frontend). | [03-api-design.md](03-api-design.md), [04-frontend.md](04-frontend.md) | +| KI-23 | Dead code | `gui/widgets/run_button_widget.py` is unused, not wired up (`clicked.connect` is commented out), and contains a bug (`self.setLayout = self._layout` assigns instead of calling). | Do not port; delete during the GUI removal phase. | [01-package-layout.md](01-package-layout.md) | +| KI-24 | Service boundary | There is no API/service boundary between the GUI and core sync logic today — the GUI calls the core functions directly. | FastAPI becomes the explicit boundary between any frontend and the core library. | [03-api-design.md](03-api-design.md) | +| KI-25 | Test infrastructure | `conftest.py` shares state via mutable attributes on the `pytest` module namespace rather than proper fixtures; only the audio sync path is ever exercised — brightness sync has zero test coverage; nearly all tests depend on a real downloaded dataset and real ffmpeg/deffcode execution (slow, network- and binary-dependent). | Redesign fixtures, add fast isolated unit tests, add brightness coverage. | [05-testing-strategy.md](05-testing-strategy.md) | +| KI-26 | Packaging metadata | `pyproject.toml` still has template boilerplate (`description = "Basic template of a python repository"`, `keywords = ["basic", "template", ...]`), dependencies are pinned with tight `==`, and `requires-python` (`>=3.9,<3.13`) doesn't match the CI matrix (only Python 3.10 is tested). | Clean up metadata, loosen pins to compatible ranges, align `requires-python` with the actual CI matrix. | [01-package-layout.md](01-package-layout.md) | diff --git a/docs/architecture/01-package-layout.md b/docs/architecture/01-package-layout.md new file mode 100644 index 0000000..3adcbf4 --- /dev/null +++ b/docs/architecture/01-package-layout.md @@ -0,0 +1,89 @@ +# Target Package Layout + +## Purpose + +This document defines the target repo/module structure and packaging decisions for the rewrite, so that a repo skeleton can be scaffolded before any subsystem code is written. + +## Monorepo, not multi-repo + +`skelly_synchronize` stays a single GitHub repository, with the new React app living in a `frontend/` subfolder. This is a local, single-user tool with one release cadence — splitting into separate repos would add release-coordination overhead (versioning, cross-repo CI, cross-repo issue tracking) with no real benefit at this scale. + +## Target tree + +``` +skelly_synchronize/ +├── packages/ +│ ├── core/ # skelly_sync_core — installable, zero FastAPI/PySide6 deps +│ │ ├── pyproject.toml +│ │ └── skelly_sync_core/ +│ │ ├── models.py # Pydantic data models (VideoInfo, AudioInfo, LagResult, ...) +│ │ ├── backends/ +│ │ │ ├── base.py # VideoBackend Protocol +│ │ │ ├── ffmpeg.py # FfmpegBackend +│ │ │ └── deffcode.py # DeffcodeBackend +│ │ ├── pipeline/ +│ │ │ ├── stages.py # PipelineStage implementations +│ │ │ └── runner.py # SyncPipeline +│ │ ├── audio.py +│ │ ├── brightness.py +│ │ ├── debug.py +│ │ ├── config.py # all constants: folder/file names, naming conventions, defaults +│ │ └── logging_setup.py # opt-in only; never called from inside core itself +│ └── api/ # skelly_sync_api — FastAPI app, depends on core +│ ├── pyproject.toml +│ └── skelly_sync_api/ +│ ├── main.py +│ ├── routers/ +│ ├── jobs.py # in-memory job store + per-job process orchestration +│ └── schemas.py # API-only wrapper models (e.g. JobCreateResponse) +├── frontend/ # React app (Vite + TypeScript), talks to api/ over HTTP only +├── cli/ # thin argparse/typer wrapper over core, replaces __main__.py +├── docs/ +│ └── architecture/ +└── pyproject.toml # root workspace config +``` + +## Dependency direction + +- `core` has zero knowledge of FastAPI, uvicorn, or PySide6. It only depends on the algorithm libraries it actually needs (numpy, scipy, librosa, opencv, deffcode, pydantic). +- `api` depends on `core` as a regular dependency. +- `cli` depends only on `core` (not `api`). +- `frontend` depends on nothing Python — it talks to `api` over HTTP only. + +## Core as its own installable package + +`core` ships as its own installable package, `skelly_sync_core`, distributed independently (e.g. on PyPI) so it can be reused by other tools — for example, embedding sync into a larger FreeMoCap pipeline — without pulling in FastAPI, uvicorn, or pydantic-settings as transitive dependencies. `api` declares `skelly_sync_core` as a normal dependency, not a path-based extra of the same package; keeping it as an "extra" of one combined package would force both to share one release cadence and one dependency footprint. + +Local development across the monorepo uses a workspace-style setup with path dependencies for `api` → `core` and `cli` → `core`, so changes in `core` are picked up immediately by the other packages during development. + +## Build backend + +Recommend switching from `flit_core` to **`hatchling`**, since it has better support for multi-package monorepo/workspace layouts. Keep the decision under review — if `hatchling` introduces friction, revisit, but there's no reason to default back to `flit_core`'s single-package assumptions once the repo has three installable Python packages. + +## Naming + +- `skelly_sync_core` and `skelly_sync_api` for the two installable packages, avoiding a name clash with the existing `skelly_synchronize` PyPI package. +- `skelly_synchronize` continues as the umbrella/meta name for the project and the CLI entry point for continuity with existing users. +- Console scripts: `skelly-sync` (CLI, replaces `python -m skelly_synchronize`/`__main__.py`) and `skelly-sync-api` (launches the FastAPI server, wraps `uvicorn`). + +## Constants and configuration (resolves KI-15) + +All magic strings and numbers — folder names, debug file names, naming conventions like the synced-video prefix, and defaults like the standard audio sample rate — are consolidated into `core/config.py`. No sync-relevant constant should live outside this module (the current codebase has one, `standard_audio_sample_rate`, defined locally inside `normalize_framerates.py` instead of the shared constants file — this must not recur). + +## Logging policy (resolves KI-18) + +Library modules (`core`, and `api`'s internal logic) use `logging.getLogger(__name__)` only and never call `logging.basicConfig`, add handlers to the root logger, or otherwise mutate global logging state. Exactly one place per process configures logging: the CLI entry point for `skelly-sync`, and the API entry point (`main.py`) for `skelly-sync-api`. + +## Removed in the rewrite + +- The entire PySide6 GUI package is deleted once the React frontend reaches feature parity (end of the migration's frontend phase) — not kept behind a flag or maintained in parallel. +- `gui/widgets/run_button_widget.py` (dead, buggy, unused — KI-23) is not ported at all. +- `sys.path.insert` hacks currently in `__init__.py`, `__main__.py`, and test files (KI-20) are removed; all imports rely on a correctly configured installed/editable package. +- The `name_synced_video` hard-coded `filename[4:]` prefix slice (KI-21) is replaced with `str.removeprefix("raw_")` (or equivalent) driven by the naming constants in `core/config.py`. +- `skelly_synchronize/tests/utilities/*` helpers that are imported by production code today (KI-17) are promoted into `core` proper; test code never becomes a runtime dependency of the library again. + +## Packaging cleanup (resolves KI-26) + +- Fix `pyproject.toml` metadata left over from the template repo (`description`, `keywords`). +- Loosen dependency pins from exact `==` to compatible ranges (e.g. `>=X, VideoInfo: ... + def trim(self, filepath: Path, start_seconds: float, end_seconds: float | None, output_path: Path) -> Path: ... + +class VideoBackendKind(str, Enum): + FFMPEG = "ffmpeg" + DEFFCODE = "deffcode" + +def get_backend(kind: VideoBackendKind) -> VideoBackend: ... +``` + +`Protocol` is used instead of `ABC` — it gives structural typing and makes mocking backends in tests trivial (a test double just needs the right method signatures, no inheritance required), while still being fully checkable with mypy. + +`FfmpegBackend` and `DeffcodeBackend` are the two concrete implementations. `DeffcodeBackend` preserves the existing rotation-metadata workaround (deffcode/ffmpeg auto-rotation combined with OpenCV's `VideoWriter` can double-apply rotation; the current code counters this with an explicit transpose filter keyed off `Sourcer`-reported orientation — this real-world fix must be carried forward, not rediscovered). + +**Explicit policy**: probing always delegates to `FfmpegBackend` internally, regardless of which `VideoBackendKind` was requested for trimming, because ffprobe is more complete/reliable for metadata than deffcode. This was true implicitly in the old code (`create_video_info_dict` hard-coded `video_handler="ffmpeg"` for probing); the rewrite keeps the same behavior but makes it an explicit, documented method on `DeffcodeBackend.probe` (delegates to an internal `FfmpegBackend` instance) rather than an accidental gap in the caller's logic. + +**Picklability**: only `VideoBackendKind` (a `str` enum) and the `get_backend` factory function are passed into `multiprocessing` worker processes — never a backend instance itself. This keeps worker task arguments trivially picklable regardless of what internal state a given backend implementation might hold. + +## Pipeline abstraction (resolves KI-04, KI-05) + +```python +class PipelineContext(BaseModel): + request: SyncRequest + videos: list[VideoInfo] = [] + normalized_folder_path: Path | None = None + lags: list[LagResult] = [] + result: SyncResult | None = None + # progress_callback is not a model field (not serializable) — threaded separately + +class PipelineStage(Protocol): + def run(self, context: PipelineContext, progress_callback: Callable[[str, float], None] | None) -> PipelineContext: ... +``` + +Shared stage list, used by **both** sync methods (this is what eliminates the current duplication between `synchronize_videos_from_audio` and `synchronize_videos_from_brightness`): + +1. `DiscoverVideosStage` — find input video files. +2. `ProbeStage` — build `VideoInfo` for each (always via the ffmpeg-delegating probe path). +3. `NormalizeFramerateStage` — conditional: runs only if fps (or, for audio, sample rate) diverges across inputs; internally re-runs discovery/probe against its own output before returning, so callers never have to manually re-probe (resolves KI-05). +4. `ComputeLagsStage` — method-specific: `AudioLagStage` or `BrightnessLagStage`, both returning `LagResult`s satisfying the shared contract above. +5. `TrimStage` — parallel trim across videos. +6. `ReattachAudioStage` — audio method only. +7. `DebugArtifactsStage` — optional, gated by `SyncRequest.create_debug_artifacts`. + +`SyncPipeline.run(request: SyncRequest, progress_callback=None) -> SyncResult` is the single public entry point. The old `synchronize_videos_from_audio`/`synchronize_videos_from_brightness` functions either become thin wrappers around `SyncPipeline.run(..., method=...)` for backward compatibility during the migration, or are removed once all callers (CLI, API) are updated to call `SyncPipeline` directly. + +## Concurrency model for trimming + +Replace `multiprocessing.Pool.starmap` with `concurrent.futures.ProcessPoolExecutor` + `as_completed`. This gives two things the current implementation lacks: + +- **Per-task error isolation**: today, if one worker's `trim_single_video` raises, the whole `starmap` call surfaces a single aggregate failure with no partial-result visibility. `as_completed` lets the pipeline report exactly which camera failed and why, while still letting sibling trims finish. +- **A natural hook for progress reporting**: each completed future can immediately report per-camera progress instead of the pipeline blocking silently until every video is done. + +Worker function signature takes only picklable arguments: `(video_info: VideoInfo, lag: LagResult, backend_kind: VideoBackendKind, output_dir: Path)`. + +### Progress reporting (cross-referenced from `03-api-design.md`) + +`core` exposes a generic, optional hook: `progress_callback: Callable[[str, float], None] | None`, threaded through `PipelineContext` and down into `TrimStage`. When run from the CLI it's a no-op or a simple print; when run from the API, `api` supplies a callback that writes into a `multiprocessing.Manager().dict()` created per job. **The mechanism lives in `api`, not `core`** — `core` stays deployment-agnostic and has no notion of "jobs" or shared process state; it just calls whatever callback it was given. + +### Trim performance fix (resolves KI-06) + +The deffcode trim path's current `frame_number in frame_list` check (O(n) list scan per decoded frame, O(n²) overall) is replaced with a `set[int]` membership test or, where the frame list is contiguous, a simple range check — O(1) per frame. + +## Audio subsystem + +- **Reference-camera selection (resolves KI-13)**: today, `find_cross_correlation_lags` picks `next(iter(audio_signal_dict))`, an implicit dependency on alphabetical file-discovery order. The rewrite makes this an explicit, documented strategy — recommend "camera with the longest audio duration" as a tie-break-free deterministic choice (falls back sensibly even if all durations happen to match, since ties then resolve to sorted-camera-name order, which is still deterministic and documented). +- **In-memory reuse (resolves KI-12)**: `trim_audio_files` reuses the signal already loaded during extraction instead of reloading each `.wav` from disk. +- **Confidence score (resolves KI-14)**: `cross_correlate` additionally returns a confidence metric (e.g. ratio of the peak correlation value to the surrounding noise floor, or peak sharpness) that populates `LagResult.confidence`. +- **In-memory-only loading (KI-11)**: accepted as a documented v1 limitation given the local single-user, minutes-not-hours use case. Not addressed by streaming in this pass. + +## Brightness subsystem + +- **No side effects in computation (resolves KI-07)**: `compute_brightness_series(video: VideoInfo, backend: VideoBackend) -> np.ndarray` is a pure function. Persistence to a `.npy` sidecar becomes a separate, explicit `save_brightness_series(series, path)` call, made only by `DebugArtifactsStage` when debug artifacts are requested. +- **Explicit event result (resolves KI-08)**: `find_first_brightness_change` returns `BrightnessEvent | None` (carrying the detected frame index and its converted lag in seconds) instead of relying on an index-0 fallback as an implicit "not found" sentinel. + +## Error handling (resolves KI-19) + +A structured exception hierarchy replaces generic `RuntimeError`s: + +```python +class SkellySyncError(Exception): ... +class VideoProbeError(SkellySyncError): ... +class BackendSubprocessError(SkellySyncError): + def __init__(self, message: str, stderr: str, returncode: int | None, timed_out: bool): ... +``` + +All ffmpeg/ffprobe subprocess invocations are wrapped with an explicit timeout and, on failure, raise `BackendSubprocessError` carrying the captured stderr — today this is logged but dropped from the exception message itself, making failures hard to diagnose from just the raised error. + +## Debug artifacts + +- Fixes the brightness debug plot's mislabeled x-axis (KI-09): the axis is either correctly divided by fps to show real time, or explicitly labeled as frame index — not both mismatched as today. +- Fixes the `Path.exists` missing-parentheses bug (KI-10) in the branch that decides whether to source brightness debug data from the raw or normalized folder. + +## Output format (KI-16) + +Trimmed/normalized output remains `.mp4` regardless of input container — a deliberate, documented v1 decision that keeps backend code simple. Revisit only if a concrete need for preserving other containers arises. + +## Known issues resolved by this document + +KI-01, KI-02, KI-03, KI-04, KI-05, KI-06, KI-07, KI-08, KI-09, KI-10, KI-11 (documented, not fixed), KI-12, KI-13, KI-14, KI-16, KI-19. diff --git a/docs/architecture/03-api-design.md b/docs/architecture/03-api-design.md new file mode 100644 index 0000000..52bba6e --- /dev/null +++ b/docs/architecture/03-api-design.md @@ -0,0 +1,73 @@ +# FastAPI Service Design (`skelly_sync_api`) + +## Deployment model + +Localhost-only, single-user. The API is launched locally via the `skelly-sync-api` console script (wrapping `uvicorn app:app --host 127.0.0.1`) and is meant to run on the same machine as the browser tab serving the React frontend. There is no authentication, no multi-tenancy, and no need for hardened CORS — CORS is configured permissively for `http://localhost:*` origins purely to support local development where the Vite dev server and the API run on different ports. + +## Job model and storage + +```python +class JobStatus(str, Enum): + PENDING = "pending" + RUNNING = "running" + SUCCEEDED = "succeeded" + FAILED = "failed" + +class Job(BaseModel): + id: UUID + status: JobStatus + progress: float = 0.0 # 0.0 - 1.0 + progress_message: str | None = None + created_at: datetime + updated_at: datetime + request: SyncRequest # reused directly from skelly_sync_core + result: SyncResult | None = None + error: str | None = None +``` + +**Storage: in-memory `dict[UUID, Job]` guarded by a lock — not SQLite.** This is a deliberate choice for a local single-user app: jobs are ephemeral coordination state, and the actual durable output of a sync run is the files it writes to disk. There is no requirement to recover job history across a server restart. Adding SQLite (schema, migrations) would be overhead with no corresponding benefit here. Trade-off, stated explicitly: restarting the API process loses in-flight and historical job records — acceptable, since the user can simply re-run a sync against the same input folder. + +**Job execution**: each job runs in its own OS process (not just a thread), launched from the job-creation endpoint. This gives two things: a crashed or hung sync job can't take down the API process itself, and `core`'s own `ProcessPoolExecutor` (used internally for parallel trimming) isn't nested awkwardly inside a thread running an asyncio event loop. + +### Progress bridge + +For each job, `api` creates a `multiprocessing.Manager().dict()` and passes a callback into `SyncPipeline.run(request, progress_callback=callback)` (see [02-core-library.md](02-core-library.md#concurrency-model-for-trimming)) that writes `{stage_name, fraction_complete}` updates into that shared dict. A lightweight read path (either a background poller updating the in-memory `Job`, or the `GET /jobs/{id}` handler reading the Manager dict directly) surfaces this as `Job.progress`/`Job.progress_message`. This keeps `core` itself unaware of "jobs" — it only ever sees a generic callback. + +## Endpoints + +| Method & path | Purpose | +|---|---| +| `GET /health` | Liveness check, `{"status": "ok"}`. | +| `GET /cameras?folder_path=...` | Validates a folder path and returns the discovered video files/camera names, using `core`'s discovery stage standalone — lets the frontend show a preview before a job is started. | +| `POST /jobs` | Body: `SyncRequest`. Starts a job in a new process, returns `201 {job_id, status: "pending"}` immediately. | +| `GET /jobs/{job_id}` | Returns the full `Job` — status, progress, progress_message, `result` if succeeded, `error` if failed. | +| `GET /jobs` | Lists recent jobs (in-memory, capped at e.g. the last 20) — powers a simple job-history panel. | +| `DELETE /jobs/{job_id}` | Best-effort cancellation: terminates the job's OS process and marks it `CANCELLED` (add to `JobStatus`) if implemented in this pass; otherwise explicitly out of scope for v1 and documented as returning `501`. | +| `GET /jobs/{job_id}/debug-plot` | Serves the `debug_plot.png` artifact directly, so the frontend can embed it via ``. Only meaningful if the job's `SyncRequest.create_debug_artifacts` was true. | + +## Request/response schemas + +`SyncRequest`, `Job`, and `SyncResult` are the `core` Pydantic models used directly as FastAPI request/response schemas — no separate duplicate schema layer. `skelly_sync_api/schemas.py` only defines API-specific wrapper types that have no equivalent in `core`, such as `JobCreateResponse`. + +## Error handling + +A FastAPI exception handler maps `core`'s exception hierarchy (see [02-core-library.md](02-core-library.md#error-handling-resolves-ki-19)) to HTTP status codes: + +| `core` exception | HTTP status | +|---|---| +| Folder not found / invalid input path | 404 | +| `VideoProbeError` | 422 | +| `BackendSubprocessError` | 500, with `stderr` included in the response `detail` | +| Any other unhandled `SkellySyncError` | 500 | + +## Polling contract (for the frontend) + +The frontend polls `GET /jobs/{id}` on a fixed interval — recommend **1 second** — while `status` is `pending` or `running`, and stops polling once a terminal status (`succeeded`, `failed`, or `cancelled`) is reached. This contract is defined here once and simply referenced from [04-frontend.md](04-frontend.md) rather than re-derived there. + +## Manual testing during development + +FastAPI's auto-generated `/docs` (Swagger UI) is sufficient for manually exercising every endpoint during the phase where the API exists but the React frontend does not yet (see the phased plan in [README.md](README.md)) — no separate manual-testing tooling is needed for that phase. + +## Known issues resolved by this document + +KI-22 (GUI-thread-blocking sync replaced by an async job model), KI-24 (no API/service boundary — this is the boundary), and (with [02-core-library.md](02-core-library.md)) KI-14 (confidence surfaced through the job result). diff --git a/docs/architecture/04-frontend.md b/docs/architecture/04-frontend.md new file mode 100644 index 0000000..f28ac6e --- /dev/null +++ b/docs/architecture/04-frontend.md @@ -0,0 +1,67 @@ +# Frontend Design (React) + +## Purpose + +Design for the React app that replaces the PySide6 desktop GUI. Kept deliberately simple, matching the project's own framing of "a simple react one" and the small number of screens the tool actually needs. + +## Tech choices + +- **Vite + React + TypeScript.** No Next.js or other SSR/routing framework — there's no server-rendering or routing complexity that justifies it for a 2–4 screen local tool. +- **API client**: a small, hand-written `src/api/client.ts` with TypeScript interfaces mirroring the Pydantic models defined in [03-api-design.md](03-api-design.md) (`SyncRequest`, `Job`, `SyncResult`, ...), wrapping plain `fetch`. The API surface is small enough that generated-client tooling (e.g. `openapi-typescript`) is a nice-to-have for later, not required for v1. + +## State management + +**No Redux, Zustand, or other state library.** Plain React state (`useState`/`useReducer`) plus one custom hook, `useJobPolling(jobId)`, that polls `GET /jobs/{id}` on the interval defined in [03-api-design.md](03-api-design.md#polling-contract-for-the-frontend) (1s) and exposes `{status, progress, progressMessage, result, error}`, stopping automatically once a terminal status is reached. This matches the app's small size — a state library would be pure overhead here. + +## Screens + +### Setup screen + +Replaces today's GUI, which only exposes 2 of the several parameters `core` actually supports. The new setup screen exposes all of them: + +- Raw video folder path — a plain text input for an absolute path, not a browser file picker. (Browsers cannot reliably expose real filesystem *paths* from a picker due to the File System Access API's security model, and since the API and browser run on the same machine here, a plain path field is simpler and fully sufficient — documented explicitly as the reason, not an oversight.) +- Sync method selector (audio / brightness). +- Backend selector (ffmpeg / deffcode) — newly exposed; today's GUI has no way to choose this. +- Brightness ratio threshold field — shown only when brightness is selected (matches today's one exposed parameter). +- Debug-artifacts toggle — newly exposed. +- Optional custom output folder path — newly exposed (`core` already supports overriding `synchronized_video_folder_path`; the GUI never surfaced it). +- "Start sync" button — `POST /jobs`, then navigates to the Job Progress screen with the returned `job_id`. + +### Job Progress screen + +- Progress bar and status text driven by `useJobPolling`. +- Cancel button, wired to `DELETE /jobs/{id}` if implemented server-side; otherwise omitted/disabled with a tooltip noting it's not yet supported (mirrors the "may be out of scope for v1" note in [03-api-design.md](03-api-design.md)). +- On `succeeded`, navigates to the Result screen; on `failed`, shows the error inline (see Error display below) with an option to return to Setup. + +### Result screen + +- Per-camera lag summary table (`camera_name`, `lag_seconds`, `confidence`) from `SyncResult.lags`. +- Debug plot image, ``, shown only if debug artifacts were requested. +- Output folder path, shown as selectable text (a browser page cannot open a native file-manager window — documented as a known limitation rather than attempted). +- "Run another sync" button, returns to Setup. + +### History screen (optional / stretch) + +Lists recent jobs from `GET /jobs`; clicking one re-displays its Result screen. Explicitly marked optional — not required for feature parity with the current GUI, which has no history at all. + +## Error display (resolves part of KI-22) + +Today, sync errors are only visible in logs/stdout — the GUI has no error dialog. The new frontend shows inline error banners on the relevant screen, reading either `Job.error` (job-level failures) or the HTTP response's `detail` field (request-level failures, e.g. an invalid folder path from `GET /cameras` or `POST /jobs`). + +## Styling + +Minimal, plain CSS (or CSS Modules) — no heavyweight design system or component library. This is a small internal tool, and adding a design system would be effort disproportionate to its scope. + +## Dev / run model + +**v1**: two processes — `skelly-sync-api` (FastAPI/uvicorn on `127.0.0.1:8000`) and `npm run dev` (Vite dev server, proxying API calls to the FastAPI port). The user runs both and opens the Vite dev server URL in a browser. + +Bundling the frontend as static files served directly by FastAPI (a single-process app, closer to the original desktop-app feel) is deferred to a later polish phase, once the API/frontend split has proven itself — not attempted in the initial rewrite. + +## Location + +Lives in-repo under `frontend/`, per the monorepo decision in [01-package-layout.md](01-package-layout.md). + +## Known issues resolved by this document + +KI-22 (blocking, feedback-less sync UI replaced by an async progress screen with error display), and the parameter-exposure gap noted in the current GUI (only 2 of several `core` parameters were ever surfaced). diff --git a/docs/architecture/05-testing-strategy.md b/docs/architecture/05-testing-strategy.md new file mode 100644 index 0000000..5351d77 --- /dev/null +++ b/docs/architecture/05-testing-strategy.md @@ -0,0 +1,51 @@ +# Testing Strategy + +## Current-state problems (resolves KI-25) + +- `conftest.py` shares state via mutable attributes set directly on the `pytest` module namespace, rather than proper fixtures — fragile, hidden coupling, not test-isolated. +- Only the audio sync path is exercised anywhere in the suite; brightness sync (`synchronize_videos_from_brightness`) has zero test coverage. +- Nearly every test depends on a real dataset downloaded from Figshare and real ffmpeg/deffcode execution — slow, requires network access, and requires FFmpeg installed. There are no fast, isolated unit tests for most logic. +- There are no GUI tests (and, going forward, will need no PySide6 tests at all once it's removed). + +## Target test pyramid + +### `core` — unit tests + +Fast, isolated, no real video/audio files or subprocess calls required: + +- Pipeline stage tests using a mocked `VideoBackend` (trivial given the `Protocol`-based interface from [02-core-library.md](02-core-library.md) — a test double just needs matching method signatures). +- Pure-function tests for lag normalization (`LagResult` contract, KI-02) and brightness-event detection (KI-08) using small synthetic numpy arrays — this is what closes the current zero-coverage gap on the brightness path cheaply, without needing real video fixtures. +- Regression tests specifically targeting each fixed known issue where feasible (e.g. a synthetic case that would have tripped the old index-0 sentinel bug, KI-08; a case verifying the `Path.exists()` fix, KI-10). + +### `core` — integration tests + +A small number of tests run against tiny real fixture videos (a few seconds each) checked into the repo, instead of relying on a large downloaded dataset for every test run. This covers real ffmpeg/deffcode invocation without the cost of the full Figshare dataset. + +One slower, more thorough tier still exercises the existing Figshare-downloaded sample dataset for full end-to-end confidence, but is marked `@pytest.mark.slow` and excluded from the default local/CI run — opt-in only (e.g. a scheduled CI job or an explicit `pytest -m slow` invocation). + +### `api` — endpoint tests + +`TestClient`-based tests hitting each endpoint in [03-api-design.md](03-api-design.md), with the underlying `core` pipeline dependency-injected as a fast/mocked implementation so these tests don't wait minutes for a real ffmpeg trim to complete. + +### `frontend` — component tests + +Vitest + React Testing Library, covering the `useJobPolling` hook and the Setup/Progress/Result screens from [04-frontend.md](04-frontend.md). No end-to-end test requirement for v1 — explicitly deferred. + +## Fixture / conftest redesign + +Replace the current `pytest`-namespace-global pattern with proper `pytest.fixture`s: + +- A session-scoped fixture providing the small checked-in fixture videos, used by default across the fast integration tier. +- A separate, explicitly opt-in fixture for downloading and using the full Figshare dataset, used only by the slow-marked tests. + +## CI plan + +- Fix the Python version mismatch (KI-26): the CI matrix should match whatever `requires-python` actually claims in `pyproject.toml`, rather than testing only 3.10 against a wider claimed range. +- Run `core` unit tests (and `api` tests) on every push/PR. +- Run the slow/integration tier (real fixture videos, and the full Figshare-based tier) on a schedule or behind an explicit label/trigger, not on every push. +- Run `frontend` tests (`npm test`) in CI alongside the Python suite. +- Actually wire up the linting that already has config but isn't enforced today: `.flake8` exists but is not run in any CI workflow, and only Black formatting is checked on PRs. Recommend consolidating Black + flake8 + isort into `ruff` (format + lint in one tool) during the rewrite to reduce tooling overhead — flagged as a nice-to-have, not a blocker for the rewrite itself. + +## Known issues resolved by this document + +KI-25, and (jointly with `02-core-library.md`) the regression-test angle on KI-08/KI-10. diff --git a/docs/architecture/README.md b/docs/architecture/README.md new file mode 100644 index 0000000..1c4e07b --- /dev/null +++ b/docs/architecture/README.md @@ -0,0 +1,72 @@ +# skelly_synchronize Rearchitecture — Target Architecture + +This directory describes the **target** architecture for the `skelly_synchronize` rewrite. It exists to guide implementation, not to document the system as it is today — the current architecture is referenced only for contrast or rationale where useful. See [00-known-issues.md](00-known-issues.md) for the full list of current bugs/smells and where each is resolved. + +## Goals + +1. Improve the code quality of the core synchronization library — typed data models, a proper backend abstraction, unified pipeline orchestration, and fixes to the concurrency/correctness smells listed in [00-known-issues.md](00-known-issues.md). +2. Add a FastAPI server exposing sync functionality over HTTP. +3. Replace the PySide6 desktop GUI with a simple React frontend. + +**Done** looks like: a `core` library with typed models and no known-issue regressions, a FastAPI server backing all the functionality the old GUI exposed (plus previously-hidden parameters it never surfaced), and a React frontend at feature parity with the old GUI — after which the PySide6 GUI package is deleted. + +## Document map + +| Doc | Purpose | +|---|---| +| [00-known-issues.md](00-known-issues.md) | Numbered `KI-##` list of current bugs/smells and where each is resolved — the checklist nothing should silently reproduce. | +| [01-package-layout.md](01-package-layout.md) | Target repo/module structure, monorepo layout, packaging and build decisions. | +| [02-core-library.md](02-core-library.md) | `skelly_sync_core` design: typed data models, `VideoBackend` interface, pipeline abstraction, concurrency model. | +| [03-api-design.md](03-api-design.md) | `skelly_sync_api` design: FastAPI endpoints, schemas, job model, progress bridge. | +| [04-frontend.md](04-frontend.md) | React app: screens, API client, state management approach. | +| [05-testing-strategy.md](05-testing-strategy.md) | Test pyramid across `core`/`api`/frontend, fixture redesign, CI plan. | + +Read in the numbered order above — each later doc assumes the decisions made in the earlier ones (package boundaries before core design, core design before the API that wraps it, API before the frontend that consumes it). + +## Target architecture at a glance + +``` + ┌────────────┐ + │ frontend │ React (Vite + TS) — talks HTTP only + └─────┬──────┘ + │ HTTP (polling) + ┌─────▼──────┐ + │ api │ FastAPI (skelly_sync_api) — job orchestration, HTTP boundary + └─────┬──────┘ + │ Python calls + ┌─────▼──────┐ ┌────────────┐ + │ core │◄──────┤ cli │ both depend only on core + │(skelly_sync_core) └────────────┘ + └────────────┘ +``` + +`core` has no knowledge of `api` or `frontend`. `api` and `cli` are both thin consumers of `core`, so the sync engine is usable standalone (scriptable, embeddable in other tools) independent of whether the API/frontend exist at all. See [01-package-layout.md](01-package-layout.md) for the full package tree. + +## Non-goals + +- No authentication or multi-user support — this is a local, single-user tool (see the deployment model in [03-api-design.md](03-api-design.md)). +- No cloud/object storage — plain local filesystem paths, same usage model as today. +- No mobile support. +- No plugin/extension system. + +## Phased migration plan + +The rewrite proceeds in phases. **Each phase ends with the tool still fully usable end-to-end** — never a broken intermediate state — so the team (or a single developer) can pause between phases without leaving the tool unusable. + +1. **Phase 0 — scaffold `core`.** Stand up the new package layout, typed models, and the `VideoBackend` protocol with only the `FfmpegBackend` implementation. Port logic incrementally with unit tests. Keep the *old* `skelly_synchronize.py` entry points working by delegating internally to the new pipeline where practical, so the existing PySide6 GUI keeps functioning throughout this phase. +2. **Phase 1 — finish the `core` rewrite.** Complete the pipeline abstraction, the brightness path, the `DeffcodeBackend`, the audio subsystem, and debug artifacts. The old GUI now runs entirely against the new `core` library — this phase proves `core`'s public surface is sufficient before any API work begins. +3. **Phase 2 — FastAPI layer.** Build `skelly_sync_api` wrapping the now-finished `core`, starting with an in-memory job store. Test manually via the FastAPI-generated `/docs` UI — no frontend exists yet. +4. **Phase 3 — React frontend.** Build the frontend against the FastAPI layer from Phase 2. Once it reaches feature parity with the old GUI, delete the PySide6 GUI package entirely. +5. **Phase 4 — cleanup.** Remove any remaining old dict-based code paths, finalize packaging/CI per [01-package-layout.md](01-package-layout.md) and [05-testing-strategy.md](05-testing-strategy.md), update the top-level README, and tag a release. + +## Key decisions at a glance + +| Decision | Choice | Rationale (detail in linked doc) | +|---|---|---| +| Job storage | In-memory dict + lock, no SQLite | Jobs are ephemeral; durable output is the files on disk. [03-api-design.md](03-api-design.md) | +| `core` as a separate installable package | Yes — `skelly_sync_core` | Reusable independent of FastAPI/PySide6. [01-package-layout.md](01-package-layout.md) | +| Repo structure | Monorepo: `packages/core`, `packages/api`, `cli/`, `frontend/` | Single release cadence, no cross-repo coordination overhead. [01-package-layout.md](01-package-layout.md) | +| Frontend location | Same repo, `frontend/` | See above. [01-package-layout.md](01-package-layout.md) | +| Progress reporting | `core` exposes a generic callback hook; `api` supplies the actual mechanism | Keeps `core` deployment-agnostic. [02-core-library.md](02-core-library.md), [03-api-design.md](03-api-design.md) | +| Typed data model approach | Pydantic throughout `core`/`api` (raw audio arrays excluded) | Avoids a dataclass↔Pydantic translation layer since FastAPI already requires Pydantic. [02-core-library.md](02-core-library.md) | +| Long-running job UX | Async job + polling (not WebSockets) | Simple, sufficient for a local single-user app. [03-api-design.md](03-api-design.md) | From f819c5100f69c3eba307397955c1e80e31cab3cf Mon Sep 17 00:00:00 2001 From: philipqueen Date: Sun, 9 Aug 2026 12:11:33 -0600 Subject: [PATCH 02/16] remove two package nonsense --- docs/architecture/01-package-layout.md | 69 ++++++++++++-------------- docs/architecture/02-core-library.md | 2 +- docs/architecture/03-api-design.md | 6 +-- docs/architecture/README.md | 16 +++--- 4 files changed, 45 insertions(+), 48 deletions(-) diff --git a/docs/architecture/01-package-layout.md b/docs/architecture/01-package-layout.md index 3adcbf4..6fea6cb 100644 --- a/docs/architecture/01-package-layout.md +++ b/docs/architecture/01-package-layout.md @@ -10,61 +10,58 @@ This document defines the target repo/module structure and packaging decisions f ## Target tree +One Python package, `skelly_synchronize`, published as a single PyPI distribution. `core` and `api` are subpackages within it, not separate distributions — see "Single package, `api` as an optional extra" below for why. + ``` skelly_synchronize/ -├── packages/ -│ ├── core/ # skelly_sync_core — installable, zero FastAPI/PySide6 deps -│ │ ├── pyproject.toml -│ │ └── skelly_sync_core/ -│ │ ├── models.py # Pydantic data models (VideoInfo, AudioInfo, LagResult, ...) -│ │ ├── backends/ -│ │ │ ├── base.py # VideoBackend Protocol -│ │ │ ├── ffmpeg.py # FfmpegBackend -│ │ │ └── deffcode.py # DeffcodeBackend -│ │ ├── pipeline/ -│ │ │ ├── stages.py # PipelineStage implementations -│ │ │ └── runner.py # SyncPipeline -│ │ ├── audio.py -│ │ ├── brightness.py -│ │ ├── debug.py -│ │ ├── config.py # all constants: folder/file names, naming conventions, defaults -│ │ └── logging_setup.py # opt-in only; never called from inside core itself -│ └── api/ # skelly_sync_api — FastAPI app, depends on core -│ ├── pyproject.toml -│ └── skelly_sync_api/ -│ ├── main.py -│ ├── routers/ -│ ├── jobs.py # in-memory job store + per-job process orchestration -│ └── schemas.py # API-only wrapper models (e.g. JobCreateResponse) +├── pyproject.toml # single package; "api" extra pulls in fastapi/uvicorn +├── skelly_synchronize/ +│ ├── core/ # sync engine — zero FastAPI/PySide6 deps +│ │ ├── models.py # Pydantic data models (VideoInfo, AudioInfo, LagResult, ...) +│ │ ├── backends/ +│ │ │ ├── base.py # VideoBackend Protocol +│ │ │ ├── ffmpeg.py # FfmpegBackend +│ │ │ └── deffcode.py # DeffcodeBackend +│ │ ├── pipeline/ +│ │ │ ├── stages.py # PipelineStage implementations +│ │ │ └── runner.py # SyncPipeline +│ │ ├── audio.py +│ │ ├── brightness.py +│ │ ├── debug.py +│ │ ├── config.py # all constants: folder/file names, naming conventions, defaults +│ │ └── logging_setup.py # opt-in only; never called from inside core itself +│ ├── api/ # FastAPI app — only importable when the "api" extra is installed +│ │ ├── main.py +│ │ ├── routers/ +│ │ ├── jobs.py # in-memory job store + per-job process orchestration +│ │ └── schemas.py # API-only wrapper models (e.g. JobCreateResponse) +│ └── cli/ # thin argparse/typer wrapper over core, replaces __main__.py ├── frontend/ # React app (Vite + TypeScript), talks to api/ over HTTP only -├── cli/ # thin argparse/typer wrapper over core, replaces __main__.py -├── docs/ -│ └── architecture/ -└── pyproject.toml # root workspace config +└── docs/ + └── architecture/ ``` ## Dependency direction - `core` has zero knowledge of FastAPI, uvicorn, or PySide6. It only depends on the algorithm libraries it actually needs (numpy, scipy, librosa, opencv, deffcode, pydantic). -- `api` depends on `core` as a regular dependency. +- `api` depends on `core` (an internal import within the same package — no separate dependency declaration needed). - `cli` depends only on `core` (not `api`). - `frontend` depends on nothing Python — it talks to `api` over HTTP only. -## Core as its own installable package +## Single package, `api` as an optional extra -`core` ships as its own installable package, `skelly_sync_core`, distributed independently (e.g. on PyPI) so it can be reused by other tools — for example, embedding sync into a larger FreeMoCap pipeline — without pulling in FastAPI, uvicorn, or pydantic-settings as transitive dependencies. `api` declares `skelly_sync_core` as a normal dependency, not a path-based extra of the same package; keeping it as an "extra" of one combined package would force both to share one release cadence and one dependency footprint. +`core` and `api` ship in one PyPI distribution (`skelly_synchronize`), not as two separately published packages. `fastapi`/`uvicorn` are declared under an optional extra — `pip install skelly_synchronize[api]` — so a consumer who only wants the sync engine (`pip install skelly_synchronize`) doesn't pull in the web-server dependencies, without requiring a second PyPI distribution, a second version number, or a second release process. -Local development across the monorepo uses a workspace-style setup with path dependencies for `api` → `core` and `cli` → `core`, so changes in `core` are picked up immediately by the other packages during development. +This intentionally defers the earlier idea of publishing `core` as its own separately-versioned package. That split only pays for itself once something *outside this repo* — e.g. the main FreeMoCap pipeline — actually wants to `import` the sync engine in-process instead of calling the API over HTTP. There's no such consumer today, so the extra release/versioning overhead of a second distribution isn't justified yet. If that need materializes later, splitting `core` out into its own package is a mechanical refactor at that point — the module boundary already exists internally (see "Dependency direction" above) — not a redesign. ## Build backend -Recommend switching from `flit_core` to **`hatchling`**, since it has better support for multi-package monorepo/workspace layouts. Keep the decision under review — if `hatchling` introduces friction, revisit, but there's no reason to default back to `flit_core`'s single-package assumptions once the repo has three installable Python packages. +`flit_core` remains sufficient for a single-package distribution with an optional extra — no need to move to a workspace-oriented build backend now that there's only one package to build. Revisit only if the repo later grows back into multiple installable packages (see above). ## Naming -- `skelly_sync_core` and `skelly_sync_api` for the two installable packages, avoiding a name clash with the existing `skelly_synchronize` PyPI package. -- `skelly_synchronize` continues as the umbrella/meta name for the project and the CLI entry point for continuity with existing users. -- Console scripts: `skelly-sync` (CLI, replaces `python -m skelly_synchronize`/`__main__.py`) and `skelly-sync-api` (launches the FastAPI server, wraps `uvicorn`). +- One PyPI distribution: `skelly_synchronize`, continuing the existing published name. +- Console scripts: `skelly-sync` (CLI, replaces `python -m skelly_synchronize`/`__main__.py`) and `skelly-sync-api` (launches the FastAPI server via `uvicorn`; only functional if the `[api]` extra is installed). ## Constants and configuration (resolves KI-15) diff --git a/docs/architecture/02-core-library.md b/docs/architecture/02-core-library.md index ad064d7..7eb35c1 100644 --- a/docs/architecture/02-core-library.md +++ b/docs/architecture/02-core-library.md @@ -1,4 +1,4 @@ -# Core Library Design (`skelly_sync_core`) +# Core Library Design (`skelly_synchronize.core`) ## Goals diff --git a/docs/architecture/03-api-design.md b/docs/architecture/03-api-design.md index 52bba6e..527ff7a 100644 --- a/docs/architecture/03-api-design.md +++ b/docs/architecture/03-api-design.md @@ -1,4 +1,4 @@ -# FastAPI Service Design (`skelly_sync_api`) +# FastAPI Service Design (`skelly_synchronize.api`) ## Deployment model @@ -20,7 +20,7 @@ class Job(BaseModel): progress_message: str | None = None created_at: datetime updated_at: datetime - request: SyncRequest # reused directly from skelly_sync_core + request: SyncRequest # reused directly from skelly_synchronize.core result: SyncResult | None = None error: str | None = None ``` @@ -47,7 +47,7 @@ For each job, `api` creates a `multiprocessing.Manager().dict()` and passes a ca ## Request/response schemas -`SyncRequest`, `Job`, and `SyncResult` are the `core` Pydantic models used directly as FastAPI request/response schemas — no separate duplicate schema layer. `skelly_sync_api/schemas.py` only defines API-specific wrapper types that have no equivalent in `core`, such as `JobCreateResponse`. +`SyncRequest`, `Job`, and `SyncResult` are the `core` Pydantic models used directly as FastAPI request/response schemas — no separate duplicate schema layer. `skelly_synchronize/api/schemas.py` only defines API-specific wrapper types that have no equivalent in `core`, such as `JobCreateResponse`. ## Error handling diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 1c4e07b..9a41d88 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -16,8 +16,8 @@ This directory describes the **target** architecture for the `skelly_synchronize |---|---| | [00-known-issues.md](00-known-issues.md) | Numbered `KI-##` list of current bugs/smells and where each is resolved — the checklist nothing should silently reproduce. | | [01-package-layout.md](01-package-layout.md) | Target repo/module structure, monorepo layout, packaging and build decisions. | -| [02-core-library.md](02-core-library.md) | `skelly_sync_core` design: typed data models, `VideoBackend` interface, pipeline abstraction, concurrency model. | -| [03-api-design.md](03-api-design.md) | `skelly_sync_api` design: FastAPI endpoints, schemas, job model, progress bridge. | +| [02-core-library.md](02-core-library.md) | `skelly_synchronize.core` design: typed data models, `VideoBackend` interface, pipeline abstraction, concurrency model. | +| [03-api-design.md](03-api-design.md) | `skelly_synchronize.api` design: FastAPI endpoints, schemas, job model, progress bridge. | | [04-frontend.md](04-frontend.md) | React app: screens, API client, state management approach. | | [05-testing-strategy.md](05-testing-strategy.md) | Test pyramid across `core`/`api`/frontend, fixture redesign, CI plan. | @@ -31,16 +31,16 @@ Read in the numbered order above — each later doc assumes the decisions made i └─────┬──────┘ │ HTTP (polling) ┌─────▼──────┐ - │ api │ FastAPI (skelly_sync_api) — job orchestration, HTTP boundary + │ api │ FastAPI (skelly_synchronize.api) — job orchestration, HTTP boundary └─────┬──────┘ │ Python calls ┌─────▼──────┐ ┌────────────┐ │ core │◄──────┤ cli │ both depend only on core - │(skelly_sync_core) └────────────┘ + │(skelly_synchronize.core) └──────┘ └────────────┘ ``` -`core` has no knowledge of `api` or `frontend`. `api` and `cli` are both thin consumers of `core`, so the sync engine is usable standalone (scriptable, embeddable in other tools) independent of whether the API/frontend exist at all. See [01-package-layout.md](01-package-layout.md) for the full package tree. +`core` has no knowledge of `api` or `frontend`. `api` and `cli` are both thin consumers of `core`, so the sync engine is usable standalone (scriptable, embeddable elsewhere within this same package) independent of whether the API/frontend exist at all. `core`, `api`, and `cli` all live in one PyPI distribution (`skelly_synchronize`), with `api`'s dependencies behind an optional extra — see [01-package-layout.md](01-package-layout.md) for the full package tree and the rationale for keeping this as one package for now. ## Non-goals @@ -55,7 +55,7 @@ The rewrite proceeds in phases. **Each phase ends with the tool still fully usab 1. **Phase 0 — scaffold `core`.** Stand up the new package layout, typed models, and the `VideoBackend` protocol with only the `FfmpegBackend` implementation. Port logic incrementally with unit tests. Keep the *old* `skelly_synchronize.py` entry points working by delegating internally to the new pipeline where practical, so the existing PySide6 GUI keeps functioning throughout this phase. 2. **Phase 1 — finish the `core` rewrite.** Complete the pipeline abstraction, the brightness path, the `DeffcodeBackend`, the audio subsystem, and debug artifacts. The old GUI now runs entirely against the new `core` library — this phase proves `core`'s public surface is sufficient before any API work begins. -3. **Phase 2 — FastAPI layer.** Build `skelly_sync_api` wrapping the now-finished `core`, starting with an in-memory job store. Test manually via the FastAPI-generated `/docs` UI — no frontend exists yet. +3. **Phase 2 — FastAPI layer.** Build `skelly_synchronize.api` wrapping the now-finished `core`, starting with an in-memory job store. Test manually via the FastAPI-generated `/docs` UI — no frontend exists yet. 4. **Phase 3 — React frontend.** Build the frontend against the FastAPI layer from Phase 2. Once it reaches feature parity with the old GUI, delete the PySide6 GUI package entirely. 5. **Phase 4 — cleanup.** Remove any remaining old dict-based code paths, finalize packaging/CI per [01-package-layout.md](01-package-layout.md) and [05-testing-strategy.md](05-testing-strategy.md), update the top-level README, and tag a release. @@ -64,8 +64,8 @@ The rewrite proceeds in phases. **Each phase ends with the tool still fully usab | Decision | Choice | Rationale (detail in linked doc) | |---|---|---| | Job storage | In-memory dict + lock, no SQLite | Jobs are ephemeral; durable output is the files on disk. [03-api-design.md](03-api-design.md) | -| `core` as a separate installable package | Yes — `skelly_sync_core` | Reusable independent of FastAPI/PySide6. [01-package-layout.md](01-package-layout.md) | -| Repo structure | Monorepo: `packages/core`, `packages/api`, `cli/`, `frontend/` | Single release cadence, no cross-repo coordination overhead. [01-package-layout.md](01-package-layout.md) | +| `core` as a separate installable package | No, for now — single `skelly_synchronize` distribution with `core`/`api`/`cli` as subpackages, `api` deps behind an optional extra | No consumer outside this repo needs to import `core` in-process yet; split out later if that changes. [01-package-layout.md](01-package-layout.md) | +| Repo structure | Monorepo: one Python package (`core`, `api`, `cli` subpackages) + `frontend/` | Single release cadence, no cross-repo or cross-package coordination overhead. [01-package-layout.md](01-package-layout.md) | | Frontend location | Same repo, `frontend/` | See above. [01-package-layout.md](01-package-layout.md) | | Progress reporting | `core` exposes a generic callback hook; `api` supplies the actual mechanism | Keeps `core` deployment-agnostic. [02-core-library.md](02-core-library.md), [03-api-design.md](03-api-design.md) | | Typed data model approach | Pydantic throughout `core`/`api` (raw audio arrays excluded) | Avoids a dataclass↔Pydantic translation layer since FastAPI already requires Pydantic. [02-core-library.md](02-core-library.md) | From 6cd6c44c814227501c09629d61f6e3ba427c281e Mon Sep 17 00:00:00 2001 From: philipqueen Date: Sun, 9 Aug 2026 14:41:50 -0600 Subject: [PATCH 03/16] further along --- pyproject.toml | 16 +- skelly_synchronize/__init__.py | 36 - .../{system => core}/__init__.py | 0 skelly_synchronize/core/audio.py | 115 + skelly_synchronize/core/backends/__init__.py | 0 skelly_synchronize/core/backends/base.py | 33 + skelly_synchronize/core/backends/deffcode.py | 132 ++ skelly_synchronize/core/backends/ffmpeg.py | 278 +++ skelly_synchronize/core/brightness.py | 88 + skelly_synchronize/core/config.py | 46 + skelly_synchronize/core/debug.py | 120 + skelly_synchronize/core/discovery.py | 23 + skelly_synchronize/core/exceptions.py | 22 + skelly_synchronize/core/logging_setup.py | 13 + skelly_synchronize/core/models.py | 56 + skelly_synchronize/core/pipeline/__init__.py | 0 skelly_synchronize/core/pipeline/runner.py | 81 + skelly_synchronize/core/pipeline/stages.py | 491 ++++ .../tests/core/backends/test_deffcode.py | 7 + .../tests/core/backends/test_ffmpeg.py | 71 + .../tests/core/pipeline/test_stages.py | 286 +++ skelly_synchronize/tests/core/test_audio.py | 88 + .../tests/core/test_brightness.py | 44 + skelly_synchronize/tests/core/test_config.py | 9 + skelly_synchronize/tests/core/test_debug.py | 57 + skelly_synchronize/tests/core/test_models.py | 39 + skelly_synchronize_old/__init__.py | 38 + .../__main__.py | 0 .../core_processes/audio_utilities.py | 6 +- .../core_processes/correlation_functions.py | 4 +- .../core_processes/debugging/debug_output.py | 0 .../core_processes/debugging/debug_plots.py | 4 +- .../core_processes/normalize_framerates.py | 8 +- .../video_functions/deffcode_functions.py | 2 +- .../video_functions/ffmpeg_functions.py | 2 +- .../video_functions/video_utilities.py | 12 +- .../gui/skelly_synchronize_gui.py | 2 +- .../gui/widgets/run_button_widget.py | 0 .../skelly_synchronize.py | 24 +- skelly_synchronize_old/system/__init__.py | 0 .../system/default_paths.py | 2 +- .../system/file_extensions.py | 0 .../system/logging_configuration.py | 0 .../system/paths_and_file_names.py | 0 .../tests/conftest.py | 6 +- .../tests/test_all_files_created.py | 2 +- .../tests/test_normalize_lag_dict.py | 2 +- .../test_number_of_videos_is_preserved.py | 2 +- .../tests/test_trim_single_video_deffcode.py | 4 +- .../tests/test_videos_are_same_length.py | 4 +- .../utilities/check_list_values_are_equal.py | 0 .../utilities/find_frame_count_of_video.py | 0 ..._number_of_frames_of_videos_in_a_folder.py | 4 +- .../tests/utilities/load_sample_data.py | 2 +- .../utils/get_video_files.py | 2 +- .../utils/path_handling_utilities.py | 4 +- uv.lock | 2050 +++++++++-------- 57 files changed, 3261 insertions(+), 1076 deletions(-) rename skelly_synchronize/{system => core}/__init__.py (100%) create mode 100644 skelly_synchronize/core/audio.py create mode 100644 skelly_synchronize/core/backends/__init__.py create mode 100644 skelly_synchronize/core/backends/base.py create mode 100644 skelly_synchronize/core/backends/deffcode.py create mode 100644 skelly_synchronize/core/backends/ffmpeg.py create mode 100644 skelly_synchronize/core/brightness.py create mode 100644 skelly_synchronize/core/config.py create mode 100644 skelly_synchronize/core/debug.py create mode 100644 skelly_synchronize/core/discovery.py create mode 100644 skelly_synchronize/core/exceptions.py create mode 100644 skelly_synchronize/core/logging_setup.py create mode 100644 skelly_synchronize/core/models.py create mode 100644 skelly_synchronize/core/pipeline/__init__.py create mode 100644 skelly_synchronize/core/pipeline/runner.py create mode 100644 skelly_synchronize/core/pipeline/stages.py create mode 100644 skelly_synchronize/tests/core/backends/test_deffcode.py create mode 100644 skelly_synchronize/tests/core/backends/test_ffmpeg.py create mode 100644 skelly_synchronize/tests/core/pipeline/test_stages.py create mode 100644 skelly_synchronize/tests/core/test_audio.py create mode 100644 skelly_synchronize/tests/core/test_brightness.py create mode 100644 skelly_synchronize/tests/core/test_config.py create mode 100644 skelly_synchronize/tests/core/test_debug.py create mode 100644 skelly_synchronize/tests/core/test_models.py create mode 100644 skelly_synchronize_old/__init__.py rename {skelly_synchronize => skelly_synchronize_old}/__main__.py (100%) rename {skelly_synchronize => skelly_synchronize_old}/core_processes/audio_utilities.py (93%) rename {skelly_synchronize => skelly_synchronize_old}/core_processes/correlation_functions.py (97%) rename {skelly_synchronize => skelly_synchronize_old}/core_processes/debugging/debug_output.py (100%) rename {skelly_synchronize => skelly_synchronize_old}/core_processes/debugging/debug_plots.py (96%) rename {skelly_synchronize => skelly_synchronize_old}/core_processes/normalize_framerates.py (80%) rename {skelly_synchronize => skelly_synchronize_old}/core_processes/video_functions/deffcode_functions.py (96%) rename {skelly_synchronize => skelly_synchronize_old}/core_processes/video_functions/ffmpeg_functions.py (99%) rename {skelly_synchronize => skelly_synchronize_old}/core_processes/video_functions/video_utilities.py (93%) rename {skelly_synchronize => skelly_synchronize_old}/gui/skelly_synchronize_gui.py (98%) rename {skelly_synchronize => skelly_synchronize_old}/gui/widgets/run_button_widget.py (100%) rename {skelly_synchronize => skelly_synchronize_old}/skelly_synchronize.py (90%) create mode 100644 skelly_synchronize_old/system/__init__.py rename {skelly_synchronize => skelly_synchronize_old}/system/default_paths.py (96%) rename {skelly_synchronize => skelly_synchronize_old}/system/file_extensions.py (100%) rename {skelly_synchronize => skelly_synchronize_old}/system/logging_configuration.py (100%) rename {skelly_synchronize => skelly_synchronize_old}/system/paths_and_file_names.py (100%) rename {skelly_synchronize => skelly_synchronize_old}/tests/conftest.py (84%) rename {skelly_synchronize => skelly_synchronize_old}/tests/test_all_files_created.py (95%) rename {skelly_synchronize => skelly_synchronize_old}/tests/test_normalize_lag_dict.py (88%) rename {skelly_synchronize => skelly_synchronize_old}/tests/test_number_of_videos_is_preserved.py (85%) rename {skelly_synchronize => skelly_synchronize_old}/tests/test_trim_single_video_deffcode.py (80%) rename {skelly_synchronize => skelly_synchronize_old}/tests/test_videos_are_same_length.py (75%) rename {skelly_synchronize => skelly_synchronize_old}/tests/utilities/check_list_values_are_equal.py (100%) rename {skelly_synchronize => skelly_synchronize_old}/tests/utilities/find_frame_count_of_video.py (100%) rename {skelly_synchronize => skelly_synchronize_old}/tests/utilities/get_number_of_frames_of_videos_in_a_folder.py (88%) rename {skelly_synchronize => skelly_synchronize_old}/tests/utilities/load_sample_data.py (93%) rename {skelly_synchronize => skelly_synchronize_old}/utils/get_video_files.py (94%) rename {skelly_synchronize => skelly_synchronize_old}/utils/path_handling_utilities.py (88%) diff --git a/pyproject.toml b/pyproject.toml index a4e6b1b..4d6663d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "flit_core.buildapi" [project] name = "skelly_synchronize" -description = "Basic template of a python repository" +description = "Synchronize multi-camera video recordings post-recording, without needing timestamps." readme = "README.md" authors = [{ name = "skelly_synchronize", email = "info@freemocap.org" }] license = { file = "LICENSE" } @@ -17,14 +17,14 @@ classifiers = [ "Programming Language :: Python :: 3", ] #additional classifiers can be found here: https://pypi.org/classifiers/ -keywords = ["basic", - "template", - "python", - "repository"] #change these to your project keywords +keywords = ["video", + "synchronization", + "motion-capture", + "freemocap"] dependencies = [ +"pydantic>=2.0", "librosa==0.10.1", -"PySide6>=6.6, <6.8", "numpy==1.26.2", "scipy==1.11.4", "opencv-contrib-python==4.8.*", @@ -56,7 +56,6 @@ Homepage = "https://freemocap.org" Documentation = "https://freemocap.github.io/skelly_synchronize/" Github = "https://github.com/freemocap/skelly_synchronize" - [tool.bumpver] #bump the version by entering `bumpver update` in the terminal current_version = "v2025.04.1037" version_pattern = "vYYYY.0M.BUILD[-TAG]" @@ -68,6 +67,3 @@ push = true [tool.bumpver.file_patterns] "pyproject.toml" = ["{version}"] "skelly_synchronize/__init__.py" = ["{version}"] - -[project.scripts] -skelly_synchronize = "skelly_synchronize.__main__:run" diff --git a/skelly_synchronize/__init__.py b/skelly_synchronize/__init__.py index 905a4d1..7a8bfe5 100644 --- a/skelly_synchronize/__init__.py +++ b/skelly_synchronize/__init__.py @@ -1,38 +1,2 @@ -"""Top-level package for basic_template_repo.""" - __package_name__ = "skelly_synchronize" __version__ = "v2025.04.1037" - -__author__ = """Philip Queen""" -__email__ = "info@freemocap.org" -__repo_owner_github_user_name__ = "freemocap" -__repo_url__ = ( - f"https://github.com/{__repo_owner_github_user_name__}/{__package_name__}/" -) -__repo_issues_url__ = f"{__repo_url__}issues" - -import sys -from pathlib import Path - - -# print(f"Thank you for using {__package_name__}!") -# print(f"This is printing from: {__file__}") -# print(f"Source code for this package is available at: {__repo_url__}") - -base_package_path = Path(__file__).parent -# print(f"adding base_package_path: {base_package_path} : to sys.path") -sys.path.insert(0, str(base_package_path)) # add parent directory to sys.path - -from skelly_synchronize.system.default_paths import get_log_file_path -from skelly_synchronize.system.logging_configuration import configure_logging -from skelly_synchronize.skelly_synchronize import ( - synchronize_videos_from_audio, - synchronize_videos_from_brightness, -) -from skelly_synchronize.core_processes.debugging.debug_plots import ( - create_audio_debug_plots, - create_brightness_debug_plots, -) - - -configure_logging(log_file_path=str(get_log_file_path())) diff --git a/skelly_synchronize/system/__init__.py b/skelly_synchronize/core/__init__.py similarity index 100% rename from skelly_synchronize/system/__init__.py rename to skelly_synchronize/core/__init__.py diff --git a/skelly_synchronize/core/audio.py b/skelly_synchronize/core/audio.py new file mode 100644 index 0000000..d541404 --- /dev/null +++ b/skelly_synchronize/core/audio.py @@ -0,0 +1,115 @@ +import logging +from pathlib import Path + +import numpy as np +import soundfile as sf +from scipy import signal + +from skelly_synchronize.core.models import LagResult, VideoInfo + +logger = logging.getLogger(__name__) + + +def get_reference_camera_name(videos: list[VideoInfo]) -> str: + """Pick a deterministic reference camera for cross-correlation (resolves KI-13). + + Strategy: the camera with the longest recorded duration; ties (including + the degenerate all-equal case) resolve to sorted-camera-name order, which + stays deterministic either way. + """ + return sorted(videos, key=lambda v: (-v.duration_seconds, v.camera_name))[ + 0 + ].camera_name + + +def cross_correlate( + reference_signal: np.ndarray, other_signal: np.ndarray +) -> tuple[int, float]: + """Cross correlate two audio signals. + + Returns (lag_in_samples, confidence). `confidence` is the ratio of the peak + correlation magnitude to the mean correlation magnitude (peak sharpness) -- + a low ratio means the peak is not well distinguished from the noise floor + (resolves KI-14). + """ + correlation = signal.correlate( + reference_signal, other_signal, mode="full", method="fft" + ) + lags = signal.correlation_lags( + reference_signal.size, other_signal.size, mode="full" + ) + + peak_index = int(np.argmax(correlation)) + lag = int(lags[peak_index]) + + noise_floor = float(np.mean(np.abs(correlation))) + 1e-12 + confidence = float(np.abs(correlation[peak_index])) / noise_floor + + return lag, confidence + + +def find_cross_correlation_lags( + audio_signals: dict[str, np.ndarray], + videos: list[VideoInfo], + sample_rate: int, +) -> list[LagResult]: + """Cross correlate every camera's audio against a deterministic reference camera. + + Returns `LagResult`s satisfying the shared contract: `lag_seconds` is the + number of seconds to trim off the front of that video so all videos align, + normalized so the minimum lag is 0 (resolves KI-02 -- normalized once, + here, rather than left as an implicit downstream assumption). + """ + reference_camera_name = get_reference_camera_name(videos) + reference_signal = audio_signals[reference_camera_name] + + logger.info( + f"Using {reference_camera_name} as the cross-correlation reference camera" + ) + + raw_lags_seconds: dict[str, float] = {} + confidences: dict[str, float] = {} + for camera_name, camera_signal in audio_signals.items(): + lag_samples, confidence = cross_correlate(reference_signal, camera_signal) + raw_lags_seconds[camera_name] = lag_samples / sample_rate + confidences[camera_name] = confidence + + max_lag = max(raw_lags_seconds.values()) + + return [ + LagResult( + camera_name=camera_name, + lag_seconds=max_lag - raw_lag, + confidence=confidences[camera_name], + ) + for camera_name, raw_lag in raw_lags_seconds.items() + ] + + +def trim_audio_in_memory( + audio_signals: dict[str, np.ndarray], + sample_rate: int, + lags_by_camera: dict[str, LagResult], + synced_length_seconds: float, + output_folder: Path, +) -> dict[str, Path]: + """Trim already-loaded audio signals to the synchronized window. + + Reuses the in-memory signals from extraction instead of reloading each + `.wav` from disk (resolves KI-12). + """ + output_folder = Path(output_folder) + output_folder.mkdir(parents=True, exist_ok=True) + + length_in_samples = int(synced_length_seconds * sample_rate) + + output_paths: dict[str, Path] = {} + for camera_name, camera_signal in audio_signals.items(): + lag_in_samples = int(lags_by_camera[camera_name].lag_seconds * sample_rate) + trimmed_signal = camera_signal[lag_in_samples:][:length_in_samples] + + output_path = output_folder / f"{camera_name}.wav" + sf.write(output_path, trimmed_signal, sample_rate, subtype="PCM_24") + output_paths[camera_name] = output_path + + return output_paths diff --git a/skelly_synchronize/core/backends/__init__.py b/skelly_synchronize/core/backends/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/skelly_synchronize/core/backends/base.py b/skelly_synchronize/core/backends/base.py new file mode 100644 index 0000000..3a9d193 --- /dev/null +++ b/skelly_synchronize/core/backends/base.py @@ -0,0 +1,33 @@ +from pathlib import Path +from typing import Protocol + +from skelly_synchronize.core.models import VideoBackendKind, VideoInfo + + +class VideoBackend(Protocol): + def probe(self, filepath: Path) -> VideoInfo: ... + + def trim( + self, + filepath: Path, + start_seconds: float, + end_seconds: float | None, + output_path: Path, + ) -> Path: ... + + +def get_backend(kind: VideoBackendKind) -> VideoBackend: + """Construct a `VideoBackend` for the given kind. + + Only `VideoBackendKind` (a picklable `str` enum) should ever be passed + into multiprocessing worker processes -- never a backend instance. + """ + if kind == VideoBackendKind.FFMPEG: + from skelly_synchronize.core.backends.ffmpeg import FfmpegBackend + + return FfmpegBackend() + if kind == VideoBackendKind.DEFFCODE: + from skelly_synchronize.core.backends.deffcode import DeffcodeBackend + + return DeffcodeBackend() + raise ValueError(f"Unknown video backend kind: {kind}") diff --git a/skelly_synchronize/core/backends/deffcode.py b/skelly_synchronize/core/backends/deffcode.py new file mode 100644 index 0000000..db670ba --- /dev/null +++ b/skelly_synchronize/core/backends/deffcode.py @@ -0,0 +1,132 @@ +import json +import logging +from pathlib import Path + +import cv2 +from deffcode import FFdecoder, Sourcer + +from skelly_synchronize.core.backends.ffmpeg import FfmpegBackend, check_for_ffmpeg +from skelly_synchronize.core.exceptions import SkellySyncError +from skelly_synchronize.core.models import VideoInfo + +logger = logging.getLogger(__name__) + +# deffcode/ffmpeg auto-rotation combined with OpenCV's VideoWriter can double-apply +# rotation, so orientation-tagged sources need an explicit transpose filter instead. +TRANSPOSITION_FILTERS = { + 90.0: "transpose=cclock", + -270.0: "transpose=cclock", + -90.0: "transpose=clock", + 270.0: "transpose=clock", + 180.0: "transpose=cclock,transpose=cclock", + -180.0: "transpose=clock,transpose=clock", +} + + +class DeffcodeBackend: + """`VideoBackend` implementation that uses deffcode for frame-accurate trimming.""" + + def __init__(self) -> None: + self._ffmpeg_backend = FfmpegBackend() + + def probe(self, filepath: Path) -> VideoInfo: + """Probing always delegates to ffmpeg/ffprobe (resolves KI-03).""" + return self._ffmpeg_backend.probe(filepath) + + def trim( + self, + filepath: Path, + start_seconds: float, + end_seconds: float | None, + output_path: Path, + ) -> Path: + filepath = Path(filepath) + output_path = Path(output_path) + video_info = self.probe(filepath) + + start_frame = int(start_seconds * video_info.fps) + end_frame = ( + int(end_seconds * video_info.fps) if end_seconds is not None else None + ) + + _trim_frames_with_deffcode( + input_video_path=filepath, + start_frame=start_frame, + end_frame=end_frame, + output_path=output_path, + ) + return output_path + + +def _get_transpose_ffparams(input_video_path: Path, ffmpeg_location: str) -> dict: + sourcer = Sourcer( + source=str(input_video_path), custom_ffmpeg=ffmpeg_location + ).probe_stream() + orientation = sourcer.retrieve_metadata()["source_video_orientation"] + + if orientation == 0: + return {} + + logger.info("Video has reversed metadata, changing FFmpeg transpose argument") + return { + "-ffprefixes": ["-noautorotate"], + "-vf": TRANSPOSITION_FILTERS[orientation], + } + + +def _trim_frames_with_deffcode( + input_video_path: Path, + start_frame: int, + end_frame: int | None, + output_path: Path, +) -> None: + try: + ffmpeg_location = check_for_ffmpeg() + except FileNotFoundError: + ffmpeg_location = "" + + ffparams = _get_transpose_ffparams(input_video_path, ffmpeg_location) + + decoder = FFdecoder( + str(input_video_path), + frame_format="bgr24", + custom_ffmpeg=ffmpeg_location, + verbose=False, + **ffparams, + ).formulate() + + metadata = json.loads(decoder.metadata) + fourcc = cv2.VideoWriter.fourcc(*"mp4v") + framerate = metadata["output_framerate"] + framesize = tuple(metadata["output_frames_resolution"]) + + video_writer = cv2.VideoWriter(str(output_path), fourcc, framerate, framesize) + + try: + current_frame = 0 + written_frames = 0 + for frame in decoder.generateFrame(): + if frame is None: + break + + # frames requested are always a contiguous range, so a simple + # bounds check is O(1) per frame instead of an O(n) list scan (KI-06). + if current_frame >= start_frame and ( + end_frame is None or current_frame < end_frame + ): + video_writer.write(frame) + written_frames += 1 + + if end_frame is not None and current_frame >= end_frame - 1: + break + + current_frame += 1 + finally: + decoder.terminate() + video_writer.release() + + if written_frames == 0: + raise SkellySyncError( + f"No frames written when trimming {input_video_path} " + f"(requested frames [{start_frame}, {end_frame}))" + ) diff --git a/skelly_synchronize/core/backends/ffmpeg.py b/skelly_synchronize/core/backends/ffmpeg.py new file mode 100644 index 0000000..a370db9 --- /dev/null +++ b/skelly_synchronize/core/backends/ffmpeg.py @@ -0,0 +1,278 @@ +import logging +import shutil +import subprocess +from pathlib import Path + +from skelly_synchronize.core.config import ( + FFMPEG_ENCODE_SUBPROCESS_TIMEOUT_SECONDS, + FFMPEG_SUBPROCESS_TIMEOUT_SECONDS, +) +from skelly_synchronize.core.exceptions import BackendSubprocessError, VideoProbeError +from skelly_synchronize.core.models import VideoInfo + +logger = logging.getLogger(__name__) + +FFMPEG_EXECUTABLE = "ffmpeg" +FFPROBE_EXECUTABLE = "ffprobe" + + +def check_for_ffmpeg() -> str: + ffmpeg_pathstring = shutil.which(FFMPEG_EXECUTABLE) + if ffmpeg_pathstring is None: + raise FileNotFoundError( + "ffmpeg not found, please install ffmpeg and add it to your PATH" + ) + return ffmpeg_pathstring + + +def check_for_ffprobe() -> str: + ffprobe_pathstring = shutil.which(FFPROBE_EXECUTABLE) + if ffprobe_pathstring is None: + raise FileNotFoundError( + "ffprobe not found, please install ffmpeg and add it to your PATH" + ) + return ffprobe_pathstring + + +def _parse_ffmpeg_output(output: str, filepath: Path) -> float: + cleaned_out = ( + str(output) + .replace("b'", "") + .replace("'", "") + .replace("\\n", "") + .replace("\\r", "") + .replace("\\t", "") + .replace("\\", "") + ) + + try: + return float(cleaned_out) + except (ValueError, RuntimeError): + split_str = cleaned_out.split("/") + if len(split_str) == 2: + return float(int(split_str[0])) / float(split_str[1]) + raise VideoProbeError( + f"Unable to parse ffprobe output {output!r} for video {filepath}" + ) + + +def _run_subprocess( + command: list[str], timeout: float = FFMPEG_SUBPROCESS_TIMEOUT_SECONDS +) -> subprocess.CompletedProcess: + try: + return subprocess.run( + command, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + timeout=timeout, + ) + except subprocess.TimeoutExpired as e: + raise BackendSubprocessError( + f"Command timed out after {timeout}s: {' '.join(command)}", + stderr=str(e.stderr or ""), + returncode=None, + timed_out=True, + ) + + +class FfmpegBackend: + """`VideoBackend` implementation that shells out to the ffmpeg/ffprobe CLI.""" + + def _extract_video_duration(self, filepath: Path) -> float: + check_for_ffprobe() + command = [ + FFPROBE_EXECUTABLE, + "-v", + "error", + "-select_streams", + "v:0", + "-show_entries", + "format=duration", + "-of", + "default=noprint_wrappers=1:nokey=1", + str(filepath), + ] + result = _run_subprocess(command) + if result.returncode != 0: + raise BackendSubprocessError( + f"Failed to extract duration for video {filepath}", + stderr=result.stderr.decode(errors="replace"), + returncode=result.returncode, + ) + return _parse_ffmpeg_output(result.stdout, filepath) + + def _extract_video_fps(self, filepath: Path) -> float: + check_for_ffprobe() + command = [ + FFPROBE_EXECUTABLE, + "-v", + "error", + "-select_streams", + "v:0", + "-show_entries", + "stream=r_frame_rate", + "-of", + "default=noprint_wrappers=1:nokey=1", + str(filepath), + ] + result = _run_subprocess(command) + if result.returncode != 0: + raise BackendSubprocessError( + f"Failed to extract fps for video {filepath}", + stderr=result.stderr.decode(errors="replace"), + returncode=result.returncode, + ) + return _parse_ffmpeg_output(result.stdout, filepath) + + def probe(self, filepath: Path) -> VideoInfo: + """Probe video metadata using ffprobe. + + Probing always uses ffmpeg/ffprobe regardless of which backend is + selected for trimming -- ffprobe is more complete/reliable for + metadata than deffcode (resolves KI-03). + """ + filepath = Path(filepath) + duration_seconds = self._extract_video_duration(filepath) + fps = self._extract_video_fps(filepath) + return VideoInfo( + filepath=filepath, + camera_name=filepath.stem, + duration_seconds=duration_seconds, + fps=fps, + ) + + def trim( + self, + filepath: Path, + start_seconds: float, + end_seconds: float | None, + output_path: Path, + ) -> Path: + check_for_ffmpeg() + filepath = Path(filepath) + output_path = Path(output_path) + + command = [ + FFMPEG_EXECUTABLE, + "-i", + str(filepath), + "-ss", + str(start_seconds), + ] + if end_seconds is not None: + command.extend(["-t", str(end_seconds - start_seconds)]) + command.extend(["-y", str(output_path)]) + + result = _run_subprocess(command) + if result.returncode != 0: + raise BackendSubprocessError( + f"Failed to trim video {filepath}", + stderr=result.stderr.decode(errors="replace"), + returncode=result.returncode, + ) + return output_path + + +def extract_audio_sample_rate(filepath: Path) -> int: + """Get the audio sample rate of a video file's audio stream via ffprobe.""" + check_for_ffprobe() + command = [ + FFPROBE_EXECUTABLE, + "-v", + "error", + "-select_streams", + "a:0", + "-show_entries", + "stream=sample_rate", + "-of", + "default=noprint_wrappers=1:nokey=1", + str(filepath), + ] + result = _run_subprocess(command) + if result.returncode != 0: + raise BackendSubprocessError( + f"Failed to extract audio sample rate for video {filepath}", + stderr=result.stderr.decode(errors="replace"), + returncode=result.returncode, + ) + if not result.stdout.strip(): + raise VideoProbeError( + f"No audio stream found for video {filepath}, ensure video has audio" + ) + return int(_parse_ffmpeg_output(result.stdout, filepath)) + + +def extract_audio(filepath: Path, output_path: Path) -> Path: + """Extract the audio track of a video file into output_path via ffmpeg.""" + check_for_ffmpeg() + filepath = Path(filepath) + output_path = Path(output_path) + command = [FFMPEG_EXECUTABLE, "-y", "-i", str(filepath), str(output_path)] + result = _run_subprocess(command, timeout=FFMPEG_ENCODE_SUBPROCESS_TIMEOUT_SECONDS) + if result.returncode != 0: + raise BackendSubprocessError( + f"Failed to extract audio from video {filepath}, check that video has audio", + stderr=result.stderr.decode(errors="replace"), + returncode=result.returncode, + ) + return output_path + + +def normalize_framerate_and_sample_rate( + filepath: Path, + output_path: Path, + desired_fps: float, + desired_sample_rate: int, +) -> Path: + """Re-encode a video to a target fps and audio sample rate via ffmpeg.""" + check_for_ffmpeg() + filepath = Path(filepath) + output_path = Path(output_path) + command = [ + FFMPEG_EXECUTABLE, + "-i", + str(filepath), + "-r", + str(desired_fps), + "-ar", + str(desired_sample_rate), + "-y", + str(output_path), + ] + result = _run_subprocess(command, timeout=FFMPEG_ENCODE_SUBPROCESS_TIMEOUT_SECONDS) + if result.returncode != 0: + raise BackendSubprocessError( + f"Failed to normalize framerate/sample rate for video {filepath}", + stderr=result.stderr.decode(errors="replace"), + returncode=result.returncode, + ) + return output_path + + +def attach_audio(video_path: Path, audio_path: Path, output_path: Path) -> Path: + """Mux an audio file onto a video (re-encoding audio as AAC, copying video) via ffmpeg.""" + check_for_ffmpeg() + video_path = Path(video_path) + audio_path = Path(audio_path) + output_path = Path(output_path) + command = [ + FFMPEG_EXECUTABLE, + "-i", + str(video_path), + "-i", + str(audio_path), + "-c:v", + "copy", + "-c:a", + "aac", + "-y", + str(output_path), + ] + result = _run_subprocess(command, timeout=FFMPEG_ENCODE_SUBPROCESS_TIMEOUT_SECONDS) + if result.returncode != 0: + raise BackendSubprocessError( + f"Failed to attach audio {audio_path} to video {video_path}", + stderr=result.stderr.decode(errors="replace"), + returncode=result.returncode, + ) + return output_path diff --git a/skelly_synchronize/core/brightness.py b/skelly_synchronize/core/brightness.py new file mode 100644 index 0000000..0429e13 --- /dev/null +++ b/skelly_synchronize/core/brightness.py @@ -0,0 +1,88 @@ +import logging +from pathlib import Path + +import cv2 +import numpy as np +from pydantic import BaseModel + +from skelly_synchronize.core.models import VideoInfo + +logger = logging.getLogger(__name__) + + +class BrightnessEvent(BaseModel): + frame_index: int + lag_seconds: float + + +def compute_brightness_series(video: VideoInfo) -> np.ndarray: + """Compute the mean grayscale brightness of every frame in a video. + + Pure function -- no file I/O side effects (resolves KI-07). Persisting the + result is a separate, explicit call to `save_brightness_series`. + """ + capture = cv2.VideoCapture(str(video.filepath)) + try: + frame_count = int(capture.get(cv2.CAP_PROP_FRAME_COUNT)) + brightness = np.zeros(frame_count) + + for frame_index in range(frame_count): + read_ok, frame = capture.read() + if not read_ok or frame is None: + brightness = brightness[:frame_index] + break + gray_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) + brightness[frame_index] = np.mean(gray_frame) + finally: + capture.release() + + return brightness + + +def save_brightness_series(series: np.ndarray, output_path: Path) -> Path: + """Persist a brightness series to a `.npy` sidecar. Explicit, opt-in (KI-07).""" + output_path = Path(output_path) + np.save(output_path, series) + return output_path + + +def find_first_brightness_change( + brightness_series: np.ndarray, + fps: float, + brightness_ratio_threshold: float = 1000.0, +) -> BrightnessEvent | None: + """Find the first frame with a significant brightness change (e.g. a flash). + + Returns an explicit `BrightnessEvent | None` instead of relying on an + index-0 fallback as an implicit "not found" sentinel (resolves KI-08). + """ + if brightness_series.size == 0: + return None + + brightness_difference = np.diff(brightness_series, prepend=brightness_series[0]) + brightness_double_difference = np.diff( + brightness_difference, prepend=brightness_difference[0] + ) + combined_brightness_metric = brightness_difference * brightness_double_difference + + crossings = np.flatnonzero(combined_brightness_metric >= brightness_ratio_threshold) + if crossings.size > 0: + frame_index = int(crossings[0]) + logger.info(f"First brightness change detected at frame {frame_index}") + return BrightnessEvent(frame_index=frame_index, lag_seconds=frame_index / fps) + + # no crossing exceeded the threshold -- fall back to the sharpest detected + # change, but only if there actually was one (a genuinely flat/no-op + # series has no brightness event to report). + fallback_frame_index = int(np.argmax(brightness_double_difference)) + if brightness_double_difference[fallback_frame_index] <= 0: + logger.info("No brightness change detected in video") + return None + + logger.info( + "No brightness change exceeded threshold, " + f"defaulting to fastest detected change at frame {fallback_frame_index}" + ) + return BrightnessEvent( + frame_index=fallback_frame_index, lag_seconds=fallback_frame_index / fps + ) diff --git a/skelly_synchronize/core/config.py b/skelly_synchronize/core/config.py new file mode 100644 index 0000000..7835a29 --- /dev/null +++ b/skelly_synchronize/core/config.py @@ -0,0 +1,46 @@ +from enum import Enum + +# directory names +SYNCHRONIZED_VIDEOS_FOLDER_NAME = "synchronized_videos" +RAW_VIDEOS_FOLDER_NAME = "raw_videos" +AUDIO_FILES_FOLDER_NAME = "audio_files" +TRIMMED_AUDIO_FOLDER_NAME = "trimmed_audio" +NORMALIZED_VIDEOS_FOLDER_NAME = "normalized_videos" + +# file names +DEBUG_TOML_NAME = "synchronization_debug.toml" +DEBUG_PLOT_NAME = "debug_plot.png" +NUMPY_EXTENSION = "npy" + +# naming conventions +SYNCED_VIDEO_PRECURSOR = "synced_" +RAW_VIDEO_PREFIX = "raw_" +BRIGHTNESS_SUFFIX = "_brightness" + +# audio defaults +STANDARD_AUDIO_SAMPLE_RATE = 44100 + +# subprocess defaults +FFMPEG_SUBPROCESS_TIMEOUT_SECONDS = 30 +FFMPEG_ENCODE_SUBPROCESS_TIMEOUT_SECONDS = 1800 + + +class AudioExtension(Enum): + WAV = "wav" + FLAC = "flac" + MP3 = "mp3" + AAC = "aac" + + +class VideoExtension(Enum): + MP4 = "mp4" + MKV = "mkv" + AVI = "avi" + MPEG = "mpeg" + MOV = "mov" + + +def synced_video_filename(raw_video_filename: str) -> str: + """Take a raw video filename, strip the raw prefix if present, and return the synced video filename.""" + stripped_name = str(raw_video_filename).removeprefix(RAW_VIDEO_PREFIX) + return f"{SYNCED_VIDEO_PRECURSOR}{stripped_name}.{VideoExtension.MP4.value}" diff --git a/skelly_synchronize/core/debug.py b/skelly_synchronize/core/debug.py new file mode 100644 index 0000000..b06444a --- /dev/null +++ b/skelly_synchronize/core/debug.py @@ -0,0 +1,120 @@ +import logging +from pathlib import Path + +import librosa +import numpy as np +import toml +from matplotlib import pyplot as plt + +from skelly_synchronize.core.models import LagResult, VideoInfo + +logger = logging.getLogger(__name__) + + +def save_debug_toml( + output_path: Path, + videos_before: list[VideoInfo], + videos_after: list[VideoInfo], + lags: list[LagResult], + synchronized_fps: float | None = None, + synchronized_frame_count: int | None = None, +) -> Path: + """Dump raw/synchronized video info and lag results to a TOML file for debugging. + + `synchronized_fps`/`synchronized_frame_count` are the single fps and frame + count shared by every synchronized video -- surfaced explicitly here + (rather than requiring a reader to cross-check every entry in + `synchronized_video_information`) since an exact frame-count match across + cameras is the core correctness guarantee synchronization is supposed to + provide. + """ + data = { + "raw_video_information": { + video.camera_name: video.model_dump(mode="json") for video in videos_before + }, + "synchronized_video_information": { + video.camera_name: video.model_dump(mode="json") for video in videos_after + }, + "lag_results": {lag.camera_name: lag.model_dump() for lag in lags}, + } + # toml has no null type, so an unknown value is an absent key, not a null value. + if synchronized_fps is not None: + data["synchronized_video_fps"] = synchronized_fps + if synchronized_frame_count is not None: + data["synchronized_video_frame_count"] = synchronized_frame_count + + output_path = Path(output_path) + with open(output_path, "w") as toml_file: + toml_file.write(toml.dumps(data)) + return output_path + + +def plot_audio_waveforms( + raw_audio_paths: list[Path], + trimmed_audio_paths: list[Path], + output_path: Path, +) -> Path: + fig, axs = plt.subplots(2, 1, sharex=True, sharey=True) + fig.suptitle("Audio Cross Correlation Debug") + + axs[0].set_ylabel("Amplitude") + axs[1].set_ylabel("Amplitude") + axs[1].set_xlabel("Time (s)") + axs[0].set_title("Before Cross Correlation") + axs[1].set_title("After Cross Correlation") + + for audio_path in raw_audio_paths: + audio_signal, sample_rate = librosa.load(path=audio_path, sr=None) + time = np.linspace(0, len(audio_signal) / sample_rate, num=len(audio_signal)) + axs[0].plot(time, audio_signal, alpha=0.4) + + for audio_path in trimmed_audio_paths: + audio_signal, sample_rate = librosa.load(path=audio_path, sr=None) + time = np.linspace(0, len(audio_signal) / sample_rate, num=len(audio_signal)) + axs[1].plot(time, audio_signal, alpha=0.4) + + output_path = Path(output_path) + logger.info(f"Saving debug plots to: {output_path}") + fig.savefig(output_path) + plt.close(fig) + return output_path + + +def plot_brightness_series( + before_series: dict[str, np.ndarray], + before_fps: dict[str, float], + after_series: dict[str, np.ndarray], + after_fps: dict[str, float], + output_path: Path, +) -> Path: + """Plot brightness-over-time before/after trimming. + + The x-axis is frame index divided by fps to show real elapsed time + (resolves KI-09 -- the old plot labeled the axis "Time (s)" but plotted + raw, unscaled frame indices). + """ + fig, axs = plt.subplots(2, 1, sharex=False, sharey=True) + fig.suptitle("Brightness Across Frames") + + axs[0].set_ylabel("Brightness") + axs[1].set_ylabel("Brightness") + axs[0].set_xlabel("Time (s)") + axs[1].set_xlabel("Time (s)") + axs[0].set_title("Before Trimming") + axs[1].set_title("After Trimming") + + for camera_name, brightness_array in before_series.items(): + fps = before_fps[camera_name] + time = np.arange(len(brightness_array)) / fps + axs[0].plot(time, brightness_array, alpha=0.5, label=camera_name) + + for camera_name, brightness_array in after_series.items(): + fps = after_fps[camera_name] + time = np.arange(len(brightness_array)) / fps + axs[1].plot(time, brightness_array, alpha=0.5, label=camera_name) + + output_path = Path(output_path) + logger.info(f"Saving debug plots to: {output_path}") + fig.savefig(output_path) + plt.close(fig) + return output_path diff --git a/skelly_synchronize/core/discovery.py b/skelly_synchronize/core/discovery.py new file mode 100644 index 0000000..1b0e00c --- /dev/null +++ b/skelly_synchronize/core/discovery.py @@ -0,0 +1,23 @@ +import logging +from pathlib import Path + +from skelly_synchronize.core.config import VideoExtension + +logger = logging.getLogger(__name__) + + +def get_video_file_list(folder_path: Path) -> list[Path]: + """Return a sorted list of unique video files in folder_path matching a supported extension.""" + folder_path = Path(folder_path) + video_filepaths: list[Path] = [] + for extension in VideoExtension: + video_filepaths.extend(folder_path.glob(f"*.{extension.value.upper()}")) + video_filepaths.extend(folder_path.glob(f"*.{extension.value.lower()}")) + + # glob behaves differently on windows vs mac/linux; de-duplicate paths that + # show up twice when the filesystem is case-insensitive. + unique_filepaths = list(dict.fromkeys(video_filepaths)) + + logger.info(f"{len(unique_filepaths)} videos found in folder {folder_path}") + + return sorted(unique_filepaths, key=lambda p: str(p).lower()) diff --git a/skelly_synchronize/core/exceptions.py b/skelly_synchronize/core/exceptions.py new file mode 100644 index 0000000..37dc7be --- /dev/null +++ b/skelly_synchronize/core/exceptions.py @@ -0,0 +1,22 @@ +class SkellySyncError(Exception): + """Base class for all errors raised by skelly_synchronize.core.""" + + +class VideoProbeError(SkellySyncError): + """Raised when video metadata (duration, fps, ...) cannot be determined.""" + + +class BackendSubprocessError(SkellySyncError): + """Raised when an ffmpeg/ffprobe subprocess invocation fails or times out.""" + + def __init__( + self, + message: str, + stderr: str = "", + returncode: int | None = None, + timed_out: bool = False, + ) -> None: + super().__init__(message, stderr, returncode, timed_out) + self.stderr = stderr + self.returncode = returncode + self.timed_out = timed_out diff --git a/skelly_synchronize/core/logging_setup.py b/skelly_synchronize/core/logging_setup.py new file mode 100644 index 0000000..f10d89c --- /dev/null +++ b/skelly_synchronize/core/logging_setup.py @@ -0,0 +1,13 @@ +import logging + +_DEFAULT_FORMAT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" + + +def configure_logging(level: int = logging.INFO) -> None: + """Configure process-wide logging. + + This is opt-in and must only be called once, from an application entry + point (the CLI or API `main.py`) -- never from inside `core` itself. + Library modules within `core` use `logging.getLogger(__name__)` only. + """ + logging.basicConfig(level=level, format=_DEFAULT_FORMAT) diff --git a/skelly_synchronize/core/models.py b/skelly_synchronize/core/models.py new file mode 100644 index 0000000..8f5496e --- /dev/null +++ b/skelly_synchronize/core/models.py @@ -0,0 +1,56 @@ +from enum import Enum +from pathlib import Path + +from pydantic import BaseModel + +CameraName = str # alias for clarity in signatures + + +class VideoBackendKind(str, Enum): + FFMPEG = "ffmpeg" + DEFFCODE = "deffcode" + + +class SyncMethod(str, Enum): + AUDIO = "audio cross-correlation" + BRIGHTNESS = "brightness change detection" + + +class VideoInfo(BaseModel): + filepath: Path + camera_name: str + duration_seconds: float + fps: float + frame_count: int | None = None + + +class AudioInfo(BaseModel): + filepath: Path + camera_name: str + sample_rate: int + duration_seconds: float + # raw signal (np.ndarray) is intentionally NOT a field here + + +class LagResult(BaseModel): + camera_name: str + lag_seconds: float + confidence: float | None = None + + +class SyncRequest(BaseModel): + raw_video_folder_path: Path + synchronized_video_folder_path: Path | None = None + method: SyncMethod + video_handler: VideoBackendKind = VideoBackendKind.DEFFCODE + brightness_ratio_threshold: float = 1000.0 # only used when method == BRIGHTNESS + create_debug_artifacts: bool = True + + +class SyncResult(BaseModel): + synchronized_video_folder_path: Path + videos_before: list[VideoInfo] + videos_after: list[VideoInfo] + lags: list[LagResult] + debug_artifact_paths: list[Path] + elapsed_seconds: float diff --git a/skelly_synchronize/core/pipeline/__init__.py b/skelly_synchronize/core/pipeline/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/skelly_synchronize/core/pipeline/runner.py b/skelly_synchronize/core/pipeline/runner.py new file mode 100644 index 0000000..3fe77a7 --- /dev/null +++ b/skelly_synchronize/core/pipeline/runner.py @@ -0,0 +1,81 @@ +import time + +from skelly_synchronize.core.models import SyncMethod, SyncRequest, SyncResult +from skelly_synchronize.core.pipeline.stages import ( + AudioLagStage, + BrightnessLagStage, + DebugArtifactsStage, + DiscoverVideosStage, + NormalizeFramerateStage, + PipelineContext, + PipelineStage, + ProbeStage, + ProgressCallback, + ReattachAudioStage, + SetupOutputFolderStage, + TrimStage, + VerifySynchronizedFrameCountStage, + VerifySynchronizedFramerateStage, +) + + +class SyncPipeline: + """Shared orchestration for both sync methods (resolves KI-04). + + `SyncPipeline.run` is the single public entry point into `core` -- CLI and + API callers both go through this rather than duplicating orchestration. + """ + + def __init__(self, stages: list[PipelineStage]) -> None: + self.stages = stages + + @classmethod + def for_request(cls, request: SyncRequest) -> "SyncPipeline": + stages: list[PipelineStage] = [ + SetupOutputFolderStage(), + DiscoverVideosStage(), + ProbeStage(), + NormalizeFramerateStage(), + ] + + if request.method == SyncMethod.AUDIO: + stages.append(AudioLagStage()) + else: + stages.append(BrightnessLagStage()) + + stages.append(TrimStage()) + stages.append(VerifySynchronizedFramerateStage()) + stages.append(VerifySynchronizedFrameCountStage()) + + if request.method == SyncMethod.AUDIO: + stages.append(ReattachAudioStage()) + + if request.create_debug_artifacts: + stages.append(DebugArtifactsStage()) + + return cls(stages) + + def run( + self, request: SyncRequest, progress_callback: ProgressCallback | None = None + ) -> SyncResult: + start_time = time.time() + context = PipelineContext(request=request) + + for stage in self.stages: + context = stage.run(context, progress_callback) + + return SyncResult( + synchronized_video_folder_path=context.synchronized_folder_path, + videos_before=context.videos_before, + videos_after=context.videos, + lags=context.lags, + debug_artifact_paths=context.debug_artifact_paths, + elapsed_seconds=time.time() - start_time, + ) + + +def run_pipeline( + request: SyncRequest, progress_callback: ProgressCallback | None = None +) -> SyncResult: + """Convenience wrapper: build the right stage list for `request` and run it.""" + return SyncPipeline.for_request(request).run(request, progress_callback) diff --git a/skelly_synchronize/core/pipeline/stages.py b/skelly_synchronize/core/pipeline/stages.py new file mode 100644 index 0000000..1a6ca17 --- /dev/null +++ b/skelly_synchronize/core/pipeline/stages.py @@ -0,0 +1,491 @@ +import logging +import os +import shutil +import tempfile +from concurrent.futures import ProcessPoolExecutor, as_completed +from pathlib import Path +from typing import Callable, Protocol + +import cv2 +import librosa +from pydantic import BaseModel, ConfigDict, Field + +from skelly_synchronize.core.audio import ( + find_cross_correlation_lags, + trim_audio_in_memory, +) +from skelly_synchronize.core.backends.base import get_backend +from skelly_synchronize.core.backends.ffmpeg import ( + FfmpegBackend, + attach_audio, + extract_audio, + extract_audio_sample_rate, + normalize_framerate_and_sample_rate, +) +from skelly_synchronize.core.brightness import ( + compute_brightness_series, + find_first_brightness_change, + save_brightness_series, +) +from skelly_synchronize.core.config import ( + AUDIO_FILES_FOLDER_NAME, + BRIGHTNESS_SUFFIX, + DEBUG_PLOT_NAME, + DEBUG_TOML_NAME, + NORMALIZED_VIDEOS_FOLDER_NAME, + NUMPY_EXTENSION, + STANDARD_AUDIO_SAMPLE_RATE, + SYNCHRONIZED_VIDEOS_FOLDER_NAME, + TRIMMED_AUDIO_FOLDER_NAME, + AudioExtension, + VideoExtension, + synced_video_filename, +) +from skelly_synchronize.core.debug import ( + plot_audio_waveforms, + plot_brightness_series, + save_debug_toml, +) +from skelly_synchronize.core.discovery import get_video_file_list +from skelly_synchronize.core.exceptions import SkellySyncError +from skelly_synchronize.core.models import ( + LagResult, + SyncMethod, + SyncRequest, + VideoBackendKind, + VideoInfo, +) + +logger = logging.getLogger(__name__) + +ProgressCallback = Callable[[str, float], None] + + +class PipelineContext(BaseModel): + model_config = ConfigDict(arbitrary_types_allowed=True) + + request: SyncRequest + synchronized_folder_path: Path | None = None + discovered_paths: list[Path] = Field(default_factory=list) + videos: list[VideoInfo] = Field(default_factory=list) + videos_before: list[VideoInfo] = Field(default_factory=list) + pre_trim_videos: list[VideoInfo] = Field(default_factory=list) + normalized_folder_path: Path | None = None + lags: list[LagResult] = Field(default_factory=list) + audio_folder_path: Path | None = None + audio_signals: dict | None = ( + None # dict[str, np.ndarray]; kept outside typed models + ) + audio_sample_rate: int | None = None + synchronized_fps: float | None = None + synchronized_frame_count: int | None = None + debug_artifact_paths: list[Path] = Field(default_factory=list) + + +class PipelineStage(Protocol): + def run( + self, context: PipelineContext, progress_callback: ProgressCallback | None + ) -> PipelineContext: ... + + +class SetupOutputFolderStage: + def run( + self, context: PipelineContext, progress_callback: ProgressCallback | None + ) -> PipelineContext: + request = context.request + if request.synchronized_video_folder_path is not None: + output_folder = Path(request.synchronized_video_folder_path) + else: + output_folder = ( + Path(request.raw_video_folder_path).parent + / SYNCHRONIZED_VIDEOS_FOLDER_NAME + ) + output_folder.mkdir(parents=True, exist_ok=True) + context.synchronized_folder_path = output_folder + return context + + +class DiscoverVideosStage: + def run( + self, context: PipelineContext, progress_callback: ProgressCallback | None + ) -> PipelineContext: + context.discovered_paths = get_video_file_list( + context.request.raw_video_folder_path + ) + return context + + +def _count_video_frames(filepath: Path) -> int: + """Read the actual frame count out of a video file's container metadata.""" + capture = cv2.VideoCapture(str(filepath)) + try: + return int(capture.get(cv2.CAP_PROP_FRAME_COUNT)) + finally: + capture.release() + + +def _probe_with_frame_count(backend, filepath: Path) -> VideoInfo: + """Probe a video and attach its real frame count. + + Frame count isn't part of what ffprobe's duration/fps query returns, so + it's filled in as a second, explicit step wherever a `VideoInfo` is + built -- for raw videos, normalized videos, and trimmed/synced videos + alike -- so every stage of the pipeline reports it consistently. + """ + video_info = backend.probe(filepath) + return video_info.model_copy(update={"frame_count": _count_video_frames(filepath)}) + + +class ProbeStage: + def run( + self, context: PipelineContext, progress_callback: ProgressCallback | None + ) -> PipelineContext: + backend = FfmpegBackend() + context.videos = [ + _probe_with_frame_count(backend, path) for path in context.discovered_paths + ] + context.videos_before = list(context.videos) + return context + + +class NormalizeFramerateStage: + """Conditional: only runs if fps (or, for audio, sample rate) diverges. + + Encapsulates the re-probe against its own output so callers never have to + manually re-run discovery/probing (resolves KI-05). + """ + + def run( + self, context: PipelineContext, progress_callback: ProgressCallback | None + ) -> PipelineContext: + videos = context.videos + fps_values = {video.fps for video in videos} + + sample_rates: set[int] | None = None + if context.request.method == SyncMethod.AUDIO: + sample_rates = { + extract_audio_sample_rate(video.filepath) for video in videos + } + + needs_normalize = len(fps_values) > 1 or ( + sample_rates is not None and len(sample_rates) > 1 + ) + if not needs_normalize: + return context + + target_fps = min(fps_values) + target_sample_rate = ( + int(min(sample_rates)) if sample_rates else STANDARD_AUDIO_SAMPLE_RATE + ) + + normalized_folder = ( + Path(context.request.raw_video_folder_path) / NORMALIZED_VIDEOS_FOLDER_NAME + ) + normalized_folder.mkdir(parents=True, exist_ok=True) + + for video in videos: + output_path = ( + normalized_folder / f"{video.camera_name}.{VideoExtension.MP4.value}" + ) + normalize_framerate_and_sample_rate( + video.filepath, output_path, target_fps, target_sample_rate + ) + + context.normalized_folder_path = normalized_folder + + backend = FfmpegBackend() + context.videos = [ + _probe_with_frame_count(backend, path) + for path in get_video_file_list(normalized_folder) + ] + return context + + +class AudioLagStage: + def run( + self, context: PipelineContext, progress_callback: ProgressCallback | None + ) -> PipelineContext: + audio_folder = context.synchronized_folder_path / AUDIO_FILES_FOLDER_NAME + audio_folder.mkdir(parents=True, exist_ok=True) + + audio_signals = {} + sample_rate = None + for video in context.videos: + audio_path = ( + audio_folder / f"{video.camera_name}.{AudioExtension.WAV.value}" + ) + extract_audio(video.filepath, audio_path) + camera_signal, sample_rate = librosa.load(path=audio_path, sr=None) + audio_signals[video.camera_name] = camera_signal + + context.pre_trim_videos = list(context.videos) + context.audio_folder_path = audio_folder + context.audio_signals = audio_signals + context.audio_sample_rate = int(sample_rate) + context.lags = find_cross_correlation_lags( + audio_signals, context.videos, int(sample_rate) + ) + return context + + +class BrightnessLagStage: + def run( + self, context: PipelineContext, progress_callback: ProgressCallback | None + ) -> PipelineContext: + threshold = context.request.brightness_ratio_threshold + context.pre_trim_videos = list(context.videos) + + raw_lags: dict[str, float] = {} + for video in context.videos: + series = compute_brightness_series(video) + event = find_first_brightness_change(series, video.fps, threshold) + if event is None: + raise SkellySyncError( + f"No brightness change detected for camera {video.camera_name}" + ) + raw_lags[video.camera_name] = event.lag_seconds + + max_lag = max(raw_lags.values()) + context.lags = [ + LagResult(camera_name=camera_name, lag_seconds=max_lag - raw_lag) + for camera_name, raw_lag in raw_lags.items() + ] + return context + + +def _trim_video_worker( + video_info: VideoInfo, + lag: LagResult, + backend_kind: VideoBackendKind, + output_dir: Path, + minimum_duration: float, +) -> VideoInfo: + """Runs in a worker process -- arguments must stay picklable. + + Only `VideoBackendKind` (a `str` enum) is passed, never a backend + instance, and probing the result always goes through ffmpeg (KI-03). + """ + backend = get_backend(backend_kind) + output_path = Path(output_dir) / synced_video_filename(video_info.camera_name) + + start_seconds = lag.lag_seconds + end_seconds = start_seconds + minimum_duration + backend.trim(video_info.filepath, start_seconds, end_seconds, output_path) + + return _probe_with_frame_count(get_backend(VideoBackendKind.FFMPEG), output_path) + + +class TrimStage: + """Trims every video to the shared window in parallel. + + Uses `ProcessPoolExecutor` + `as_completed` instead of + `multiprocessing.Pool.starmap` so a single camera's trim failure doesn't + hide which camera failed or block progress reporting for the others. + """ + + def run( + self, context: PipelineContext, progress_callback: ProgressCallback | None + ) -> PipelineContext: + videos = context.videos + lags_by_camera = {lag.camera_name: lag for lag in context.lags} + output_dir = context.synchronized_folder_path + + minimum_duration = min( + video.duration_seconds - lags_by_camera[video.camera_name].lag_seconds + for video in videos + ) + + max_workers = max(1, min(len(videos), (os.cpu_count() or 2) - 1)) + + results: list[VideoInfo] = [] + errors: list[tuple[str, BaseException]] = [] + with ProcessPoolExecutor(max_workers=max_workers) as executor: + future_to_camera = { + executor.submit( + _trim_video_worker, + video, + lags_by_camera[video.camera_name], + context.request.video_handler, + output_dir, + minimum_duration, + ): video.camera_name + for video in videos + } + for future in as_completed(future_to_camera): + camera_name = future_to_camera[future] + try: + results.append(future.result()) + if progress_callback is not None: + progress_callback(camera_name, 1.0) + except Exception as e: # noqa: BLE001 - isolate per-camera failures + logger.error( + f"Error trimming video {camera_name}: {e}", exc_info=True + ) + errors.append((camera_name, e)) + + if errors: + failed_cameras = ", ".join(camera_name for camera_name, _ in errors) + raise SkellySyncError( + f"Trimming failed for camera(s): {failed_cameras}" + ) from errors[0][1] + + context.videos = sorted(results, key=lambda video: video.camera_name) + return context + + +class VerifySynchronizedFramerateStage: + """Hard-fail if the trimmed videos don't all share one exact framerate. + + Synchronized videos are only actually synchronized if every frame index + maps to the same wall-clock time across cameras -- a framerate mismatch + (even a tiny one introduced by a backend's re-encode) silently breaks + that guarantee, so this is checked explicitly rather than assumed. + """ + + def run( + self, context: PipelineContext, progress_callback: ProgressCallback | None + ) -> PipelineContext: + fps_by_camera = {video.camera_name: video.fps for video in context.videos} + unique_fps_values = set(fps_by_camera.values()) + + if len(unique_fps_values) > 1: + raise SkellySyncError( + "Synchronized videos do not share an identical framerate: " + f"{fps_by_camera}" + ) + + context.synchronized_fps = ( + unique_fps_values.pop() if unique_fps_values else None + ) + return context + + +class VerifySynchronizedFrameCountStage: + """Hard-fail if the trimmed videos don't all have identical frame counts. + + This is the actual correctness guarantee of synchronization: if any + camera's output has even one more or fewer frames than the others, frame + N no longer corresponds to the same instant across cameras. Each video's + `frame_count` was already read directly from its file during probing + (`_probe_with_frame_count`), so this stage just compares those real, + already-measured counts rather than re-deriving anything from duration/fps + arithmetic. + """ + + def run( + self, context: PipelineContext, progress_callback: ProgressCallback | None + ) -> PipelineContext: + frame_counts_by_camera = { + video.camera_name: video.frame_count for video in context.videos + } + unique_frame_counts = set(frame_counts_by_camera.values()) + + if len(unique_frame_counts) > 1: + raise SkellySyncError( + "Synchronized videos do not have identical frame counts: " + f"{frame_counts_by_camera}" + ) + + context.synchronized_frame_count = ( + unique_frame_counts.pop() if unique_frame_counts else None + ) + return context + + +class ReattachAudioStage: + def run( + self, context: PipelineContext, progress_callback: ProgressCallback | None + ) -> PipelineContext: + lags_by_camera = {lag.camera_name: lag for lag in context.lags} + synced_length_seconds = ( + context.videos[0].duration_seconds if context.videos else 0.0 + ) + + trimmed_audio_folder = context.audio_folder_path / TRIMMED_AUDIO_FOLDER_NAME + trimmed_audio_paths = trim_audio_in_memory( + context.audio_signals, + context.audio_sample_rate, + lags_by_camera, + synced_length_seconds, + trimmed_audio_folder, + ) + + for lag in context.lags: + video_path = context.synchronized_folder_path / synced_video_filename( + lag.camera_name + ) + audio_path = trimmed_audio_paths[lag.camera_name] + + with tempfile.TemporaryDirectory( + dir=str(context.synchronized_folder_path) + ) as temp_dir: + temp_output_path = ( + Path(temp_dir) + / f"{video_path.stem}_with_audio.{VideoExtension.MP4.value}" + ) + attach_audio(video_path, audio_path, temp_output_path) + shutil.move(str(temp_output_path), str(video_path)) + + return context + + +class DebugArtifactsStage: + def run( + self, context: PipelineContext, progress_callback: ProgressCallback | None + ) -> PipelineContext: + synced_folder = context.synchronized_folder_path + artifact_paths = [] + + toml_path = save_debug_toml( + synced_folder / DEBUG_TOML_NAME, + videos_before=context.videos_before, + videos_after=context.videos, + lags=context.lags, + synchronized_fps=context.synchronized_fps, + synchronized_frame_count=context.synchronized_frame_count, + ) + artifact_paths.append(toml_path) + + plot_path = synced_folder / DEBUG_PLOT_NAME + if context.request.method == SyncMethod.AUDIO: + raw_audio_paths = sorted( + context.audio_folder_path.glob(f"*.{AudioExtension.WAV.value}") + ) + trimmed_audio_paths = sorted( + (context.audio_folder_path / TRIMMED_AUDIO_FOLDER_NAME).glob( + f"*.{AudioExtension.WAV.value}" + ) + ) + plot_audio_waveforms(raw_audio_paths, trimmed_audio_paths, plot_path) + else: + # Whether debug data comes from the raw or normalized folder is + # resolved from pipeline state, not a filesystem existence check + # (resolves KI-10). + source_folder = context.normalized_folder_path or Path( + context.request.raw_video_folder_path + ) + + before_series = { + video.camera_name: compute_brightness_series(video) + for video in context.pre_trim_videos + } + after_series = { + video.camera_name: compute_brightness_series(video) + for video in context.videos + } + for camera_name, series in before_series.items(): + save_brightness_series( + series, + source_folder + / f"{camera_name}{BRIGHTNESS_SUFFIX}.{NUMPY_EXTENSION}", + ) + + before_fps = {v.camera_name: v.fps for v in context.pre_trim_videos} + after_fps = {v.camera_name: v.fps for v in context.videos} + plot_brightness_series( + before_series, before_fps, after_series, after_fps, plot_path + ) + artifact_paths.append(plot_path) + + context.debug_artifact_paths = artifact_paths + return context diff --git a/skelly_synchronize/tests/core/backends/test_deffcode.py b/skelly_synchronize/tests/core/backends/test_deffcode.py new file mode 100644 index 0000000..43b347a --- /dev/null +++ b/skelly_synchronize/tests/core/backends/test_deffcode.py @@ -0,0 +1,7 @@ +from skelly_synchronize.core.backends.deffcode import TRANSPOSITION_FILTERS + + +def test_transposition_filters_cover_all_common_orientations(): + for orientation in (90.0, -270.0, -90.0, 270.0, 180.0, -180.0): + assert orientation in TRANSPOSITION_FILTERS + assert "transpose" in TRANSPOSITION_FILTERS[orientation] diff --git a/skelly_synchronize/tests/core/backends/test_ffmpeg.py b/skelly_synchronize/tests/core/backends/test_ffmpeg.py new file mode 100644 index 0000000..d9c3efa --- /dev/null +++ b/skelly_synchronize/tests/core/backends/test_ffmpeg.py @@ -0,0 +1,71 @@ +import subprocess +from pathlib import Path + +import pytest + +from skelly_synchronize.core.backends.ffmpeg import FfmpegBackend +from skelly_synchronize.core.exceptions import BackendSubprocessError + + +class FakeCompletedProcess: + def __init__(self, returncode: int, stdout: bytes = b"", stderr: bytes = b""): + self.returncode = returncode + self.stdout = stdout + self.stderr = stderr + + +@pytest.fixture +def backend(monkeypatch): + monkeypatch.setattr( + "skelly_synchronize.core.backends.ffmpeg.shutil.which", + lambda _: "/usr/bin/ffprobe", + ) + return FfmpegBackend() + + +def test_probe_parses_duration_and_fps(monkeypatch, backend): + responses = iter( + [ + FakeCompletedProcess(returncode=0, stdout=b"12.5"), + FakeCompletedProcess(returncode=0, stdout=b"30/1"), + ] + ) + monkeypatch.setattr( + "skelly_synchronize.core.backends.ffmpeg.subprocess.run", + lambda *args, **kwargs: next(responses), + ) + + video_info = backend.probe(Path("some_video.mp4")) + + assert video_info.camera_name == "some_video" + assert video_info.duration_seconds == 12.5 + assert video_info.fps == 30.0 + + +def test_probe_raises_backend_subprocess_error_on_failure(monkeypatch, backend): + monkeypatch.setattr( + "skelly_synchronize.core.backends.ffmpeg.subprocess.run", + lambda *args, **kwargs: FakeCompletedProcess( + returncode=1, stderr=b"no such file" + ), + ) + + with pytest.raises(BackendSubprocessError) as exc_info: + backend.probe(Path("missing_video.mp4")) + + assert exc_info.value.stderr == "no such file" + assert exc_info.value.returncode == 1 + + +def test_run_subprocess_raises_on_timeout(monkeypatch, backend): + def raise_timeout(*args, **kwargs): + raise subprocess.TimeoutExpired(cmd="ffprobe", timeout=30) + + monkeypatch.setattr( + "skelly_synchronize.core.backends.ffmpeg.subprocess.run", raise_timeout + ) + + with pytest.raises(BackendSubprocessError) as exc_info: + backend.probe(Path("slow_video.mp4")) + + assert exc_info.value.timed_out is True diff --git a/skelly_synchronize/tests/core/pipeline/test_stages.py b/skelly_synchronize/tests/core/pipeline/test_stages.py new file mode 100644 index 0000000..9926e61 --- /dev/null +++ b/skelly_synchronize/tests/core/pipeline/test_stages.py @@ -0,0 +1,286 @@ +import concurrent.futures +from pathlib import Path + +import numpy as np +import pytest + +from skelly_synchronize.core.exceptions import SkellySyncError +from skelly_synchronize.core.models import ( + LagResult, + SyncMethod, + SyncRequest, + VideoBackendKind, + VideoInfo, +) +from skelly_synchronize.core.pipeline import stages as stages_module +from skelly_synchronize.core.pipeline.stages import ( + BrightnessLagStage, + NormalizeFramerateStage, + PipelineContext, + SetupOutputFolderStage, + TrimStage, + VerifySynchronizedFrameCountStage, + VerifySynchronizedFramerateStage, + _probe_with_frame_count, + _trim_video_worker, +) + + +def _make_video_info( + camera_name: str, + fps: float = 30.0, + duration_seconds: float = 10.0, + frame_count: int | None = None, +): + return VideoInfo( + filepath=Path(f"{camera_name}.mp4"), + camera_name=camera_name, + duration_seconds=duration_seconds, + fps=fps, + frame_count=frame_count, + ) + + +def _make_request( + tmp_path: Path, method: SyncMethod = SyncMethod.BRIGHTNESS +) -> SyncRequest: + raw_folder = tmp_path / "raw_videos" + raw_folder.mkdir() + return SyncRequest(raw_video_folder_path=raw_folder, method=method) + + +def test_setup_output_folder_stage_creates_default_folder(tmp_path): + request = _make_request(tmp_path) + context = PipelineContext(request=request) + + SetupOutputFolderStage().run(context, None) + + assert ( + context.synchronized_folder_path + == request.raw_video_folder_path.parent / "synchronized_videos" + ) + assert context.synchronized_folder_path.is_dir() + + +def test_normalize_framerate_stage_skips_when_fps_uniform(tmp_path): + request = _make_request(tmp_path) + videos = [_make_video_info("cam_a", fps=30.0), _make_video_info("cam_b", fps=30.0)] + context = PipelineContext(request=request, videos=videos) + + result = NormalizeFramerateStage().run(context, None) + + assert result.normalized_folder_path is None + assert result.videos == videos + + +class _FakeBrightnessEvent: + def __init__(self, lag_seconds: float): + self.lag_seconds = lag_seconds + + +def test_brightness_lag_stage_normalizes_lags(monkeypatch, tmp_path): + request = _make_request(tmp_path, method=SyncMethod.BRIGHTNESS) + videos = [_make_video_info("cam_a", fps=10.0), _make_video_info("cam_b", fps=10.0)] + context = PipelineContext(request=request, videos=videos) + + raw_lag_seconds = {"cam_a": 1.0, "cam_b": 3.0} + + # encode the camera's raw lag in the "series" so the fake event-finder + # can decode it back out, without needing to track call order. + monkeypatch.setattr( + stages_module, + "compute_brightness_series", + lambda video: np.array([raw_lag_seconds[video.camera_name]]), + ) + monkeypatch.setattr( + stages_module, + "find_first_brightness_change", + lambda series, fps, threshold: _FakeBrightnessEvent( + lag_seconds=float(series[0]) + ), + ) + + lags = BrightnessLagStage().run(context, None).lags + lag_by_camera = {lag.camera_name: lag.lag_seconds for lag in lags} + + max_lag = max(raw_lag_seconds.values()) + assert min(lag_by_camera.values()) == 0.0 + assert lag_by_camera["cam_a"] == max_lag - raw_lag_seconds["cam_a"] + assert lag_by_camera["cam_b"] == max_lag - raw_lag_seconds["cam_b"] + + +def test_brightness_lag_stage_raises_when_no_event_detected(monkeypatch, tmp_path): + request = _make_request(tmp_path, method=SyncMethod.BRIGHTNESS) + videos = [_make_video_info("cam_a")] + context = PipelineContext(request=request, videos=videos) + + monkeypatch.setattr( + stages_module, "compute_brightness_series", lambda video: np.zeros(5) + ) + monkeypatch.setattr( + stages_module, + "find_first_brightness_change", + lambda series, fps, threshold: None, + ) + + with pytest.raises(SkellySyncError): + BrightnessLagStage().run(context, None) + + +class FakeBackend: + def __init__(self, should_fail_for: set[str] | None = None): + self.should_fail_for = should_fail_for or set() + + def trim(self, filepath, start_seconds, end_seconds, output_path): + camera_name = Path(filepath).stem + if camera_name in self.should_fail_for: + raise RuntimeError(f"boom for {camera_name}") + Path(output_path).touch() + + def probe(self, filepath): + return _make_video_info(Path(filepath).stem, duration_seconds=5.0) + + +def test_trim_video_worker_uses_ffmpeg_backend_for_final_probe(monkeypatch, tmp_path): + fake_backend = FakeBackend() + monkeypatch.setattr(stages_module, "get_backend", lambda kind: fake_backend) + + video_info = _make_video_info("raw_cam_a", duration_seconds=10.0) + lag = LagResult(camera_name="raw_cam_a", lag_seconds=1.0) + + result = _trim_video_worker( + video_info, lag, VideoBackendKind.FFMPEG, tmp_path, minimum_duration=5.0 + ) + + assert result.camera_name == "synced_cam_a" + + +def test_trim_stage_isolates_per_camera_errors(monkeypatch, tmp_path): + # Run the pool in-thread (not in a subprocess) so the monkeypatched + # backend factory is visible to "worker" code during the test. + monkeypatch.setattr( + stages_module, "ProcessPoolExecutor", concurrent.futures.ThreadPoolExecutor + ) + + fake_backend = FakeBackend(should_fail_for={"cam_b"}) + monkeypatch.setattr(stages_module, "get_backend", lambda kind: fake_backend) + + request = _make_request(tmp_path, method=SyncMethod.BRIGHTNESS) + output_dir = tmp_path / "synchronized_videos" + output_dir.mkdir() + + videos = [ + _make_video_info("cam_a", duration_seconds=10.0), + _make_video_info("cam_b", duration_seconds=10.0), + ] + lags = [ + LagResult(camera_name="cam_a", lag_seconds=0.0), + LagResult(camera_name="cam_b", lag_seconds=0.0), + ] + context = PipelineContext( + request=request, videos=videos, lags=lags, synchronized_folder_path=output_dir + ) + + with pytest.raises(SkellySyncError, match="cam_b"): + TrimStage().run(context, None) + + +def test_trim_stage_succeeds_when_all_cameras_trim_cleanly(monkeypatch, tmp_path): + monkeypatch.setattr( + stages_module, "ProcessPoolExecutor", concurrent.futures.ThreadPoolExecutor + ) + + fake_backend = FakeBackend() + monkeypatch.setattr(stages_module, "get_backend", lambda kind: fake_backend) + + request = _make_request(tmp_path, method=SyncMethod.BRIGHTNESS) + output_dir = tmp_path / "synchronized_videos" + output_dir.mkdir() + + videos = [ + _make_video_info("cam_a", duration_seconds=10.0), + _make_video_info("cam_b", duration_seconds=10.0), + ] + lags = [ + LagResult(camera_name="cam_a", lag_seconds=0.0), + LagResult(camera_name="cam_b", lag_seconds=1.0), + ] + context = PipelineContext( + request=request, videos=videos, lags=lags, synchronized_folder_path=output_dir + ) + + result = TrimStage().run(context, None) + + assert {video.camera_name for video in result.videos} == { + "synced_cam_a", + "synced_cam_b", + } + + +def test_verify_synchronized_framerate_stage_passes_when_fps_matches(tmp_path): + request = _make_request(tmp_path) + videos = [ + _make_video_info("synced_cam_a", fps=29.97), + _make_video_info("synced_cam_b", fps=29.97), + ] + context = PipelineContext(request=request, videos=videos) + + result = VerifySynchronizedFramerateStage().run(context, None) + + assert result.synchronized_fps == 29.97 + + +def test_verify_synchronized_framerate_stage_raises_on_any_mismatch(tmp_path): + # this is the core correctness guarantee of synchronization: even a tiny + # fps discrepancy between cameras (e.g. from a lossy backend re-encode) + # must fail loudly instead of silently producing misaligned output. + videos = [ + _make_video_info("synced_cam_a", fps=29.97), + _make_video_info("synced_cam_b", fps=29.970000001), + ] + context = PipelineContext( + request=_make_request(tmp_path), + videos=videos, + ) + + with pytest.raises(SkellySyncError, match="framerate"): + VerifySynchronizedFramerateStage().run(context, None) + + +def test_probe_with_frame_count_attaches_real_frame_count(monkeypatch, tmp_path): + class FakeCaptureBackend: + def probe(self, filepath): + return _make_video_info(Path(filepath).stem) + + monkeypatch.setattr(stages_module, "_count_video_frames", lambda filepath: 872) + + video_info = _probe_with_frame_count(FakeCaptureBackend(), tmp_path / "cam_a.mp4") + + assert video_info.frame_count == 872 + + +def test_verify_synchronized_frame_count_stage_passes_when_counts_match(tmp_path): + request = _make_request(tmp_path) + videos = [ + _make_video_info("synced_cam_a", frame_count=300), + _make_video_info("synced_cam_b", frame_count=300), + ] + context = PipelineContext(request=request, videos=videos) + + result = VerifySynchronizedFrameCountStage().run(context, None) + + assert result.synchronized_frame_count == 300 + + +def test_verify_synchronized_frame_count_stage_raises_on_any_mismatch(tmp_path): + # this is the actual correctness guarantee of synchronization: matching + # fps doesn't help if one camera's output is a frame longer or shorter. + request = _make_request(tmp_path) + videos = [ + _make_video_info("synced_cam_a", frame_count=300), + _make_video_info("synced_cam_b", frame_count=299), + ] + context = PipelineContext(request=request, videos=videos) + + with pytest.raises(SkellySyncError, match="frame count"): + VerifySynchronizedFrameCountStage().run(context, None) diff --git a/skelly_synchronize/tests/core/test_audio.py b/skelly_synchronize/tests/core/test_audio.py new file mode 100644 index 0000000..6544a21 --- /dev/null +++ b/skelly_synchronize/tests/core/test_audio.py @@ -0,0 +1,88 @@ +from pathlib import Path + +import numpy as np + +from skelly_synchronize.core.audio import ( + cross_correlate, + find_cross_correlation_lags, + get_reference_camera_name, + trim_audio_in_memory, +) +from skelly_synchronize.core.models import LagResult, VideoInfo + + +def _make_video_info(camera_name: str, duration_seconds: float) -> VideoInfo: + return VideoInfo( + filepath=Path(f"{camera_name}.mp4"), + camera_name=camera_name, + duration_seconds=duration_seconds, + fps=30.0, + ) + + +def test_get_reference_camera_name_picks_longest_duration(): + videos = [ + _make_video_info("cam_a", 10.0), + _make_video_info("cam_b", 12.0), + _make_video_info("cam_c", 11.0), + ] + assert get_reference_camera_name(videos) == "cam_b" + + +def test_get_reference_camera_name_ties_break_by_name(): + videos = [ + _make_video_info("cam_b", 10.0), + _make_video_info("cam_a", 10.0), + ] + assert get_reference_camera_name(videos) == "cam_a" + + +def test_cross_correlate_detects_known_shift(): + rng = np.random.default_rng(42) + reference_signal = rng.standard_normal(1000) + shift_samples = 50 + # zero-pad (not circular roll) so the shift is unambiguous for a noise signal + shifted_signal = np.concatenate([np.zeros(shift_samples), reference_signal])[:1000] + + lag, confidence = cross_correlate(reference_signal, shifted_signal) + + assert lag == -shift_samples + assert confidence > 1.0 + + +def test_find_cross_correlation_lags_normalizes_to_zero_minimum(): + sample_rate = 1000 + rng = np.random.default_rng(7) + base_signal = rng.standard_normal(sample_rate) + shift_samples = 20 + + audio_signals = { + "cam_a": base_signal, + "cam_b": np.concatenate([np.zeros(shift_samples), base_signal])[:sample_rate], + } + videos = [_make_video_info("cam_a", 1.0), _make_video_info("cam_b", 1.0)] + + lags = find_cross_correlation_lags(audio_signals, videos, sample_rate) + lag_by_camera = {lag.camera_name: lag.lag_seconds for lag in lags} + + assert min(lag_by_camera.values()) == 0.0 + assert all(confidence.confidence is not None for confidence in lags) + + +def test_trim_audio_in_memory_reuses_loaded_signals(tmp_path): + sample_rate = 100 + signals = { + "cam_a": np.arange(0, 100, dtype=float), + "cam_b": np.arange(0, 100, dtype=float), + } + lags = { + "cam_a": LagResult(camera_name="cam_a", lag_seconds=0.0), + "cam_b": LagResult(camera_name="cam_b", lag_seconds=0.1), + } + + output_paths = trim_audio_in_memory( + signals, sample_rate, lags, synced_length_seconds=0.5, output_folder=tmp_path + ) + + assert output_paths["cam_a"].exists() + assert output_paths["cam_b"].exists() diff --git a/skelly_synchronize/tests/core/test_brightness.py b/skelly_synchronize/tests/core/test_brightness.py new file mode 100644 index 0000000..e240f5e --- /dev/null +++ b/skelly_synchronize/tests/core/test_brightness.py @@ -0,0 +1,44 @@ +import numpy as np + +from skelly_synchronize.core.brightness import find_first_brightness_change + + +def test_find_first_brightness_change_detects_flash(): + brightness = np.concatenate([np.full(10, 10.0), np.full(20, 200.0)]) + + event = find_first_brightness_change( + brightness, fps=30.0, brightness_ratio_threshold=1000.0 + ) + + assert event is not None + assert event.frame_index == 10 + assert event.lag_seconds == 10 / 30.0 + + +def test_find_first_brightness_change_returns_none_for_flat_series(): + # regression test for KI-08: a flat series used to fall through to an + # implicit index-0 "no crossing found" sentinel that looked like a real event. + brightness = np.full(30, 128.0) + + event = find_first_brightness_change( + brightness, fps=30.0, brightness_ratio_threshold=1000.0 + ) + + assert event is None + + +def test_find_first_brightness_change_returns_none_for_empty_series(): + event = find_first_brightness_change(np.array([]), fps=30.0) + assert event is None + + +def test_find_first_brightness_change_falls_back_to_sharpest_change_below_threshold(): + # small, gradual change that never crosses the (high) threshold + brightness = np.array([10.0, 10.0, 12.0, 10.0, 10.0]) + + event = find_first_brightness_change( + brightness, fps=10.0, brightness_ratio_threshold=1000.0 + ) + + assert event is not None + assert event.lag_seconds == event.frame_index / 10.0 diff --git a/skelly_synchronize/tests/core/test_config.py b/skelly_synchronize/tests/core/test_config.py new file mode 100644 index 0000000..f10a947 --- /dev/null +++ b/skelly_synchronize/tests/core/test_config.py @@ -0,0 +1,9 @@ +from skelly_synchronize.core.config import synced_video_filename + + +def test_synced_video_filename_strips_raw_prefix(): + assert synced_video_filename("raw_cam_1") == "synced_cam_1.mp4" + + +def test_synced_video_filename_without_raw_prefix(): + assert synced_video_filename("cam_1") == "synced_cam_1.mp4" diff --git a/skelly_synchronize/tests/core/test_debug.py b/skelly_synchronize/tests/core/test_debug.py new file mode 100644 index 0000000..16585ce --- /dev/null +++ b/skelly_synchronize/tests/core/test_debug.py @@ -0,0 +1,57 @@ +from pathlib import Path + +import toml + +from skelly_synchronize.core.debug import save_debug_toml +from skelly_synchronize.core.models import LagResult, VideoInfo + + +def _make_video_info(camera_name: str, fps: float = 29.97) -> VideoInfo: + return VideoInfo( + filepath=Path(f"{camera_name}.mp4"), + camera_name=camera_name, + duration_seconds=10.0, + fps=fps, + ) + + +def test_save_debug_toml_surfaces_synchronized_fps(tmp_path): + output_path = tmp_path / "synchronization_debug.toml" + + save_debug_toml( + output_path, + videos_before=[ + _make_video_info("cam_a", fps=30.0), + _make_video_info("cam_b", fps=29.97), + ], + videos_after=[ + _make_video_info("synced_cam_a"), + _make_video_info("synced_cam_b"), + ], + lags=[LagResult(camera_name="cam_a", lag_seconds=0.0)], + synchronized_fps=29.97, + synchronized_frame_count=872, + ) + + data = toml.load(output_path) + + assert data["synchronized_video_fps"] == 29.97 + assert data["synchronized_video_frame_count"] == 872 + + +def test_save_debug_toml_omits_synchronized_fields_when_unknown(tmp_path): + output_path = tmp_path / "synchronization_debug.toml" + + save_debug_toml( + output_path, + videos_before=[], + videos_after=[], + lags=[], + synchronized_fps=None, + synchronized_frame_count=None, + ) + + data = toml.load(output_path) + + assert "synchronized_video_fps" not in data + assert "synchronized_video_frame_count" not in data diff --git a/skelly_synchronize/tests/core/test_models.py b/skelly_synchronize/tests/core/test_models.py new file mode 100644 index 0000000..cc6ae95 --- /dev/null +++ b/skelly_synchronize/tests/core/test_models.py @@ -0,0 +1,39 @@ +from pathlib import Path + +import pytest +from pydantic import ValidationError + +from skelly_synchronize.core.models import ( + LagResult, + SyncMethod, + VideoBackendKind, + VideoInfo, +) + + +def test_video_info_requires_all_fields(): + with pytest.raises(ValidationError): + VideoInfo(filepath=Path("video.mp4"), camera_name="cam_1") + + video_info = VideoInfo( + filepath=Path("video.mp4"), + camera_name="cam_1", + duration_seconds=10.0, + fps=30.0, + ) + assert video_info.frame_count is None + + +def test_lag_result_confidence_is_optional(): + lag_result = LagResult(camera_name="cam_1", lag_seconds=0.5) + assert lag_result.confidence is None + + +def test_sync_method_values(): + assert SyncMethod.AUDIO == "audio cross-correlation" + assert SyncMethod.BRIGHTNESS == "brightness change detection" + + +def test_video_backend_kind_values(): + assert VideoBackendKind.FFMPEG == "ffmpeg" + assert VideoBackendKind.DEFFCODE == "deffcode" diff --git a/skelly_synchronize_old/__init__.py b/skelly_synchronize_old/__init__.py new file mode 100644 index 0000000..905a4d1 --- /dev/null +++ b/skelly_synchronize_old/__init__.py @@ -0,0 +1,38 @@ +"""Top-level package for basic_template_repo.""" + +__package_name__ = "skelly_synchronize" +__version__ = "v2025.04.1037" + +__author__ = """Philip Queen""" +__email__ = "info@freemocap.org" +__repo_owner_github_user_name__ = "freemocap" +__repo_url__ = ( + f"https://github.com/{__repo_owner_github_user_name__}/{__package_name__}/" +) +__repo_issues_url__ = f"{__repo_url__}issues" + +import sys +from pathlib import Path + + +# print(f"Thank you for using {__package_name__}!") +# print(f"This is printing from: {__file__}") +# print(f"Source code for this package is available at: {__repo_url__}") + +base_package_path = Path(__file__).parent +# print(f"adding base_package_path: {base_package_path} : to sys.path") +sys.path.insert(0, str(base_package_path)) # add parent directory to sys.path + +from skelly_synchronize.system.default_paths import get_log_file_path +from skelly_synchronize.system.logging_configuration import configure_logging +from skelly_synchronize.skelly_synchronize import ( + synchronize_videos_from_audio, + synchronize_videos_from_brightness, +) +from skelly_synchronize.core_processes.debugging.debug_plots import ( + create_audio_debug_plots, + create_brightness_debug_plots, +) + + +configure_logging(log_file_path=str(get_log_file_path())) diff --git a/skelly_synchronize/__main__.py b/skelly_synchronize_old/__main__.py similarity index 100% rename from skelly_synchronize/__main__.py rename to skelly_synchronize_old/__main__.py diff --git a/skelly_synchronize/core_processes/audio_utilities.py b/skelly_synchronize_old/core_processes/audio_utilities.py similarity index 93% rename from skelly_synchronize/core_processes/audio_utilities.py rename to skelly_synchronize_old/core_processes/audio_utilities.py index 6bb1c4e..9a3fc16 100644 --- a/skelly_synchronize/core_processes/audio_utilities.py +++ b/skelly_synchronize_old/core_processes/audio_utilities.py @@ -5,12 +5,12 @@ import numpy as np from typing import Dict -from skelly_synchronize.core_processes.video_functions.ffmpeg_functions import ( +from skelly_synchronize_old.core_processes.video_functions.ffmpeg_functions import ( extract_audio_from_video_ffmpeg, extract_audio_sample_rate_ffmpeg, ) -from skelly_synchronize.system.file_extensions import AudioExtension -from skelly_synchronize.system.paths_and_file_names import TRIMMED_AUDIO_FOLDER_NAME +from skelly_synchronize_old.system.file_extensions import AudioExtension +from skelly_synchronize_old.system.paths_and_file_names import TRIMMED_AUDIO_FOLDER_NAME logger = logging.getLogger(__name__) diff --git a/skelly_synchronize/core_processes/correlation_functions.py b/skelly_synchronize_old/core_processes/correlation_functions.py similarity index 97% rename from skelly_synchronize/core_processes/correlation_functions.py rename to skelly_synchronize_old/core_processes/correlation_functions.py index 6a944df..ccf4e4f 100644 --- a/skelly_synchronize/core_processes/correlation_functions.py +++ b/skelly_synchronize_old/core_processes/correlation_functions.py @@ -5,8 +5,8 @@ from typing import Dict from scipy import signal -from skelly_synchronize.system.file_extensions import NUMPY_EXTENSION -from skelly_synchronize.system.paths_and_file_names import BRIGHTNESS_SUFFIX +from skelly_synchronize_old.system.file_extensions import NUMPY_EXTENSION +from skelly_synchronize_old.system.paths_and_file_names import BRIGHTNESS_SUFFIX logger = logging.getLogger(__name__) diff --git a/skelly_synchronize/core_processes/debugging/debug_output.py b/skelly_synchronize_old/core_processes/debugging/debug_output.py similarity index 100% rename from skelly_synchronize/core_processes/debugging/debug_output.py rename to skelly_synchronize_old/core_processes/debugging/debug_output.py diff --git a/skelly_synchronize/core_processes/debugging/debug_plots.py b/skelly_synchronize_old/core_processes/debugging/debug_plots.py similarity index 96% rename from skelly_synchronize/core_processes/debugging/debug_plots.py rename to skelly_synchronize_old/core_processes/debugging/debug_plots.py index 819fcfc..18abf2c 100644 --- a/skelly_synchronize/core_processes/debugging/debug_plots.py +++ b/skelly_synchronize_old/core_processes/debugging/debug_plots.py @@ -5,8 +5,8 @@ from pathlib import Path from typing import List -from skelly_synchronize.system.file_extensions import NUMPY_EXTENSION, AudioExtension -from skelly_synchronize.system.paths_and_file_names import ( +from skelly_synchronize_old.system.file_extensions import NUMPY_EXTENSION, AudioExtension +from skelly_synchronize_old.system.paths_and_file_names import ( BRIGHTNESS_SUFFIX, DEBUG_PLOT_NAME, AUDIO_FILES_FOLDER_NAME, diff --git a/skelly_synchronize/core_processes/normalize_framerates.py b/skelly_synchronize_old/core_processes/normalize_framerates.py similarity index 80% rename from skelly_synchronize/core_processes/normalize_framerates.py rename to skelly_synchronize_old/core_processes/normalize_framerates.py index 85f6554..97ae8bb 100644 --- a/skelly_synchronize/core_processes/normalize_framerates.py +++ b/skelly_synchronize_old/core_processes/normalize_framerates.py @@ -1,12 +1,12 @@ from pathlib import Path from typing import Dict, List, Optional -from skelly_synchronize.core_processes.video_functions.ffmpeg_functions import ( +from skelly_synchronize_old.core_processes.video_functions.ffmpeg_functions import ( normalize_framerates_in_video_ffmpeg, ) -from skelly_synchronize.system.file_extensions import VideoExtension -from skelly_synchronize.system.paths_and_file_names import NORMALIZED_VIDEOS_FOLDER_NAME +from skelly_synchronize_old.system.file_extensions import VideoExtension +from skelly_synchronize_old.system.paths_and_file_names import NORMALIZED_VIDEOS_FOLDER_NAME -from skelly_synchronize.utils.path_handling_utilities import create_directory +from skelly_synchronize_old.utils.path_handling_utilities import create_directory standard_audio_sample_rate = 44100 diff --git a/skelly_synchronize/core_processes/video_functions/deffcode_functions.py b/skelly_synchronize_old/core_processes/video_functions/deffcode_functions.py similarity index 96% rename from skelly_synchronize/core_processes/video_functions/deffcode_functions.py rename to skelly_synchronize_old/core_processes/video_functions/deffcode_functions.py index b13d010..2a88c63 100644 --- a/skelly_synchronize/core_processes/video_functions/deffcode_functions.py +++ b/skelly_synchronize_old/core_processes/video_functions/deffcode_functions.py @@ -3,7 +3,7 @@ import cv2 from deffcode import FFdecoder, Sourcer -from skelly_synchronize.core_processes.video_functions.ffmpeg_functions import ( +from skelly_synchronize_old.core_processes.video_functions.ffmpeg_functions import ( check_for_ffmpeg, ) diff --git a/skelly_synchronize/core_processes/video_functions/ffmpeg_functions.py b/skelly_synchronize_old/core_processes/video_functions/ffmpeg_functions.py similarity index 99% rename from skelly_synchronize/core_processes/video_functions/ffmpeg_functions.py rename to skelly_synchronize_old/core_processes/video_functions/ffmpeg_functions.py index 5936439..68b3146 100644 --- a/skelly_synchronize/core_processes/video_functions/ffmpeg_functions.py +++ b/skelly_synchronize_old/core_processes/video_functions/ffmpeg_functions.py @@ -4,7 +4,7 @@ from pathlib import Path from typing import Union -from skelly_synchronize.system.file_extensions import AudioExtension +from skelly_synchronize_old.system.file_extensions import AudioExtension logger = logging.getLogger(__name__) diff --git a/skelly_synchronize/core_processes/video_functions/video_utilities.py b/skelly_synchronize_old/core_processes/video_functions/video_utilities.py similarity index 93% rename from skelly_synchronize/core_processes/video_functions/video_utilities.py rename to skelly_synchronize_old/core_processes/video_functions/video_utilities.py index 9c6895f..cddd5ea 100644 --- a/skelly_synchronize/core_processes/video_functions/video_utilities.py +++ b/skelly_synchronize_old/core_processes/video_functions/video_utilities.py @@ -5,19 +5,19 @@ from pathlib import Path from typing import Dict -from skelly_synchronize.core_processes.audio_utilities import trim_audio_files -from skelly_synchronize.core_processes.video_functions.deffcode_functions import ( +from skelly_synchronize_old.core_processes.audio_utilities import trim_audio_files +from skelly_synchronize_old.core_processes.video_functions.deffcode_functions import ( trim_single_video_deffcode, ) -from skelly_synchronize.core_processes.video_functions.ffmpeg_functions import ( +from skelly_synchronize_old.core_processes.video_functions.ffmpeg_functions import ( attach_audio_to_video_ffmpeg, extract_video_duration_ffmpeg, extract_video_fps_ffmpeg, trim_single_video_ffmpeg, ) -from skelly_synchronize.system.file_extensions import AudioExtension, VideoExtension -from skelly_synchronize.utils.get_video_files import get_video_file_list -from skelly_synchronize.utils.path_handling_utilities import ( +from skelly_synchronize_old.system.file_extensions import AudioExtension, VideoExtension +from skelly_synchronize_old.utils.get_video_files import get_video_file_list +from skelly_synchronize_old.utils.path_handling_utilities import ( name_synced_video, ) diff --git a/skelly_synchronize/gui/skelly_synchronize_gui.py b/skelly_synchronize_old/gui/skelly_synchronize_gui.py similarity index 98% rename from skelly_synchronize/gui/skelly_synchronize_gui.py rename to skelly_synchronize_old/gui/skelly_synchronize_gui.py index 3883d93..40da6c9 100644 --- a/skelly_synchronize/gui/skelly_synchronize_gui.py +++ b/skelly_synchronize_old/gui/skelly_synchronize_gui.py @@ -12,7 +12,7 @@ QHBoxLayout, ) -from skelly_synchronize.skelly_synchronize import ( +from skelly_synchronize_old.skelly_synchronize import ( synchronize_videos_from_audio, synchronize_videos_from_brightness, ) diff --git a/skelly_synchronize/gui/widgets/run_button_widget.py b/skelly_synchronize_old/gui/widgets/run_button_widget.py similarity index 100% rename from skelly_synchronize/gui/widgets/run_button_widget.py rename to skelly_synchronize_old/gui/widgets/run_button_widget.py diff --git a/skelly_synchronize/skelly_synchronize.py b/skelly_synchronize_old/skelly_synchronize.py similarity index 90% rename from skelly_synchronize/skelly_synchronize.py rename to skelly_synchronize_old/skelly_synchronize.py index 7a36a37..d5d1dc4 100644 --- a/skelly_synchronize/skelly_synchronize.py +++ b/skelly_synchronize_old/skelly_synchronize.py @@ -2,42 +2,42 @@ import logging from pathlib import Path from typing import Optional -from skelly_synchronize.core_processes.debugging.debug_plots import ( +from skelly_synchronize_old.core_processes.debugging.debug_plots import ( create_audio_debug_plots, create_brightness_debug_plots, ) -from skelly_synchronize.core_processes.normalize_framerates import normalize_framerates +from skelly_synchronize_old.core_processes.normalize_framerates import normalize_framerates -from skelly_synchronize.utils.get_video_files import get_video_file_list -from skelly_synchronize.core_processes.audio_utilities import ( +from skelly_synchronize_old.utils.get_video_files import get_video_file_list +from skelly_synchronize_old.core_processes.audio_utilities import ( extract_audio_files, get_audio_sample_rates, ) -from skelly_synchronize.core_processes.correlation_functions import ( +from skelly_synchronize_old.core_processes.correlation_functions import ( find_brightest_point_lags, find_brightness_across_frames, find_cross_correlation_lags, ) -from skelly_synchronize.core_processes.video_functions.video_utilities import ( +from skelly_synchronize_old.core_processes.video_functions.video_utilities import ( attach_audio_to_videos, get_fps_list, create_video_info_dict, trim_videos, ) -from skelly_synchronize.core_processes.debugging.debug_output import ( +from skelly_synchronize_old.core_processes.debugging.debug_output import ( remove_audio_files_from_audio_signal_dict, save_dictionaries_to_toml, ) -from skelly_synchronize.utils.path_handling_utilities import ( +from skelly_synchronize_old.utils.path_handling_utilities import ( create_directory, ) -from skelly_synchronize.tests.utilities.check_list_values_are_equal import ( +from skelly_synchronize_old.tests.utilities.check_list_values_are_equal import ( check_list_values_are_equal, ) -from skelly_synchronize.tests.utilities.get_number_of_frames_of_videos_in_a_folder import ( +from skelly_synchronize_old.tests.utilities.get_number_of_frames_of_videos_in_a_folder import ( get_number_of_frames_of_videos_in_a_folder, ) -from skelly_synchronize.system.paths_and_file_names import ( +from skelly_synchronize_old.system.paths_and_file_names import ( AUDIO_NAME, DEBUG_TOML_NAME, LAG_DICTIONARY_NAME, @@ -47,7 +47,7 @@ SYNCHRONIZED_VIDEOS_FOLDER_NAME, AUDIO_FILES_FOLDER_NAME, ) -from skelly_synchronize.system.file_extensions import AudioExtension +from skelly_synchronize_old.system.file_extensions import AudioExtension logger = logging.getLogger(__name__) diff --git a/skelly_synchronize_old/system/__init__.py b/skelly_synchronize_old/system/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/skelly_synchronize/system/default_paths.py b/skelly_synchronize_old/system/default_paths.py similarity index 96% rename from skelly_synchronize/system/default_paths.py rename to skelly_synchronize_old/system/default_paths.py index 07e18db..09bb41c 100644 --- a/skelly_synchronize/system/default_paths.py +++ b/skelly_synchronize_old/system/default_paths.py @@ -2,7 +2,7 @@ import time from pathlib import Path -from skelly_synchronize import __package_name__ +from skelly_synchronize_old import __package_name__ BASE_FOLDER_NAME = f"{__package_name__}_data" LOGS_INFO_AND_SETTINGS_FOLDER_NAME = "logs_info_and_settings" diff --git a/skelly_synchronize/system/file_extensions.py b/skelly_synchronize_old/system/file_extensions.py similarity index 100% rename from skelly_synchronize/system/file_extensions.py rename to skelly_synchronize_old/system/file_extensions.py diff --git a/skelly_synchronize/system/logging_configuration.py b/skelly_synchronize_old/system/logging_configuration.py similarity index 100% rename from skelly_synchronize/system/logging_configuration.py rename to skelly_synchronize_old/system/logging_configuration.py diff --git a/skelly_synchronize/system/paths_and_file_names.py b/skelly_synchronize_old/system/paths_and_file_names.py similarity index 100% rename from skelly_synchronize/system/paths_and_file_names.py rename to skelly_synchronize_old/system/paths_and_file_names.py diff --git a/skelly_synchronize/tests/conftest.py b/skelly_synchronize_old/tests/conftest.py similarity index 84% rename from skelly_synchronize/tests/conftest.py rename to skelly_synchronize_old/tests/conftest.py index 146246f..430eeec 100644 --- a/skelly_synchronize/tests/conftest.py +++ b/skelly_synchronize_old/tests/conftest.py @@ -9,12 +9,12 @@ print(f"adding base_package_path: {base_package_path} : to sys.path") sys.path.insert(0, str(base_package_path)) # add parent directory to sys.path -from skelly_synchronize.skelly_synchronize import synchronize_videos_from_audio -from skelly_synchronize.tests.utilities.load_sample_data import ( +from skelly_synchronize_old.skelly_synchronize import synchronize_videos_from_audio +from skelly_synchronize_old.tests.utilities.load_sample_data import ( find_raw_videos_folder_path, load_sample_data, ) -from skelly_synchronize.utils.get_video_files import get_video_file_list +from skelly_synchronize_old.utils.get_video_files import get_video_file_list def pytest_sessionstart(): diff --git a/skelly_synchronize/tests/test_all_files_created.py b/skelly_synchronize_old/tests/test_all_files_created.py similarity index 95% rename from skelly_synchronize/tests/test_all_files_created.py rename to skelly_synchronize_old/tests/test_all_files_created.py index 9ca912b..2e21cb9 100644 --- a/skelly_synchronize/tests/test_all_files_created.py +++ b/skelly_synchronize_old/tests/test_all_files_created.py @@ -2,7 +2,7 @@ from pathlib import Path from typing import Union -from skelly_synchronize.system.paths_and_file_names import ( +from skelly_synchronize_old.system.paths_and_file_names import ( AUDIO_FILES_FOLDER_NAME, DEBUG_PLOT_NAME, DEBUG_TOML_NAME, diff --git a/skelly_synchronize/tests/test_normalize_lag_dict.py b/skelly_synchronize_old/tests/test_normalize_lag_dict.py similarity index 88% rename from skelly_synchronize/tests/test_normalize_lag_dict.py rename to skelly_synchronize_old/tests/test_normalize_lag_dict.py index fb06dc4..1a266e6 100644 --- a/skelly_synchronize/tests/test_normalize_lag_dict.py +++ b/skelly_synchronize_old/tests/test_normalize_lag_dict.py @@ -1,6 +1,6 @@ import pytest -from skelly_synchronize.core_processes.correlation_functions import ( +from skelly_synchronize_old.core_processes.correlation_functions import ( normalize_lag_dictionary, ) diff --git a/skelly_synchronize/tests/test_number_of_videos_is_preserved.py b/skelly_synchronize_old/tests/test_number_of_videos_is_preserved.py similarity index 85% rename from skelly_synchronize/tests/test_number_of_videos_is_preserved.py rename to skelly_synchronize_old/tests/test_number_of_videos_is_preserved.py index 87020ef..4497267 100644 --- a/skelly_synchronize/tests/test_number_of_videos_is_preserved.py +++ b/skelly_synchronize_old/tests/test_number_of_videos_is_preserved.py @@ -2,7 +2,7 @@ from typing import Union from pathlib import Path -from skelly_synchronize.utils.get_video_files import get_video_file_list +from skelly_synchronize_old.utils.get_video_files import get_video_file_list @pytest.mark.usefixtures("raw_video_folder_path", "synchronized_video_folder_path") diff --git a/skelly_synchronize/tests/test_trim_single_video_deffcode.py b/skelly_synchronize_old/tests/test_trim_single_video_deffcode.py similarity index 80% rename from skelly_synchronize/tests/test_trim_single_video_deffcode.py rename to skelly_synchronize_old/tests/test_trim_single_video_deffcode.py index 9afe7cc..f751681 100644 --- a/skelly_synchronize/tests/test_trim_single_video_deffcode.py +++ b/skelly_synchronize_old/tests/test_trim_single_video_deffcode.py @@ -2,10 +2,10 @@ import logging -from skelly_synchronize.tests.utilities.find_frame_count_of_video import ( +from skelly_synchronize_old.tests.utilities.find_frame_count_of_video import ( find_frame_count_of_video, ) -from skelly_synchronize.core_processes.video_functions.deffcode_functions import ( +from skelly_synchronize_old.core_processes.video_functions.deffcode_functions import ( trim_single_video_deffcode, ) diff --git a/skelly_synchronize/tests/test_videos_are_same_length.py b/skelly_synchronize_old/tests/test_videos_are_same_length.py similarity index 75% rename from skelly_synchronize/tests/test_videos_are_same_length.py rename to skelly_synchronize_old/tests/test_videos_are_same_length.py index 182e5bf..ff2a244 100644 --- a/skelly_synchronize/tests/test_videos_are_same_length.py +++ b/skelly_synchronize_old/tests/test_videos_are_same_length.py @@ -2,10 +2,10 @@ from pathlib import Path from typing import Union -from skelly_synchronize.tests.utilities.check_list_values_are_equal import ( +from skelly_synchronize_old.tests.utilities.check_list_values_are_equal import ( check_list_values_are_equal, ) -from skelly_synchronize.tests.utilities.get_number_of_frames_of_videos_in_a_folder import ( +from skelly_synchronize_old.tests.utilities.get_number_of_frames_of_videos_in_a_folder import ( get_number_of_frames_of_videos_in_a_folder, ) diff --git a/skelly_synchronize/tests/utilities/check_list_values_are_equal.py b/skelly_synchronize_old/tests/utilities/check_list_values_are_equal.py similarity index 100% rename from skelly_synchronize/tests/utilities/check_list_values_are_equal.py rename to skelly_synchronize_old/tests/utilities/check_list_values_are_equal.py diff --git a/skelly_synchronize/tests/utilities/find_frame_count_of_video.py b/skelly_synchronize_old/tests/utilities/find_frame_count_of_video.py similarity index 100% rename from skelly_synchronize/tests/utilities/find_frame_count_of_video.py rename to skelly_synchronize_old/tests/utilities/find_frame_count_of_video.py diff --git a/skelly_synchronize/tests/utilities/get_number_of_frames_of_videos_in_a_folder.py b/skelly_synchronize_old/tests/utilities/get_number_of_frames_of_videos_in_a_folder.py similarity index 88% rename from skelly_synchronize/tests/utilities/get_number_of_frames_of_videos_in_a_folder.py rename to skelly_synchronize_old/tests/utilities/get_number_of_frames_of_videos_in_a_folder.py index 8f290dc..a8c2e32 100644 --- a/skelly_synchronize/tests/utilities/get_number_of_frames_of_videos_in_a_folder.py +++ b/skelly_synchronize_old/tests/utilities/get_number_of_frames_of_videos_in_a_folder.py @@ -7,8 +7,8 @@ print(f"adding base_package_path: {base_package_path} : to sys.path") sys.path.insert(0, str(base_package_path)) # add parent directory to sys.path -from skelly_synchronize.utils.get_video_files import get_video_file_list -from skelly_synchronize.tests.utilities.find_frame_count_of_video import ( +from skelly_synchronize_old.utils.get_video_files import get_video_file_list +from skelly_synchronize_old.tests.utilities.find_frame_count_of_video import ( find_frame_count_of_video, ) diff --git a/skelly_synchronize/tests/utilities/load_sample_data.py b/skelly_synchronize_old/tests/utilities/load_sample_data.py similarity index 93% rename from skelly_synchronize/tests/utilities/load_sample_data.py rename to skelly_synchronize_old/tests/utilities/load_sample_data.py index e2ae282..4085f0a 100644 --- a/skelly_synchronize/tests/utilities/load_sample_data.py +++ b/skelly_synchronize_old/tests/utilities/load_sample_data.py @@ -3,7 +3,7 @@ import zipfile from pathlib import Path -from skelly_synchronize.system.paths_and_file_names import ( +from skelly_synchronize_old.system.paths_and_file_names import ( FIGSHARE_SAMPLE_DATA_FILE_NAME, FIGSHARE_ZIP_FILE_URL, RAW_VIDEOS_FOLDER_NAME, diff --git a/skelly_synchronize/utils/get_video_files.py b/skelly_synchronize_old/utils/get_video_files.py similarity index 94% rename from skelly_synchronize/utils/get_video_files.py rename to skelly_synchronize_old/utils/get_video_files.py index 7c09d20..7d7d25d 100644 --- a/skelly_synchronize/utils/get_video_files.py +++ b/skelly_synchronize_old/utils/get_video_files.py @@ -1,7 +1,7 @@ import logging from pathlib import Path -from skelly_synchronize.system.file_extensions import VideoExtension +from skelly_synchronize_old.system.file_extensions import VideoExtension logger = logging.getLogger(__name__) diff --git a/skelly_synchronize/utils/path_handling_utilities.py b/skelly_synchronize_old/utils/path_handling_utilities.py similarity index 88% rename from skelly_synchronize/utils/path_handling_utilities.py rename to skelly_synchronize_old/utils/path_handling_utilities.py index 079db08..737d2fe 100644 --- a/skelly_synchronize/utils/path_handling_utilities.py +++ b/skelly_synchronize_old/utils/path_handling_utilities.py @@ -1,8 +1,8 @@ import logging from pathlib import Path -from skelly_synchronize.system.file_extensions import VideoExtension -from skelly_synchronize.system.paths_and_file_names import SYNCED_VIDEO_PRECURSOR +from skelly_synchronize_old.system.file_extensions import VideoExtension +from skelly_synchronize_old.system.paths_and_file_names import SYNCED_VIDEO_PRECURSOR logger = logging.getLogger(__name__) diff --git a/uv.lock b/uv.lock index ef71b21..4a854ac 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 1 +revision = 3 requires-python = ">=3.9, <3.13" resolution-markers = [ "python_full_version >= '3.11' and sys_platform == 'darwin'", @@ -13,22 +13,53 @@ resolution-markers = [ "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", ] +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", + "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11' and sys_platform == 'darwin'", + "python_full_version >= '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.10.*' and sys_platform == 'darwin'", + "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +sdist = { url = "https://files.pythonhosted.org/packages/5f/56/a8120250d128bed162cd73c76d45f6ef9991f3e068f62a8ee060afa3104a/annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7", size = 15893, upload-time = "2026-07-23T20:16:13.995Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0", size = 13427, upload-time = "2026-07-23T20:16:12.938Z" }, +] + [[package]] name = "attrs" version = "25.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032 } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032, upload-time = "2025-03-13T11:10:22.779Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815 }, + { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" }, ] [[package]] name = "audioread" version = "3.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/db/d2/87016ca9f083acadffb2d8da59bfa3253e4da7eeb9f71fb8e7708dc97ecd/audioread-3.0.1.tar.gz", hash = "sha256:ac5460a5498c48bdf2e8e767402583a4dcd13f4414d286f42ce4379e8b35066d", size = 116513 } +sdist = { url = "https://files.pythonhosted.org/packages/db/d2/87016ca9f083acadffb2d8da59bfa3253e4da7eeb9f71fb8e7708dc97ecd/audioread-3.0.1.tar.gz", hash = "sha256:ac5460a5498c48bdf2e8e767402583a4dcd13f4414d286f42ce4379e8b35066d", size = 116513, upload-time = "2023-09-27T19:27:53.084Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/57/8d/30aa32745af16af0a9a650115fbe81bde7c610ed5c21b381fca0196f3a7f/audioread-3.0.1-py3-none-any.whl", hash = "sha256:4cdce70b8adc0da0a3c9e0d85fb10b3ace30fbdf8d1670fd443929b61d117c33", size = 23492 }, + { url = "https://files.pythonhosted.org/packages/57/8d/30aa32745af16af0a9a650115fbe81bde7c610ed5c21b381fca0196f3a7f/audioread-3.0.1-py3-none-any.whl", hash = "sha256:4cdce70b8adc0da0a3c9e0d85fb10b3ace30fbdf8d1670fd443929b61d117c33", size = 23492, upload-time = "2023-09-27T19:27:51.334Z" }, ] [[package]] @@ -41,9 +72,9 @@ dependencies = [ { name = "rich" }, { name = "stevedore" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1a/a5/144a45f8e67df9d66c3bc3f7e69a39537db8bff1189ab7cff4e9459215da/bandit-1.8.3.tar.gz", hash = "sha256:f5847beb654d309422985c36644649924e0ea4425c76dec2e89110b87506193a", size = 4232005 } +sdist = { url = "https://files.pythonhosted.org/packages/1a/a5/144a45f8e67df9d66c3bc3f7e69a39537db8bff1189ab7cff4e9459215da/bandit-1.8.3.tar.gz", hash = "sha256:f5847beb654d309422985c36644649924e0ea4425c76dec2e89110b87506193a", size = 4232005, upload-time = "2025-02-17T05:24:57.031Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/85/db74b9233e0aa27ec96891045c5e920a64dd5cbccd50f8e64e9460f48d35/bandit-1.8.3-py3-none-any.whl", hash = "sha256:28f04dc0d258e1dd0f99dee8eefa13d1cb5e3fde1a5ab0c523971f97b289bcd8", size = 129078 }, + { url = "https://files.pythonhosted.org/packages/88/85/db74b9233e0aa27ec96891045c5e920a64dd5cbccd50f8e64e9460f48d35/bandit-1.8.3-py3-none-any.whl", hash = "sha256:28f04dc0d258e1dd0f99dee8eefa13d1cb5e3fde1a5ab0c523971f97b289bcd8", size = 129078, upload-time = "2025-02-17T05:24:54.068Z" }, ] [[package]] @@ -59,25 +90,25 @@ dependencies = [ { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/94/49/26a7b0f3f35da4b5a65f081943b7bcd22d7002f5f0fb8098ec1ff21cb6ef/black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666", size = 649449 } +sdist = { url = "https://files.pythonhosted.org/packages/94/49/26a7b0f3f35da4b5a65f081943b7bcd22d7002f5f0fb8098ec1ff21cb6ef/black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666", size = 649449, upload-time = "2025-01-29T04:15:40.373Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/3b/4ba3f93ac8d90410423fdd31d7541ada9bcee1df32fb90d26de41ed40e1d/black-25.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:759e7ec1e050a15f89b770cefbf91ebee8917aac5c20483bc2d80a6c3a04df32", size = 1629419 }, - { url = "https://files.pythonhosted.org/packages/b4/02/0bde0485146a8a5e694daed47561785e8b77a0466ccc1f3e485d5ef2925e/black-25.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e519ecf93120f34243e6b0054db49c00a35f84f195d5bce7e9f5cfc578fc2da", size = 1461080 }, - { url = "https://files.pythonhosted.org/packages/52/0e/abdf75183c830eaca7589144ff96d49bce73d7ec6ad12ef62185cc0f79a2/black-25.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:055e59b198df7ac0b7efca5ad7ff2516bca343276c466be72eb04a3bcc1f82d7", size = 1766886 }, - { url = "https://files.pythonhosted.org/packages/dc/a6/97d8bb65b1d8a41f8a6736222ba0a334db7b7b77b8023ab4568288f23973/black-25.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:db8ea9917d6f8fc62abd90d944920d95e73c83a5ee3383493e35d271aca872e9", size = 1419404 }, - { url = "https://files.pythonhosted.org/packages/7e/4f/87f596aca05c3ce5b94b8663dbfe242a12843caaa82dd3f85f1ffdc3f177/black-25.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a39337598244de4bae26475f77dda852ea00a93bd4c728e09eacd827ec929df0", size = 1614372 }, - { url = "https://files.pythonhosted.org/packages/e7/d0/2c34c36190b741c59c901e56ab7f6e54dad8df05a6272a9747ecef7c6036/black-25.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96c1c7cd856bba8e20094e36e0f948718dc688dba4a9d78c3adde52b9e6c2299", size = 1442865 }, - { url = "https://files.pythonhosted.org/packages/21/d4/7518c72262468430ead45cf22bd86c883a6448b9eb43672765d69a8f1248/black-25.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce2e264d59c91e52d8000d507eb20a9aca4a778731a08cfff7e5ac4a4bb7096", size = 1749699 }, - { url = "https://files.pythonhosted.org/packages/58/db/4f5beb989b547f79096e035c4981ceb36ac2b552d0ac5f2620e941501c99/black-25.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:172b1dbff09f86ce6f4eb8edf9dede08b1fce58ba194c87d7a4f1a5aa2f5b3c2", size = 1428028 }, - { url = "https://files.pythonhosted.org/packages/83/71/3fe4741df7adf015ad8dfa082dd36c94ca86bb21f25608eb247b4afb15b2/black-25.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4b60580e829091e6f9238c848ea6750efed72140b91b048770b64e74fe04908b", size = 1650988 }, - { url = "https://files.pythonhosted.org/packages/13/f3/89aac8a83d73937ccd39bbe8fc6ac8860c11cfa0af5b1c96d081facac844/black-25.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e2978f6df243b155ef5fa7e558a43037c3079093ed5d10fd84c43900f2d8ecc", size = 1453985 }, - { url = "https://files.pythonhosted.org/packages/6f/22/b99efca33f1f3a1d2552c714b1e1b5ae92efac6c43e790ad539a163d1754/black-25.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b48735872ec535027d979e8dcb20bf4f70b5ac75a8ea99f127c106a7d7aba9f", size = 1783816 }, - { url = "https://files.pythonhosted.org/packages/18/7e/a27c3ad3822b6f2e0e00d63d58ff6299a99a5b3aee69fa77cd4b0076b261/black-25.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:ea0213189960bda9cf99be5b8c8ce66bb054af5e9e861249cd23471bd7b0b3ba", size = 1440860 }, - { url = "https://files.pythonhosted.org/packages/d3/b6/ae7507470a4830dbbfe875c701e84a4a5fb9183d1497834871a715716a92/black-25.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a1ee0a0c330f7b5130ce0caed9936a904793576ef4d2b98c40835d6a65afa6a0", size = 1628593 }, - { url = "https://files.pythonhosted.org/packages/24/c1/ae36fa59a59f9363017ed397750a0cd79a470490860bc7713967d89cdd31/black-25.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3df5f1bf91d36002b0a75389ca8663510cf0531cca8aa5c1ef695b46d98655f", size = 1460000 }, - { url = "https://files.pythonhosted.org/packages/ac/b6/98f832e7a6c49aa3a464760c67c7856363aa644f2f3c74cf7d624168607e/black-25.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9e6827d563a2c820772b32ce8a42828dc6790f095f441beef18f96aa6f8294e", size = 1765963 }, - { url = "https://files.pythonhosted.org/packages/ce/e9/2cb0a017eb7024f70e0d2e9bdb8c5a5b078c5740c7f8816065d06f04c557/black-25.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:bacabb307dca5ebaf9c118d2d2f6903da0d62c9faa82bd21a33eecc319559355", size = 1419419 }, - { url = "https://files.pythonhosted.org/packages/09/71/54e999902aed72baf26bca0d50781b01838251a462612966e9fc4891eadd/black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717", size = 207646 }, + { url = "https://files.pythonhosted.org/packages/4d/3b/4ba3f93ac8d90410423fdd31d7541ada9bcee1df32fb90d26de41ed40e1d/black-25.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:759e7ec1e050a15f89b770cefbf91ebee8917aac5c20483bc2d80a6c3a04df32", size = 1629419, upload-time = "2025-01-29T05:37:06.642Z" }, + { url = "https://files.pythonhosted.org/packages/b4/02/0bde0485146a8a5e694daed47561785e8b77a0466ccc1f3e485d5ef2925e/black-25.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e519ecf93120f34243e6b0054db49c00a35f84f195d5bce7e9f5cfc578fc2da", size = 1461080, upload-time = "2025-01-29T05:37:09.321Z" }, + { url = "https://files.pythonhosted.org/packages/52/0e/abdf75183c830eaca7589144ff96d49bce73d7ec6ad12ef62185cc0f79a2/black-25.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:055e59b198df7ac0b7efca5ad7ff2516bca343276c466be72eb04a3bcc1f82d7", size = 1766886, upload-time = "2025-01-29T04:18:24.432Z" }, + { url = "https://files.pythonhosted.org/packages/dc/a6/97d8bb65b1d8a41f8a6736222ba0a334db7b7b77b8023ab4568288f23973/black-25.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:db8ea9917d6f8fc62abd90d944920d95e73c83a5ee3383493e35d271aca872e9", size = 1419404, upload-time = "2025-01-29T04:19:04.296Z" }, + { url = "https://files.pythonhosted.org/packages/7e/4f/87f596aca05c3ce5b94b8663dbfe242a12843caaa82dd3f85f1ffdc3f177/black-25.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a39337598244de4bae26475f77dda852ea00a93bd4c728e09eacd827ec929df0", size = 1614372, upload-time = "2025-01-29T05:37:11.71Z" }, + { url = "https://files.pythonhosted.org/packages/e7/d0/2c34c36190b741c59c901e56ab7f6e54dad8df05a6272a9747ecef7c6036/black-25.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96c1c7cd856bba8e20094e36e0f948718dc688dba4a9d78c3adde52b9e6c2299", size = 1442865, upload-time = "2025-01-29T05:37:14.309Z" }, + { url = "https://files.pythonhosted.org/packages/21/d4/7518c72262468430ead45cf22bd86c883a6448b9eb43672765d69a8f1248/black-25.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce2e264d59c91e52d8000d507eb20a9aca4a778731a08cfff7e5ac4a4bb7096", size = 1749699, upload-time = "2025-01-29T04:18:17.688Z" }, + { url = "https://files.pythonhosted.org/packages/58/db/4f5beb989b547f79096e035c4981ceb36ac2b552d0ac5f2620e941501c99/black-25.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:172b1dbff09f86ce6f4eb8edf9dede08b1fce58ba194c87d7a4f1a5aa2f5b3c2", size = 1428028, upload-time = "2025-01-29T04:18:51.711Z" }, + { url = "https://files.pythonhosted.org/packages/83/71/3fe4741df7adf015ad8dfa082dd36c94ca86bb21f25608eb247b4afb15b2/black-25.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4b60580e829091e6f9238c848ea6750efed72140b91b048770b64e74fe04908b", size = 1650988, upload-time = "2025-01-29T05:37:16.707Z" }, + { url = "https://files.pythonhosted.org/packages/13/f3/89aac8a83d73937ccd39bbe8fc6ac8860c11cfa0af5b1c96d081facac844/black-25.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e2978f6df243b155ef5fa7e558a43037c3079093ed5d10fd84c43900f2d8ecc", size = 1453985, upload-time = "2025-01-29T05:37:18.273Z" }, + { url = "https://files.pythonhosted.org/packages/6f/22/b99efca33f1f3a1d2552c714b1e1b5ae92efac6c43e790ad539a163d1754/black-25.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b48735872ec535027d979e8dcb20bf4f70b5ac75a8ea99f127c106a7d7aba9f", size = 1783816, upload-time = "2025-01-29T04:18:33.823Z" }, + { url = "https://files.pythonhosted.org/packages/18/7e/a27c3ad3822b6f2e0e00d63d58ff6299a99a5b3aee69fa77cd4b0076b261/black-25.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:ea0213189960bda9cf99be5b8c8ce66bb054af5e9e861249cd23471bd7b0b3ba", size = 1440860, upload-time = "2025-01-29T04:19:12.944Z" }, + { url = "https://files.pythonhosted.org/packages/d3/b6/ae7507470a4830dbbfe875c701e84a4a5fb9183d1497834871a715716a92/black-25.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a1ee0a0c330f7b5130ce0caed9936a904793576ef4d2b98c40835d6a65afa6a0", size = 1628593, upload-time = "2025-01-29T05:37:23.672Z" }, + { url = "https://files.pythonhosted.org/packages/24/c1/ae36fa59a59f9363017ed397750a0cd79a470490860bc7713967d89cdd31/black-25.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3df5f1bf91d36002b0a75389ca8663510cf0531cca8aa5c1ef695b46d98655f", size = 1460000, upload-time = "2025-01-29T05:37:25.829Z" }, + { url = "https://files.pythonhosted.org/packages/ac/b6/98f832e7a6c49aa3a464760c67c7856363aa644f2f3c74cf7d624168607e/black-25.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9e6827d563a2c820772b32ce8a42828dc6790f095f441beef18f96aa6f8294e", size = 1765963, upload-time = "2025-01-29T04:18:38.116Z" }, + { url = "https://files.pythonhosted.org/packages/ce/e9/2cb0a017eb7024f70e0d2e9bdb8c5a5b078c5740c7f8816065d06f04c557/black-25.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:bacabb307dca5ebaf9c118d2d2f6903da0d62c9faa82bd21a33eecc319559355", size = 1419419, upload-time = "2025-01-29T04:18:30.191Z" }, + { url = "https://files.pythonhosted.org/packages/09/71/54e999902aed72baf26bca0d50781b01838251a462612966e9fc4891eadd/black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717", size = 207646, upload-time = "2025-01-29T04:15:38.082Z" }, ] [[package]] @@ -91,9 +122,9 @@ dependencies = [ { name = "pyproject-hooks" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7d/46/aeab111f8e06793e4f0e421fcad593d547fb8313b50990f31681ee2fb1ad/build-1.2.2.post1.tar.gz", hash = "sha256:b36993e92ca9375a219c99e606a122ff365a760a2d4bba0caa09bd5278b608b7", size = 46701 } +sdist = { url = "https://files.pythonhosted.org/packages/7d/46/aeab111f8e06793e4f0e421fcad593d547fb8313b50990f31681ee2fb1ad/build-1.2.2.post1.tar.gz", hash = "sha256:b36993e92ca9375a219c99e606a122ff365a760a2d4bba0caa09bd5278b608b7", size = 46701, upload-time = "2024-10-06T17:22:25.251Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/84/c2/80633736cd183ee4a62107413def345f7e6e3c01563dbca1417363cf957e/build-1.2.2.post1-py3-none-any.whl", hash = "sha256:1d61c0887fa860c01971625baae8bdd338e517b836a2f70dd1f7aa3a6b2fc5b5", size = 22950 }, + { url = "https://files.pythonhosted.org/packages/84/c2/80633736cd183ee4a62107413def345f7e6e3c01563dbca1417363cf957e/build-1.2.2.post1-py3-none-any.whl", hash = "sha256:1d61c0887fa860c01971625baae8bdd338e517b836a2f70dd1f7aa3a6b2fc5b5", size = 22950, upload-time = "2024-10-06T17:22:23.299Z" }, ] [[package]] @@ -106,18 +137,18 @@ dependencies = [ { name = "lexid" }, { name = "toml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bb/a9/becf78cc86211bd2287114c4f990a3bed450816696f14810cc59d7815bb5/bumpver-2024.1130.tar.gz", hash = "sha256:74f7ebc294b2240f346e99748cc6f238e57b050999d7428db75d76baf2bf1437", size = 115102 } +sdist = { url = "https://files.pythonhosted.org/packages/bb/a9/becf78cc86211bd2287114c4f990a3bed450816696f14810cc59d7815bb5/bumpver-2024.1130.tar.gz", hash = "sha256:74f7ebc294b2240f346e99748cc6f238e57b050999d7428db75d76baf2bf1437", size = 115102, upload-time = "2024-11-10T20:51:53.072Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/09/34/57d038ae30374976ce4ec57db9dea95bf55d1b5543b35e77aa9ce3543198/bumpver-2024.1130-py2.py3-none-any.whl", hash = "sha256:8e54220aefe7db25148622f45959f7beb6b8513af0b0429b38b9072566665a49", size = 65273 }, + { url = "https://files.pythonhosted.org/packages/09/34/57d038ae30374976ce4ec57db9dea95bf55d1b5543b35e77aa9ce3543198/bumpver-2024.1130-py2.py3-none-any.whl", hash = "sha256:8e54220aefe7db25148622f45959f7beb6b8513af0b0429b38b9072566665a49", size = 65273, upload-time = "2024-11-10T20:51:50.383Z" }, ] [[package]] name = "certifi" version = "2025.1.31" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 } +sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577, upload-time = "2025-01-31T02:16:47.166Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 }, + { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393, upload-time = "2025-01-31T02:16:45.015Z" }, ] [[package]] @@ -127,116 +158,116 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pycparser" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/90/07/f44ca684db4e4f08a3fdc6eeb9a0d15dc6883efc7b8c90357fdbf74e186c/cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", size = 182191 }, - { url = "https://files.pythonhosted.org/packages/08/fd/cc2fedbd887223f9f5d170c96e57cbf655df9831a6546c1727ae13fa977a/cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", size = 178592 }, - { url = "https://files.pythonhosted.org/packages/de/cc/4635c320081c78d6ffc2cab0a76025b691a91204f4aa317d568ff9280a2d/cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", size = 426024 }, - { url = "https://files.pythonhosted.org/packages/b6/7b/3b2b250f3aab91abe5f8a51ada1b717935fdaec53f790ad4100fe2ec64d1/cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", size = 448188 }, - { url = "https://files.pythonhosted.org/packages/d3/48/1b9283ebbf0ec065148d8de05d647a986c5f22586b18120020452fff8f5d/cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", size = 455571 }, - { url = "https://files.pythonhosted.org/packages/40/87/3b8452525437b40f39ca7ff70276679772ee7e8b394934ff60e63b7b090c/cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", size = 436687 }, - { url = "https://files.pythonhosted.org/packages/8d/fb/4da72871d177d63649ac449aec2e8a29efe0274035880c7af59101ca2232/cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", size = 446211 }, - { url = "https://files.pythonhosted.org/packages/ab/a0/62f00bcb411332106c02b663b26f3545a9ef136f80d5df746c05878f8c4b/cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", size = 461325 }, - { url = "https://files.pythonhosted.org/packages/36/83/76127035ed2e7e27b0787604d99da630ac3123bfb02d8e80c633f218a11d/cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", size = 438784 }, - { url = "https://files.pythonhosted.org/packages/21/81/a6cd025db2f08ac88b901b745c163d884641909641f9b826e8cb87645942/cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", size = 461564 }, - { url = "https://files.pythonhosted.org/packages/f8/fe/4d41c2f200c4a457933dbd98d3cf4e911870877bd94d9656cc0fcb390681/cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", size = 171804 }, - { url = "https://files.pythonhosted.org/packages/d1/b6/0b0f5ab93b0df4acc49cae758c81fe4e5ef26c3ae2e10cc69249dfd8b3ab/cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", size = 181299 }, - { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264 }, - { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651 }, - { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259 }, - { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200 }, - { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235 }, - { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721 }, - { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242 }, - { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999 }, - { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242 }, - { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604 }, - { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727 }, - { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400 }, - { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178 }, - { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840 }, - { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803 }, - { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850 }, - { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729 }, - { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256 }, - { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424 }, - { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568 }, - { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736 }, - { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448 }, - { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976 }, - { url = "https://files.pythonhosted.org/packages/b9/ea/8bb50596b8ffbc49ddd7a1ad305035daa770202a6b782fc164647c2673ad/cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16", size = 182220 }, - { url = "https://files.pythonhosted.org/packages/ae/11/e77c8cd24f58285a82c23af484cf5b124a376b32644e445960d1a4654c3a/cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36", size = 178605 }, - { url = "https://files.pythonhosted.org/packages/ed/65/25a8dc32c53bf5b7b6c2686b42ae2ad58743f7ff644844af7cdb29b49361/cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8", size = 424910 }, - { url = "https://files.pythonhosted.org/packages/42/7a/9d086fab7c66bd7c4d0f27c57a1b6b068ced810afc498cc8c49e0088661c/cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576", size = 447200 }, - { url = "https://files.pythonhosted.org/packages/da/63/1785ced118ce92a993b0ec9e0d0ac8dc3e5dbfbcaa81135be56c69cabbb6/cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87", size = 454565 }, - { url = "https://files.pythonhosted.org/packages/74/06/90b8a44abf3556599cdec107f7290277ae8901a58f75e6fe8f970cd72418/cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0", size = 435635 }, - { url = "https://files.pythonhosted.org/packages/bd/62/a1f468e5708a70b1d86ead5bab5520861d9c7eacce4a885ded9faa7729c3/cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3", size = 445218 }, - { url = "https://files.pythonhosted.org/packages/5b/95/b34462f3ccb09c2594aa782d90a90b045de4ff1f70148ee79c69d37a0a5a/cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595", size = 460486 }, - { url = "https://files.pythonhosted.org/packages/fc/fc/a1e4bebd8d680febd29cf6c8a40067182b64f00c7d105f8f26b5bc54317b/cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a", size = 437911 }, - { url = "https://files.pythonhosted.org/packages/e6/c3/21cab7a6154b6a5ea330ae80de386e7665254835b9e98ecc1340b3a7de9a/cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e", size = 460632 }, - { url = "https://files.pythonhosted.org/packages/cb/b5/fd9f8b5a84010ca169ee49f4e4ad6f8c05f4e3545b72ee041dbbcb159882/cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7", size = 171820 }, - { url = "https://files.pythonhosted.org/packages/8c/52/b08750ce0bce45c143e1b5d7357ee8c55341b52bdef4b0f081af1eb248c2/cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662", size = 181290 }, +sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/07/f44ca684db4e4f08a3fdc6eeb9a0d15dc6883efc7b8c90357fdbf74e186c/cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", size = 182191, upload-time = "2024-09-04T20:43:30.027Z" }, + { url = "https://files.pythonhosted.org/packages/08/fd/cc2fedbd887223f9f5d170c96e57cbf655df9831a6546c1727ae13fa977a/cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", size = 178592, upload-time = "2024-09-04T20:43:32.108Z" }, + { url = "https://files.pythonhosted.org/packages/de/cc/4635c320081c78d6ffc2cab0a76025b691a91204f4aa317d568ff9280a2d/cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", size = 426024, upload-time = "2024-09-04T20:43:34.186Z" }, + { url = "https://files.pythonhosted.org/packages/b6/7b/3b2b250f3aab91abe5f8a51ada1b717935fdaec53f790ad4100fe2ec64d1/cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", size = 448188, upload-time = "2024-09-04T20:43:36.286Z" }, + { url = "https://files.pythonhosted.org/packages/d3/48/1b9283ebbf0ec065148d8de05d647a986c5f22586b18120020452fff8f5d/cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", size = 455571, upload-time = "2024-09-04T20:43:38.586Z" }, + { url = "https://files.pythonhosted.org/packages/40/87/3b8452525437b40f39ca7ff70276679772ee7e8b394934ff60e63b7b090c/cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", size = 436687, upload-time = "2024-09-04T20:43:40.084Z" }, + { url = "https://files.pythonhosted.org/packages/8d/fb/4da72871d177d63649ac449aec2e8a29efe0274035880c7af59101ca2232/cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", size = 446211, upload-time = "2024-09-04T20:43:41.526Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a0/62f00bcb411332106c02b663b26f3545a9ef136f80d5df746c05878f8c4b/cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", size = 461325, upload-time = "2024-09-04T20:43:43.117Z" }, + { url = "https://files.pythonhosted.org/packages/36/83/76127035ed2e7e27b0787604d99da630ac3123bfb02d8e80c633f218a11d/cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", size = 438784, upload-time = "2024-09-04T20:43:45.256Z" }, + { url = "https://files.pythonhosted.org/packages/21/81/a6cd025db2f08ac88b901b745c163d884641909641f9b826e8cb87645942/cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", size = 461564, upload-time = "2024-09-04T20:43:46.779Z" }, + { url = "https://files.pythonhosted.org/packages/f8/fe/4d41c2f200c4a457933dbd98d3cf4e911870877bd94d9656cc0fcb390681/cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", size = 171804, upload-time = "2024-09-04T20:43:48.186Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b6/0b0f5ab93b0df4acc49cae758c81fe4e5ef26c3ae2e10cc69249dfd8b3ab/cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", size = 181299, upload-time = "2024-09-04T20:43:49.812Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264, upload-time = "2024-09-04T20:43:51.124Z" }, + { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651, upload-time = "2024-09-04T20:43:52.872Z" }, + { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259, upload-time = "2024-09-04T20:43:56.123Z" }, + { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200, upload-time = "2024-09-04T20:43:57.891Z" }, + { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235, upload-time = "2024-09-04T20:44:00.18Z" }, + { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721, upload-time = "2024-09-04T20:44:01.585Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242, upload-time = "2024-09-04T20:44:03.467Z" }, + { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999, upload-time = "2024-09-04T20:44:05.023Z" }, + { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242, upload-time = "2024-09-04T20:44:06.444Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604, upload-time = "2024-09-04T20:44:08.206Z" }, + { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727, upload-time = "2024-09-04T20:44:09.481Z" }, + { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400, upload-time = "2024-09-04T20:44:10.873Z" }, + { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178, upload-time = "2024-09-04T20:44:12.232Z" }, + { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840, upload-time = "2024-09-04T20:44:13.739Z" }, + { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803, upload-time = "2024-09-04T20:44:15.231Z" }, + { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850, upload-time = "2024-09-04T20:44:17.188Z" }, + { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729, upload-time = "2024-09-04T20:44:18.688Z" }, + { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256, upload-time = "2024-09-04T20:44:20.248Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424, upload-time = "2024-09-04T20:44:21.673Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568, upload-time = "2024-09-04T20:44:23.245Z" }, + { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736, upload-time = "2024-09-04T20:44:24.757Z" }, + { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448, upload-time = "2024-09-04T20:44:26.208Z" }, + { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976, upload-time = "2024-09-04T20:44:27.578Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ea/8bb50596b8ffbc49ddd7a1ad305035daa770202a6b782fc164647c2673ad/cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16", size = 182220, upload-time = "2024-09-04T20:45:01.577Z" }, + { url = "https://files.pythonhosted.org/packages/ae/11/e77c8cd24f58285a82c23af484cf5b124a376b32644e445960d1a4654c3a/cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36", size = 178605, upload-time = "2024-09-04T20:45:03.837Z" }, + { url = "https://files.pythonhosted.org/packages/ed/65/25a8dc32c53bf5b7b6c2686b42ae2ad58743f7ff644844af7cdb29b49361/cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8", size = 424910, upload-time = "2024-09-04T20:45:05.315Z" }, + { url = "https://files.pythonhosted.org/packages/42/7a/9d086fab7c66bd7c4d0f27c57a1b6b068ced810afc498cc8c49e0088661c/cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576", size = 447200, upload-time = "2024-09-04T20:45:06.903Z" }, + { url = "https://files.pythonhosted.org/packages/da/63/1785ced118ce92a993b0ec9e0d0ac8dc3e5dbfbcaa81135be56c69cabbb6/cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87", size = 454565, upload-time = "2024-09-04T20:45:08.975Z" }, + { url = "https://files.pythonhosted.org/packages/74/06/90b8a44abf3556599cdec107f7290277ae8901a58f75e6fe8f970cd72418/cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0", size = 435635, upload-time = "2024-09-04T20:45:10.64Z" }, + { url = "https://files.pythonhosted.org/packages/bd/62/a1f468e5708a70b1d86ead5bab5520861d9c7eacce4a885ded9faa7729c3/cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3", size = 445218, upload-time = "2024-09-04T20:45:12.366Z" }, + { url = "https://files.pythonhosted.org/packages/5b/95/b34462f3ccb09c2594aa782d90a90b045de4ff1f70148ee79c69d37a0a5a/cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595", size = 460486, upload-time = "2024-09-04T20:45:13.935Z" }, + { url = "https://files.pythonhosted.org/packages/fc/fc/a1e4bebd8d680febd29cf6c8a40067182b64f00c7d105f8f26b5bc54317b/cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a", size = 437911, upload-time = "2024-09-04T20:45:15.696Z" }, + { url = "https://files.pythonhosted.org/packages/e6/c3/21cab7a6154b6a5ea330ae80de386e7665254835b9e98ecc1340b3a7de9a/cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e", size = 460632, upload-time = "2024-09-04T20:45:17.284Z" }, + { url = "https://files.pythonhosted.org/packages/cb/b5/fd9f8b5a84010ca169ee49f4e4ad6f8c05f4e3545b72ee041dbbcb159882/cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7", size = 171820, upload-time = "2024-09-04T20:45:18.762Z" }, + { url = "https://files.pythonhosted.org/packages/8c/52/b08750ce0bce45c143e1b5d7357ee8c55341b52bdef4b0f081af1eb248c2/cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662", size = 181290, upload-time = "2024-09-04T20:45:20.226Z" }, ] [[package]] name = "charset-normalizer" version = "3.4.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/58/5580c1716040bc89206c77d8f74418caf82ce519aae06450393ca73475d1/charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de", size = 198013 }, - { url = "https://files.pythonhosted.org/packages/d0/11/00341177ae71c6f5159a08168bcb98c6e6d196d372c94511f9f6c9afe0c6/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176", size = 141285 }, - { url = "https://files.pythonhosted.org/packages/01/09/11d684ea5819e5a8f5100fb0b38cf8d02b514746607934134d31233e02c8/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037", size = 151449 }, - { url = "https://files.pythonhosted.org/packages/08/06/9f5a12939db324d905dc1f70591ae7d7898d030d7662f0d426e2286f68c9/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f", size = 143892 }, - { url = "https://files.pythonhosted.org/packages/93/62/5e89cdfe04584cb7f4d36003ffa2936681b03ecc0754f8e969c2becb7e24/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a", size = 146123 }, - { url = "https://files.pythonhosted.org/packages/a9/ac/ab729a15c516da2ab70a05f8722ecfccc3f04ed7a18e45c75bbbaa347d61/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a", size = 147943 }, - { url = "https://files.pythonhosted.org/packages/03/d2/3f392f23f042615689456e9a274640c1d2e5dd1d52de36ab8f7955f8f050/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247", size = 142063 }, - { url = "https://files.pythonhosted.org/packages/f2/e3/e20aae5e1039a2cd9b08d9205f52142329f887f8cf70da3650326670bddf/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408", size = 150578 }, - { url = "https://files.pythonhosted.org/packages/8d/af/779ad72a4da0aed925e1139d458adc486e61076d7ecdcc09e610ea8678db/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb", size = 153629 }, - { url = "https://files.pythonhosted.org/packages/c2/b6/7aa450b278e7aa92cf7732140bfd8be21f5f29d5bf334ae987c945276639/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d", size = 150778 }, - { url = "https://files.pythonhosted.org/packages/39/f4/d9f4f712d0951dcbfd42920d3db81b00dd23b6ab520419626f4023334056/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807", size = 146453 }, - { url = "https://files.pythonhosted.org/packages/49/2b/999d0314e4ee0cff3cb83e6bc9aeddd397eeed693edb4facb901eb8fbb69/charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f", size = 95479 }, - { url = "https://files.pythonhosted.org/packages/2d/ce/3cbed41cff67e455a386fb5e5dd8906cdda2ed92fbc6297921f2e4419309/charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f", size = 102790 }, - { url = "https://files.pythonhosted.org/packages/72/80/41ef5d5a7935d2d3a773e3eaebf0a9350542f2cab4eac59a7a4741fbbbbe/charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125", size = 194995 }, - { url = "https://files.pythonhosted.org/packages/7a/28/0b9fefa7b8b080ec492110af6d88aa3dea91c464b17d53474b6e9ba5d2c5/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1", size = 139471 }, - { url = "https://files.pythonhosted.org/packages/71/64/d24ab1a997efb06402e3fc07317e94da358e2585165930d9d59ad45fcae2/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3", size = 149831 }, - { url = "https://files.pythonhosted.org/packages/37/ed/be39e5258e198655240db5e19e0b11379163ad7070962d6b0c87ed2c4d39/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd", size = 142335 }, - { url = "https://files.pythonhosted.org/packages/88/83/489e9504711fa05d8dde1574996408026bdbdbd938f23be67deebb5eca92/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00", size = 143862 }, - { url = "https://files.pythonhosted.org/packages/c6/c7/32da20821cf387b759ad24627a9aca289d2822de929b8a41b6241767b461/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12", size = 145673 }, - { url = "https://files.pythonhosted.org/packages/68/85/f4288e96039abdd5aeb5c546fa20a37b50da71b5cf01e75e87f16cd43304/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77", size = 140211 }, - { url = "https://files.pythonhosted.org/packages/28/a3/a42e70d03cbdabc18997baf4f0227c73591a08041c149e710045c281f97b/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146", size = 148039 }, - { url = "https://files.pythonhosted.org/packages/85/e4/65699e8ab3014ecbe6f5c71d1a55d810fb716bbfd74f6283d5c2aa87febf/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd", size = 151939 }, - { url = "https://files.pythonhosted.org/packages/b1/82/8e9fe624cc5374193de6860aba3ea8070f584c8565ee77c168ec13274bd2/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6", size = 149075 }, - { url = "https://files.pythonhosted.org/packages/3d/7b/82865ba54c765560c8433f65e8acb9217cb839a9e32b42af4aa8e945870f/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8", size = 144340 }, - { url = "https://files.pythonhosted.org/packages/b5/b6/9674a4b7d4d99a0d2df9b215da766ee682718f88055751e1e5e753c82db0/charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b", size = 95205 }, - { url = "https://files.pythonhosted.org/packages/1e/ab/45b180e175de4402dcf7547e4fb617283bae54ce35c27930a6f35b6bef15/charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76", size = 102441 }, - { url = "https://files.pythonhosted.org/packages/0a/9a/dd1e1cdceb841925b7798369a09279bd1cf183cef0f9ddf15a3a6502ee45/charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", size = 196105 }, - { url = "https://files.pythonhosted.org/packages/d3/8c/90bfabf8c4809ecb648f39794cf2a84ff2e7d2a6cf159fe68d9a26160467/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", size = 140404 }, - { url = "https://files.pythonhosted.org/packages/ad/8f/e410d57c721945ea3b4f1a04b74f70ce8fa800d393d72899f0a40526401f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", size = 150423 }, - { url = "https://files.pythonhosted.org/packages/f0/b8/e6825e25deb691ff98cf5c9072ee0605dc2acfca98af70c2d1b1bc75190d/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", size = 143184 }, - { url = "https://files.pythonhosted.org/packages/3e/a2/513f6cbe752421f16d969e32f3583762bfd583848b763913ddab8d9bfd4f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", size = 145268 }, - { url = "https://files.pythonhosted.org/packages/74/94/8a5277664f27c3c438546f3eb53b33f5b19568eb7424736bdc440a88a31f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616", size = 147601 }, - { url = "https://files.pythonhosted.org/packages/7c/5f/6d352c51ee763623a98e31194823518e09bfa48be2a7e8383cf691bbb3d0/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", size = 141098 }, - { url = "https://files.pythonhosted.org/packages/78/d4/f5704cb629ba5ab16d1d3d741396aec6dc3ca2b67757c45b0599bb010478/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", size = 149520 }, - { url = "https://files.pythonhosted.org/packages/c5/96/64120b1d02b81785f222b976c0fb79a35875457fa9bb40827678e54d1bc8/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", size = 152852 }, - { url = "https://files.pythonhosted.org/packages/84/c9/98e3732278a99f47d487fd3468bc60b882920cef29d1fa6ca460a1fdf4e6/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", size = 150488 }, - { url = "https://files.pythonhosted.org/packages/13/0e/9c8d4cb99c98c1007cc11eda969ebfe837bbbd0acdb4736d228ccaabcd22/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", size = 146192 }, - { url = "https://files.pythonhosted.org/packages/b2/21/2b6b5b860781a0b49427309cb8670785aa543fb2178de875b87b9cc97746/charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", size = 95550 }, - { url = "https://files.pythonhosted.org/packages/21/5b/1b390b03b1d16c7e382b561c5329f83cc06623916aab983e8ab9239c7d5c/charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", size = 102785 }, - { url = "https://files.pythonhosted.org/packages/7f/c0/b913f8f02836ed9ab32ea643c6fe4d3325c3d8627cf6e78098671cafff86/charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41", size = 197867 }, - { url = "https://files.pythonhosted.org/packages/0f/6c/2bee440303d705b6fb1e2ec789543edec83d32d258299b16eed28aad48e0/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f", size = 141385 }, - { url = "https://files.pythonhosted.org/packages/3d/04/cb42585f07f6f9fd3219ffb6f37d5a39b4fd2db2355b23683060029c35f7/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2", size = 151367 }, - { url = "https://files.pythonhosted.org/packages/54/54/2412a5b093acb17f0222de007cc129ec0e0df198b5ad2ce5699355269dfe/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770", size = 143928 }, - { url = "https://files.pythonhosted.org/packages/5a/6d/e2773862b043dcf8a221342954f375392bb2ce6487bcd9f2c1b34e1d6781/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4", size = 146203 }, - { url = "https://files.pythonhosted.org/packages/b9/f8/ca440ef60d8f8916022859885f231abb07ada3c347c03d63f283bec32ef5/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537", size = 148082 }, - { url = "https://files.pythonhosted.org/packages/04/d2/42fd330901aaa4b805a1097856c2edf5095e260a597f65def493f4b8c833/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496", size = 142053 }, - { url = "https://files.pythonhosted.org/packages/9e/af/3a97a4fa3c53586f1910dadfc916e9c4f35eeada36de4108f5096cb7215f/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78", size = 150625 }, - { url = "https://files.pythonhosted.org/packages/26/ae/23d6041322a3556e4da139663d02fb1b3c59a23ab2e2b56432bd2ad63ded/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7", size = 153549 }, - { url = "https://files.pythonhosted.org/packages/94/22/b8f2081c6a77cb20d97e57e0b385b481887aa08019d2459dc2858ed64871/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6", size = 150945 }, - { url = "https://files.pythonhosted.org/packages/c7/0b/c5ec5092747f801b8b093cdf5610e732b809d6cb11f4c51e35fc28d1d389/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294", size = 146595 }, - { url = "https://files.pythonhosted.org/packages/0c/5a/0b59704c38470df6768aa154cc87b1ac7c9bb687990a1559dc8765e8627e/charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5", size = 95453 }, - { url = "https://files.pythonhosted.org/packages/85/2d/a9790237cb4d01a6d57afadc8573c8b73c609ade20b80f4cda30802009ee/charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765", size = 102811 }, - { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 }, +sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188, upload-time = "2024-12-24T18:12:35.43Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/58/5580c1716040bc89206c77d8f74418caf82ce519aae06450393ca73475d1/charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de", size = 198013, upload-time = "2024-12-24T18:09:43.671Z" }, + { url = "https://files.pythonhosted.org/packages/d0/11/00341177ae71c6f5159a08168bcb98c6e6d196d372c94511f9f6c9afe0c6/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176", size = 141285, upload-time = "2024-12-24T18:09:48.113Z" }, + { url = "https://files.pythonhosted.org/packages/01/09/11d684ea5819e5a8f5100fb0b38cf8d02b514746607934134d31233e02c8/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037", size = 151449, upload-time = "2024-12-24T18:09:50.845Z" }, + { url = "https://files.pythonhosted.org/packages/08/06/9f5a12939db324d905dc1f70591ae7d7898d030d7662f0d426e2286f68c9/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f", size = 143892, upload-time = "2024-12-24T18:09:52.078Z" }, + { url = "https://files.pythonhosted.org/packages/93/62/5e89cdfe04584cb7f4d36003ffa2936681b03ecc0754f8e969c2becb7e24/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a", size = 146123, upload-time = "2024-12-24T18:09:54.575Z" }, + { url = "https://files.pythonhosted.org/packages/a9/ac/ab729a15c516da2ab70a05f8722ecfccc3f04ed7a18e45c75bbbaa347d61/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a", size = 147943, upload-time = "2024-12-24T18:09:57.324Z" }, + { url = "https://files.pythonhosted.org/packages/03/d2/3f392f23f042615689456e9a274640c1d2e5dd1d52de36ab8f7955f8f050/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247", size = 142063, upload-time = "2024-12-24T18:09:59.794Z" }, + { url = "https://files.pythonhosted.org/packages/f2/e3/e20aae5e1039a2cd9b08d9205f52142329f887f8cf70da3650326670bddf/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408", size = 150578, upload-time = "2024-12-24T18:10:02.357Z" }, + { url = "https://files.pythonhosted.org/packages/8d/af/779ad72a4da0aed925e1139d458adc486e61076d7ecdcc09e610ea8678db/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb", size = 153629, upload-time = "2024-12-24T18:10:03.678Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b6/7aa450b278e7aa92cf7732140bfd8be21f5f29d5bf334ae987c945276639/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d", size = 150778, upload-time = "2024-12-24T18:10:06.197Z" }, + { url = "https://files.pythonhosted.org/packages/39/f4/d9f4f712d0951dcbfd42920d3db81b00dd23b6ab520419626f4023334056/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807", size = 146453, upload-time = "2024-12-24T18:10:08.848Z" }, + { url = "https://files.pythonhosted.org/packages/49/2b/999d0314e4ee0cff3cb83e6bc9aeddd397eeed693edb4facb901eb8fbb69/charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f", size = 95479, upload-time = "2024-12-24T18:10:10.044Z" }, + { url = "https://files.pythonhosted.org/packages/2d/ce/3cbed41cff67e455a386fb5e5dd8906cdda2ed92fbc6297921f2e4419309/charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f", size = 102790, upload-time = "2024-12-24T18:10:11.323Z" }, + { url = "https://files.pythonhosted.org/packages/72/80/41ef5d5a7935d2d3a773e3eaebf0a9350542f2cab4eac59a7a4741fbbbbe/charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125", size = 194995, upload-time = "2024-12-24T18:10:12.838Z" }, + { url = "https://files.pythonhosted.org/packages/7a/28/0b9fefa7b8b080ec492110af6d88aa3dea91c464b17d53474b6e9ba5d2c5/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1", size = 139471, upload-time = "2024-12-24T18:10:14.101Z" }, + { url = "https://files.pythonhosted.org/packages/71/64/d24ab1a997efb06402e3fc07317e94da358e2585165930d9d59ad45fcae2/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3", size = 149831, upload-time = "2024-12-24T18:10:15.512Z" }, + { url = "https://files.pythonhosted.org/packages/37/ed/be39e5258e198655240db5e19e0b11379163ad7070962d6b0c87ed2c4d39/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd", size = 142335, upload-time = "2024-12-24T18:10:18.369Z" }, + { url = "https://files.pythonhosted.org/packages/88/83/489e9504711fa05d8dde1574996408026bdbdbd938f23be67deebb5eca92/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00", size = 143862, upload-time = "2024-12-24T18:10:19.743Z" }, + { url = "https://files.pythonhosted.org/packages/c6/c7/32da20821cf387b759ad24627a9aca289d2822de929b8a41b6241767b461/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12", size = 145673, upload-time = "2024-12-24T18:10:21.139Z" }, + { url = "https://files.pythonhosted.org/packages/68/85/f4288e96039abdd5aeb5c546fa20a37b50da71b5cf01e75e87f16cd43304/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77", size = 140211, upload-time = "2024-12-24T18:10:22.382Z" }, + { url = "https://files.pythonhosted.org/packages/28/a3/a42e70d03cbdabc18997baf4f0227c73591a08041c149e710045c281f97b/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146", size = 148039, upload-time = "2024-12-24T18:10:24.802Z" }, + { url = "https://files.pythonhosted.org/packages/85/e4/65699e8ab3014ecbe6f5c71d1a55d810fb716bbfd74f6283d5c2aa87febf/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd", size = 151939, upload-time = "2024-12-24T18:10:26.124Z" }, + { url = "https://files.pythonhosted.org/packages/b1/82/8e9fe624cc5374193de6860aba3ea8070f584c8565ee77c168ec13274bd2/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6", size = 149075, upload-time = "2024-12-24T18:10:30.027Z" }, + { url = "https://files.pythonhosted.org/packages/3d/7b/82865ba54c765560c8433f65e8acb9217cb839a9e32b42af4aa8e945870f/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8", size = 144340, upload-time = "2024-12-24T18:10:32.679Z" }, + { url = "https://files.pythonhosted.org/packages/b5/b6/9674a4b7d4d99a0d2df9b215da766ee682718f88055751e1e5e753c82db0/charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b", size = 95205, upload-time = "2024-12-24T18:10:34.724Z" }, + { url = "https://files.pythonhosted.org/packages/1e/ab/45b180e175de4402dcf7547e4fb617283bae54ce35c27930a6f35b6bef15/charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76", size = 102441, upload-time = "2024-12-24T18:10:37.574Z" }, + { url = "https://files.pythonhosted.org/packages/0a/9a/dd1e1cdceb841925b7798369a09279bd1cf183cef0f9ddf15a3a6502ee45/charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", size = 196105, upload-time = "2024-12-24T18:10:38.83Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8c/90bfabf8c4809ecb648f39794cf2a84ff2e7d2a6cf159fe68d9a26160467/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", size = 140404, upload-time = "2024-12-24T18:10:44.272Z" }, + { url = "https://files.pythonhosted.org/packages/ad/8f/e410d57c721945ea3b4f1a04b74f70ce8fa800d393d72899f0a40526401f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", size = 150423, upload-time = "2024-12-24T18:10:45.492Z" }, + { url = "https://files.pythonhosted.org/packages/f0/b8/e6825e25deb691ff98cf5c9072ee0605dc2acfca98af70c2d1b1bc75190d/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", size = 143184, upload-time = "2024-12-24T18:10:47.898Z" }, + { url = "https://files.pythonhosted.org/packages/3e/a2/513f6cbe752421f16d969e32f3583762bfd583848b763913ddab8d9bfd4f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", size = 145268, upload-time = "2024-12-24T18:10:50.589Z" }, + { url = "https://files.pythonhosted.org/packages/74/94/8a5277664f27c3c438546f3eb53b33f5b19568eb7424736bdc440a88a31f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616", size = 147601, upload-time = "2024-12-24T18:10:52.541Z" }, + { url = "https://files.pythonhosted.org/packages/7c/5f/6d352c51ee763623a98e31194823518e09bfa48be2a7e8383cf691bbb3d0/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", size = 141098, upload-time = "2024-12-24T18:10:53.789Z" }, + { url = "https://files.pythonhosted.org/packages/78/d4/f5704cb629ba5ab16d1d3d741396aec6dc3ca2b67757c45b0599bb010478/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", size = 149520, upload-time = "2024-12-24T18:10:55.048Z" }, + { url = "https://files.pythonhosted.org/packages/c5/96/64120b1d02b81785f222b976c0fb79a35875457fa9bb40827678e54d1bc8/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", size = 152852, upload-time = "2024-12-24T18:10:57.647Z" }, + { url = "https://files.pythonhosted.org/packages/84/c9/98e3732278a99f47d487fd3468bc60b882920cef29d1fa6ca460a1fdf4e6/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", size = 150488, upload-time = "2024-12-24T18:10:59.43Z" }, + { url = "https://files.pythonhosted.org/packages/13/0e/9c8d4cb99c98c1007cc11eda969ebfe837bbbd0acdb4736d228ccaabcd22/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", size = 146192, upload-time = "2024-12-24T18:11:00.676Z" }, + { url = "https://files.pythonhosted.org/packages/b2/21/2b6b5b860781a0b49427309cb8670785aa543fb2178de875b87b9cc97746/charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", size = 95550, upload-time = "2024-12-24T18:11:01.952Z" }, + { url = "https://files.pythonhosted.org/packages/21/5b/1b390b03b1d16c7e382b561c5329f83cc06623916aab983e8ab9239c7d5c/charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", size = 102785, upload-time = "2024-12-24T18:11:03.142Z" }, + { url = "https://files.pythonhosted.org/packages/7f/c0/b913f8f02836ed9ab32ea643c6fe4d3325c3d8627cf6e78098671cafff86/charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41", size = 197867, upload-time = "2024-12-24T18:12:10.438Z" }, + { url = "https://files.pythonhosted.org/packages/0f/6c/2bee440303d705b6fb1e2ec789543edec83d32d258299b16eed28aad48e0/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f", size = 141385, upload-time = "2024-12-24T18:12:11.847Z" }, + { url = "https://files.pythonhosted.org/packages/3d/04/cb42585f07f6f9fd3219ffb6f37d5a39b4fd2db2355b23683060029c35f7/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2", size = 151367, upload-time = "2024-12-24T18:12:13.177Z" }, + { url = "https://files.pythonhosted.org/packages/54/54/2412a5b093acb17f0222de007cc129ec0e0df198b5ad2ce5699355269dfe/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770", size = 143928, upload-time = "2024-12-24T18:12:14.497Z" }, + { url = "https://files.pythonhosted.org/packages/5a/6d/e2773862b043dcf8a221342954f375392bb2ce6487bcd9f2c1b34e1d6781/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4", size = 146203, upload-time = "2024-12-24T18:12:15.731Z" }, + { url = "https://files.pythonhosted.org/packages/b9/f8/ca440ef60d8f8916022859885f231abb07ada3c347c03d63f283bec32ef5/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537", size = 148082, upload-time = "2024-12-24T18:12:18.641Z" }, + { url = "https://files.pythonhosted.org/packages/04/d2/42fd330901aaa4b805a1097856c2edf5095e260a597f65def493f4b8c833/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496", size = 142053, upload-time = "2024-12-24T18:12:20.036Z" }, + { url = "https://files.pythonhosted.org/packages/9e/af/3a97a4fa3c53586f1910dadfc916e9c4f35eeada36de4108f5096cb7215f/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78", size = 150625, upload-time = "2024-12-24T18:12:22.804Z" }, + { url = "https://files.pythonhosted.org/packages/26/ae/23d6041322a3556e4da139663d02fb1b3c59a23ab2e2b56432bd2ad63ded/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7", size = 153549, upload-time = "2024-12-24T18:12:24.163Z" }, + { url = "https://files.pythonhosted.org/packages/94/22/b8f2081c6a77cb20d97e57e0b385b481887aa08019d2459dc2858ed64871/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6", size = 150945, upload-time = "2024-12-24T18:12:25.415Z" }, + { url = "https://files.pythonhosted.org/packages/c7/0b/c5ec5092747f801b8b093cdf5610e732b809d6cb11f4c51e35fc28d1d389/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294", size = 146595, upload-time = "2024-12-24T18:12:28.03Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5a/0b59704c38470df6768aa154cc87b1ac7c9bb687990a1559dc8765e8627e/charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5", size = 95453, upload-time = "2024-12-24T18:12:29.569Z" }, + { url = "https://files.pythonhosted.org/packages/85/2d/a9790237cb4d01a6d57afadc8573c8b73c609ade20b80f4cda30802009ee/charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765", size = 102811, upload-time = "2024-12-24T18:12:30.83Z" }, + { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767, upload-time = "2024-12-24T18:12:32.852Z" }, ] [[package]] @@ -246,18 +277,18 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 }, + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" }, ] [[package]] name = "colorama" version = "0.4.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] [[package]] @@ -267,9 +298,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d3/7a/359f4d5df2353f26172b3cc39ea32daa39af8de522205f512f458923e677/colorlog-6.9.0.tar.gz", hash = "sha256:bfba54a1b93b94f54e1f4fe48395725a3d92fd2a4af702f6bd70946bdc0c6ac2", size = 16624 } +sdist = { url = "https://files.pythonhosted.org/packages/d3/7a/359f4d5df2353f26172b3cc39ea32daa39af8de522205f512f458923e677/colorlog-6.9.0.tar.gz", hash = "sha256:bfba54a1b93b94f54e1f4fe48395725a3d92fd2a4af702f6bd70946bdc0c6ac2", size = 16624, upload-time = "2024-10-29T18:34:51.011Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e3/51/9b208e85196941db2f0654ad0357ca6388ab3ed67efdbfc799f35d1f83aa/colorlog-6.9.0-py3-none-any.whl", hash = "sha256:5906e71acd67cb07a71e779c47c4bcb45fb8c2993eebe9e5adcd6a6f1b283eff", size = 11424 }, + { url = "https://files.pythonhosted.org/packages/e3/51/9b208e85196941db2f0654ad0357ca6388ab3ed67efdbfc799f35d1f83aa/colorlog-6.9.0-py3-none-any.whl", hash = "sha256:5906e71acd67cb07a71e779c47c4bcb45fb8c2993eebe9e5adcd6a6f1b283eff", size = 11424, upload-time = "2024-10-29T18:34:49.815Z" }, ] [[package]] @@ -282,56 +313,56 @@ resolution-markers = [ "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", ] dependencies = [ - { name = "numpy", marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f5/f6/31a8f28b4a2a4fa0e01085e542f3081ab0588eff8e589d39d775172c9792/contourpy-1.3.0.tar.gz", hash = "sha256:7ffa0db17717a8ffb127efd0c95a4362d996b892c2904db72428d5b52e1938a4", size = 13464370 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6c/e0/be8dcc796cfdd96708933e0e2da99ba4bb8f9b2caa9d560a50f3f09a65f3/contourpy-1.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:880ea32e5c774634f9fcd46504bf9f080a41ad855f4fef54f5380f5133d343c7", size = 265366 }, - { url = "https://files.pythonhosted.org/packages/50/d6/c953b400219443535d412fcbbc42e7a5e823291236bc0bb88936e3cc9317/contourpy-1.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:76c905ef940a4474a6289c71d53122a4f77766eef23c03cd57016ce19d0f7b42", size = 249226 }, - { url = "https://files.pythonhosted.org/packages/6f/b4/6fffdf213ffccc28483c524b9dad46bb78332851133b36ad354b856ddc7c/contourpy-1.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92f8557cbb07415a4d6fa191f20fd9d2d9eb9c0b61d1b2f52a8926e43c6e9af7", size = 308460 }, - { url = "https://files.pythonhosted.org/packages/cf/6c/118fc917b4050f0afe07179a6dcbe4f3f4ec69b94f36c9e128c4af480fb8/contourpy-1.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36f965570cff02b874773c49bfe85562b47030805d7d8360748f3eca570f4cab", size = 347623 }, - { url = "https://files.pythonhosted.org/packages/f9/a4/30ff110a81bfe3abf7b9673284d21ddce8cc1278f6f77393c91199da4c90/contourpy-1.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cacd81e2d4b6f89c9f8a5b69b86490152ff39afc58a95af002a398273e5ce589", size = 317761 }, - { url = "https://files.pythonhosted.org/packages/99/e6/d11966962b1aa515f5586d3907ad019f4b812c04e4546cc19ebf62b5178e/contourpy-1.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69375194457ad0fad3a839b9e29aa0b0ed53bb54db1bfb6c3ae43d111c31ce41", size = 322015 }, - { url = "https://files.pythonhosted.org/packages/4d/e3/182383743751d22b7b59c3c753277b6aee3637049197624f333dac5b4c80/contourpy-1.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a52040312b1a858b5e31ef28c2e865376a386c60c0e248370bbea2d3f3b760d", size = 1262672 }, - { url = "https://files.pythonhosted.org/packages/78/53/974400c815b2e605f252c8fb9297e2204347d1755a5374354ee77b1ea259/contourpy-1.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3faeb2998e4fcb256542e8a926d08da08977f7f5e62cf733f3c211c2a5586223", size = 1321688 }, - { url = "https://files.pythonhosted.org/packages/52/29/99f849faed5593b2926a68a31882af98afbeac39c7fdf7de491d9c85ec6a/contourpy-1.3.0-cp310-cp310-win32.whl", hash = "sha256:36e0cff201bcb17a0a8ecc7f454fe078437fa6bda730e695a92f2d9932bd507f", size = 171145 }, - { url = "https://files.pythonhosted.org/packages/a9/97/3f89bba79ff6ff2b07a3cbc40aa693c360d5efa90d66e914f0ff03b95ec7/contourpy-1.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:87ddffef1dbe5e669b5c2440b643d3fdd8622a348fe1983fad7a0f0ccb1cd67b", size = 216019 }, - { url = "https://files.pythonhosted.org/packages/b3/1f/9375917786cb39270b0ee6634536c0e22abf225825602688990d8f5c6c19/contourpy-1.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0fa4c02abe6c446ba70d96ece336e621efa4aecae43eaa9b030ae5fb92b309ad", size = 266356 }, - { url = "https://files.pythonhosted.org/packages/05/46/9256dd162ea52790c127cb58cfc3b9e3413a6e3478917d1f811d420772ec/contourpy-1.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:834e0cfe17ba12f79963861e0f908556b2cedd52e1f75e6578801febcc6a9f49", size = 250915 }, - { url = "https://files.pythonhosted.org/packages/e1/5d/3056c167fa4486900dfbd7e26a2fdc2338dc58eee36d490a0ed3ddda5ded/contourpy-1.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dbc4c3217eee163fa3984fd1567632b48d6dfd29216da3ded3d7b844a8014a66", size = 310443 }, - { url = "https://files.pythonhosted.org/packages/ca/c2/1a612e475492e07f11c8e267ea5ec1ce0d89971be496c195e27afa97e14a/contourpy-1.3.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4865cd1d419e0c7a7bf6de1777b185eebdc51470800a9f42b9e9decf17762081", size = 348548 }, - { url = "https://files.pythonhosted.org/packages/45/cf/2c2fc6bb5874158277b4faf136847f0689e1b1a1f640a36d76d52e78907c/contourpy-1.3.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:303c252947ab4b14c08afeb52375b26781ccd6a5ccd81abcdfc1fafd14cf93c1", size = 319118 }, - { url = "https://files.pythonhosted.org/packages/03/33/003065374f38894cdf1040cef474ad0546368eea7e3a51d48b8a423961f8/contourpy-1.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:637f674226be46f6ba372fd29d9523dd977a291f66ab2a74fbeb5530bb3f445d", size = 323162 }, - { url = "https://files.pythonhosted.org/packages/42/80/e637326e85e4105a802e42959f56cff2cd39a6b5ef68d5d9aee3ea5f0e4c/contourpy-1.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:76a896b2f195b57db25d6b44e7e03f221d32fe318d03ede41f8b4d9ba1bff53c", size = 1265396 }, - { url = "https://files.pythonhosted.org/packages/7c/3b/8cbd6416ca1bbc0202b50f9c13b2e0b922b64be888f9d9ee88e6cfabfb51/contourpy-1.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e1fd23e9d01591bab45546c089ae89d926917a66dceb3abcf01f6105d927e2cb", size = 1324297 }, - { url = "https://files.pythonhosted.org/packages/4d/2c/021a7afaa52fe891f25535506cc861c30c3c4e5a1c1ce94215e04b293e72/contourpy-1.3.0-cp311-cp311-win32.whl", hash = "sha256:d402880b84df3bec6eab53cd0cf802cae6a2ef9537e70cf75e91618a3801c20c", size = 171808 }, - { url = "https://files.pythonhosted.org/packages/8d/2f/804f02ff30a7fae21f98198828d0857439ec4c91a96e20cf2d6c49372966/contourpy-1.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:6cb6cc968059db9c62cb35fbf70248f40994dfcd7aa10444bbf8b3faeb7c2d67", size = 217181 }, - { url = "https://files.pythonhosted.org/packages/c9/92/8e0bbfe6b70c0e2d3d81272b58c98ac69ff1a4329f18c73bd64824d8b12e/contourpy-1.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:570ef7cf892f0afbe5b2ee410c507ce12e15a5fa91017a0009f79f7d93a1268f", size = 267838 }, - { url = "https://files.pythonhosted.org/packages/e3/04/33351c5d5108460a8ce6d512307690b023f0cfcad5899499f5c83b9d63b1/contourpy-1.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:da84c537cb8b97d153e9fb208c221c45605f73147bd4cadd23bdae915042aad6", size = 251549 }, - { url = "https://files.pythonhosted.org/packages/51/3d/aa0fe6ae67e3ef9f178389e4caaaa68daf2f9024092aa3c6032e3d174670/contourpy-1.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0be4d8425bfa755e0fd76ee1e019636ccc7c29f77a7c86b4328a9eb6a26d0639", size = 303177 }, - { url = "https://files.pythonhosted.org/packages/56/c3/c85a7e3e0cab635575d3b657f9535443a6f5d20fac1a1911eaa4bbe1aceb/contourpy-1.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9c0da700bf58f6e0b65312d0a5e695179a71d0163957fa381bb3c1f72972537c", size = 341735 }, - { url = "https://files.pythonhosted.org/packages/dd/8d/20f7a211a7be966a53f474bc90b1a8202e9844b3f1ef85f3ae45a77151ee/contourpy-1.3.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eb8b141bb00fa977d9122636b16aa67d37fd40a3d8b52dd837e536d64b9a4d06", size = 314679 }, - { url = "https://files.pythonhosted.org/packages/6e/be/524e377567defac0e21a46e2a529652d165fed130a0d8a863219303cee18/contourpy-1.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3634b5385c6716c258d0419c46d05c8aa7dc8cb70326c9a4fb66b69ad2b52e09", size = 320549 }, - { url = "https://files.pythonhosted.org/packages/0f/96/fdb2552a172942d888915f3a6663812e9bc3d359d53dafd4289a0fb462f0/contourpy-1.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0dce35502151b6bd35027ac39ba6e5a44be13a68f55735c3612c568cac3805fd", size = 1263068 }, - { url = "https://files.pythonhosted.org/packages/2a/25/632eab595e3140adfa92f1322bf8915f68c932bac468e89eae9974cf1c00/contourpy-1.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aea348f053c645100612b333adc5983d87be69acdc6d77d3169c090d3b01dc35", size = 1322833 }, - { url = "https://files.pythonhosted.org/packages/73/e3/69738782e315a1d26d29d71a550dbbe3eb6c653b028b150f70c1a5f4f229/contourpy-1.3.0-cp312-cp312-win32.whl", hash = "sha256:90f73a5116ad1ba7174341ef3ea5c3150ddf20b024b98fb0c3b29034752c8aeb", size = 172681 }, - { url = "https://files.pythonhosted.org/packages/0c/89/9830ba00d88e43d15e53d64931e66b8792b46eb25e2050a88fec4a0df3d5/contourpy-1.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:b11b39aea6be6764f84360fce6c82211a9db32a7c7de8fa6dd5397cf1d079c3b", size = 218283 }, - { url = "https://files.pythonhosted.org/packages/b3/e3/b9f72758adb6ef7397327ceb8b9c39c75711affb220e4f53c745ea1d5a9a/contourpy-1.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a11077e395f67ffc2c44ec2418cfebed032cd6da3022a94fc227b6faf8e2acb8", size = 265518 }, - { url = "https://files.pythonhosted.org/packages/ec/22/19f5b948367ab5260fb41d842c7a78dae645603881ea6bc39738bcfcabf6/contourpy-1.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e8134301d7e204c88ed7ab50028ba06c683000040ede1d617298611f9dc6240c", size = 249350 }, - { url = "https://files.pythonhosted.org/packages/26/76/0c7d43263dd00ae21a91a24381b7e813d286a3294d95d179ef3a7b9fb1d7/contourpy-1.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e12968fdfd5bb45ffdf6192a590bd8ddd3ba9e58360b29683c6bb71a7b41edca", size = 309167 }, - { url = "https://files.pythonhosted.org/packages/96/3b/cadff6773e89f2a5a492c1a8068e21d3fccaf1a1c1df7d65e7c8e3ef60ba/contourpy-1.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fd2a0fc506eccaaa7595b7e1418951f213cf8255be2600f1ea1b61e46a60c55f", size = 348279 }, - { url = "https://files.pythonhosted.org/packages/e1/86/158cc43aa549d2081a955ab11c6bdccc7a22caacc2af93186d26f5f48746/contourpy-1.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4cfb5c62ce023dfc410d6059c936dcf96442ba40814aefbfa575425a3a7f19dc", size = 318519 }, - { url = "https://files.pythonhosted.org/packages/05/11/57335544a3027e9b96a05948c32e566328e3a2f84b7b99a325b7a06d2b06/contourpy-1.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68a32389b06b82c2fdd68276148d7b9275b5f5cf13e5417e4252f6d1a34f72a2", size = 321922 }, - { url = "https://files.pythonhosted.org/packages/0b/e3/02114f96543f4a1b694333b92a6dcd4f8eebbefcc3a5f3bbb1316634178f/contourpy-1.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:94e848a6b83da10898cbf1311a815f770acc9b6a3f2d646f330d57eb4e87592e", size = 1258017 }, - { url = "https://files.pythonhosted.org/packages/f3/3b/bfe4c81c6d5881c1c643dde6620be0b42bf8aab155976dd644595cfab95c/contourpy-1.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:d78ab28a03c854a873787a0a42254a0ccb3cb133c672f645c9f9c8f3ae9d0800", size = 1316773 }, - { url = "https://files.pythonhosted.org/packages/f1/17/c52d2970784383cafb0bd918b6fb036d98d96bbf0bc1befb5d1e31a07a70/contourpy-1.3.0-cp39-cp39-win32.whl", hash = "sha256:81cb5ed4952aae6014bc9d0421dec7c5835c9c8c31cdf51910b708f548cf58e5", size = 171353 }, - { url = "https://files.pythonhosted.org/packages/53/23/db9f69676308e094d3c45f20cc52e12d10d64f027541c995d89c11ad5c75/contourpy-1.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:14e262f67bd7e6eb6880bc564dcda30b15e351a594657e55b7eec94b6ef72843", size = 211817 }, - { url = "https://files.pythonhosted.org/packages/d1/09/60e486dc2b64c94ed33e58dcfb6f808192c03dfc5574c016218b9b7680dc/contourpy-1.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fe41b41505a5a33aeaed2a613dccaeaa74e0e3ead6dd6fd3a118fb471644fd6c", size = 261886 }, - { url = "https://files.pythonhosted.org/packages/19/20/b57f9f7174fcd439a7789fb47d764974ab646fa34d1790551de386457a8e/contourpy-1.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eca7e17a65f72a5133bdbec9ecf22401c62bcf4821361ef7811faee695799779", size = 311008 }, - { url = "https://files.pythonhosted.org/packages/74/fc/5040d42623a1845d4f17a418e590fd7a79ae8cb2bad2b2f83de63c3bdca4/contourpy-1.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:1ec4dc6bf570f5b22ed0d7efba0dfa9c5b9e0431aeea7581aa217542d9e809a4", size = 215690 }, - { url = "https://files.pythonhosted.org/packages/2b/24/dc3dcd77ac7460ab7e9d2b01a618cb31406902e50e605a8d6091f0a8f7cc/contourpy-1.3.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:00ccd0dbaad6d804ab259820fa7cb0b8036bda0686ef844d24125d8287178ce0", size = 261894 }, - { url = "https://files.pythonhosted.org/packages/b1/db/531642a01cfec39d1682e46b5457b07cf805e3c3c584ec27e2a6223f8f6c/contourpy-1.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ca947601224119117f7c19c9cdf6b3ab54c5726ef1d906aa4a69dfb6dd58102", size = 311099 }, - { url = "https://files.pythonhosted.org/packages/38/1e/94bda024d629f254143a134eead69e21c836429a2a6ce82209a00ddcb79a/contourpy-1.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c6ec93afeb848a0845a18989da3beca3eec2c0f852322efe21af1931147d12cb", size = 215838 }, + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/f6/31a8f28b4a2a4fa0e01085e542f3081ab0588eff8e589d39d775172c9792/contourpy-1.3.0.tar.gz", hash = "sha256:7ffa0db17717a8ffb127efd0c95a4362d996b892c2904db72428d5b52e1938a4", size = 13464370, upload-time = "2024-08-27T21:00:03.328Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/e0/be8dcc796cfdd96708933e0e2da99ba4bb8f9b2caa9d560a50f3f09a65f3/contourpy-1.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:880ea32e5c774634f9fcd46504bf9f080a41ad855f4fef54f5380f5133d343c7", size = 265366, upload-time = "2024-08-27T20:50:09.947Z" }, + { url = "https://files.pythonhosted.org/packages/50/d6/c953b400219443535d412fcbbc42e7a5e823291236bc0bb88936e3cc9317/contourpy-1.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:76c905ef940a4474a6289c71d53122a4f77766eef23c03cd57016ce19d0f7b42", size = 249226, upload-time = "2024-08-27T20:50:16.1Z" }, + { url = "https://files.pythonhosted.org/packages/6f/b4/6fffdf213ffccc28483c524b9dad46bb78332851133b36ad354b856ddc7c/contourpy-1.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92f8557cbb07415a4d6fa191f20fd9d2d9eb9c0b61d1b2f52a8926e43c6e9af7", size = 308460, upload-time = "2024-08-27T20:50:22.536Z" }, + { url = "https://files.pythonhosted.org/packages/cf/6c/118fc917b4050f0afe07179a6dcbe4f3f4ec69b94f36c9e128c4af480fb8/contourpy-1.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36f965570cff02b874773c49bfe85562b47030805d7d8360748f3eca570f4cab", size = 347623, upload-time = "2024-08-27T20:50:28.806Z" }, + { url = "https://files.pythonhosted.org/packages/f9/a4/30ff110a81bfe3abf7b9673284d21ddce8cc1278f6f77393c91199da4c90/contourpy-1.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cacd81e2d4b6f89c9f8a5b69b86490152ff39afc58a95af002a398273e5ce589", size = 317761, upload-time = "2024-08-27T20:50:35.126Z" }, + { url = "https://files.pythonhosted.org/packages/99/e6/d11966962b1aa515f5586d3907ad019f4b812c04e4546cc19ebf62b5178e/contourpy-1.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69375194457ad0fad3a839b9e29aa0b0ed53bb54db1bfb6c3ae43d111c31ce41", size = 322015, upload-time = "2024-08-27T20:50:40.318Z" }, + { url = "https://files.pythonhosted.org/packages/4d/e3/182383743751d22b7b59c3c753277b6aee3637049197624f333dac5b4c80/contourpy-1.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a52040312b1a858b5e31ef28c2e865376a386c60c0e248370bbea2d3f3b760d", size = 1262672, upload-time = "2024-08-27T20:50:55.643Z" }, + { url = "https://files.pythonhosted.org/packages/78/53/974400c815b2e605f252c8fb9297e2204347d1755a5374354ee77b1ea259/contourpy-1.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3faeb2998e4fcb256542e8a926d08da08977f7f5e62cf733f3c211c2a5586223", size = 1321688, upload-time = "2024-08-27T20:51:11.293Z" }, + { url = "https://files.pythonhosted.org/packages/52/29/99f849faed5593b2926a68a31882af98afbeac39c7fdf7de491d9c85ec6a/contourpy-1.3.0-cp310-cp310-win32.whl", hash = "sha256:36e0cff201bcb17a0a8ecc7f454fe078437fa6bda730e695a92f2d9932bd507f", size = 171145, upload-time = "2024-08-27T20:51:15.2Z" }, + { url = "https://files.pythonhosted.org/packages/a9/97/3f89bba79ff6ff2b07a3cbc40aa693c360d5efa90d66e914f0ff03b95ec7/contourpy-1.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:87ddffef1dbe5e669b5c2440b643d3fdd8622a348fe1983fad7a0f0ccb1cd67b", size = 216019, upload-time = "2024-08-27T20:51:19.365Z" }, + { url = "https://files.pythonhosted.org/packages/b3/1f/9375917786cb39270b0ee6634536c0e22abf225825602688990d8f5c6c19/contourpy-1.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0fa4c02abe6c446ba70d96ece336e621efa4aecae43eaa9b030ae5fb92b309ad", size = 266356, upload-time = "2024-08-27T20:51:24.146Z" }, + { url = "https://files.pythonhosted.org/packages/05/46/9256dd162ea52790c127cb58cfc3b9e3413a6e3478917d1f811d420772ec/contourpy-1.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:834e0cfe17ba12f79963861e0f908556b2cedd52e1f75e6578801febcc6a9f49", size = 250915, upload-time = "2024-08-27T20:51:28.683Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5d/3056c167fa4486900dfbd7e26a2fdc2338dc58eee36d490a0ed3ddda5ded/contourpy-1.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dbc4c3217eee163fa3984fd1567632b48d6dfd29216da3ded3d7b844a8014a66", size = 310443, upload-time = "2024-08-27T20:51:33.675Z" }, + { url = "https://files.pythonhosted.org/packages/ca/c2/1a612e475492e07f11c8e267ea5ec1ce0d89971be496c195e27afa97e14a/contourpy-1.3.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4865cd1d419e0c7a7bf6de1777b185eebdc51470800a9f42b9e9decf17762081", size = 348548, upload-time = "2024-08-27T20:51:39.322Z" }, + { url = "https://files.pythonhosted.org/packages/45/cf/2c2fc6bb5874158277b4faf136847f0689e1b1a1f640a36d76d52e78907c/contourpy-1.3.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:303c252947ab4b14c08afeb52375b26781ccd6a5ccd81abcdfc1fafd14cf93c1", size = 319118, upload-time = "2024-08-27T20:51:44.717Z" }, + { url = "https://files.pythonhosted.org/packages/03/33/003065374f38894cdf1040cef474ad0546368eea7e3a51d48b8a423961f8/contourpy-1.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:637f674226be46f6ba372fd29d9523dd977a291f66ab2a74fbeb5530bb3f445d", size = 323162, upload-time = "2024-08-27T20:51:49.683Z" }, + { url = "https://files.pythonhosted.org/packages/42/80/e637326e85e4105a802e42959f56cff2cd39a6b5ef68d5d9aee3ea5f0e4c/contourpy-1.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:76a896b2f195b57db25d6b44e7e03f221d32fe318d03ede41f8b4d9ba1bff53c", size = 1265396, upload-time = "2024-08-27T20:52:04.926Z" }, + { url = "https://files.pythonhosted.org/packages/7c/3b/8cbd6416ca1bbc0202b50f9c13b2e0b922b64be888f9d9ee88e6cfabfb51/contourpy-1.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e1fd23e9d01591bab45546c089ae89d926917a66dceb3abcf01f6105d927e2cb", size = 1324297, upload-time = "2024-08-27T20:52:21.843Z" }, + { url = "https://files.pythonhosted.org/packages/4d/2c/021a7afaa52fe891f25535506cc861c30c3c4e5a1c1ce94215e04b293e72/contourpy-1.3.0-cp311-cp311-win32.whl", hash = "sha256:d402880b84df3bec6eab53cd0cf802cae6a2ef9537e70cf75e91618a3801c20c", size = 171808, upload-time = "2024-08-27T20:52:25.163Z" }, + { url = "https://files.pythonhosted.org/packages/8d/2f/804f02ff30a7fae21f98198828d0857439ec4c91a96e20cf2d6c49372966/contourpy-1.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:6cb6cc968059db9c62cb35fbf70248f40994dfcd7aa10444bbf8b3faeb7c2d67", size = 217181, upload-time = "2024-08-27T20:52:29.13Z" }, + { url = "https://files.pythonhosted.org/packages/c9/92/8e0bbfe6b70c0e2d3d81272b58c98ac69ff1a4329f18c73bd64824d8b12e/contourpy-1.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:570ef7cf892f0afbe5b2ee410c507ce12e15a5fa91017a0009f79f7d93a1268f", size = 267838, upload-time = "2024-08-27T20:52:33.911Z" }, + { url = "https://files.pythonhosted.org/packages/e3/04/33351c5d5108460a8ce6d512307690b023f0cfcad5899499f5c83b9d63b1/contourpy-1.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:da84c537cb8b97d153e9fb208c221c45605f73147bd4cadd23bdae915042aad6", size = 251549, upload-time = "2024-08-27T20:52:39.179Z" }, + { url = "https://files.pythonhosted.org/packages/51/3d/aa0fe6ae67e3ef9f178389e4caaaa68daf2f9024092aa3c6032e3d174670/contourpy-1.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0be4d8425bfa755e0fd76ee1e019636ccc7c29f77a7c86b4328a9eb6a26d0639", size = 303177, upload-time = "2024-08-27T20:52:44.789Z" }, + { url = "https://files.pythonhosted.org/packages/56/c3/c85a7e3e0cab635575d3b657f9535443a6f5d20fac1a1911eaa4bbe1aceb/contourpy-1.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9c0da700bf58f6e0b65312d0a5e695179a71d0163957fa381bb3c1f72972537c", size = 341735, upload-time = "2024-08-27T20:52:51.05Z" }, + { url = "https://files.pythonhosted.org/packages/dd/8d/20f7a211a7be966a53f474bc90b1a8202e9844b3f1ef85f3ae45a77151ee/contourpy-1.3.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eb8b141bb00fa977d9122636b16aa67d37fd40a3d8b52dd837e536d64b9a4d06", size = 314679, upload-time = "2024-08-27T20:52:58.473Z" }, + { url = "https://files.pythonhosted.org/packages/6e/be/524e377567defac0e21a46e2a529652d165fed130a0d8a863219303cee18/contourpy-1.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3634b5385c6716c258d0419c46d05c8aa7dc8cb70326c9a4fb66b69ad2b52e09", size = 320549, upload-time = "2024-08-27T20:53:06.593Z" }, + { url = "https://files.pythonhosted.org/packages/0f/96/fdb2552a172942d888915f3a6663812e9bc3d359d53dafd4289a0fb462f0/contourpy-1.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0dce35502151b6bd35027ac39ba6e5a44be13a68f55735c3612c568cac3805fd", size = 1263068, upload-time = "2024-08-27T20:53:23.442Z" }, + { url = "https://files.pythonhosted.org/packages/2a/25/632eab595e3140adfa92f1322bf8915f68c932bac468e89eae9974cf1c00/contourpy-1.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aea348f053c645100612b333adc5983d87be69acdc6d77d3169c090d3b01dc35", size = 1322833, upload-time = "2024-08-27T20:53:39.243Z" }, + { url = "https://files.pythonhosted.org/packages/73/e3/69738782e315a1d26d29d71a550dbbe3eb6c653b028b150f70c1a5f4f229/contourpy-1.3.0-cp312-cp312-win32.whl", hash = "sha256:90f73a5116ad1ba7174341ef3ea5c3150ddf20b024b98fb0c3b29034752c8aeb", size = 172681, upload-time = "2024-08-27T20:53:43.05Z" }, + { url = "https://files.pythonhosted.org/packages/0c/89/9830ba00d88e43d15e53d64931e66b8792b46eb25e2050a88fec4a0df3d5/contourpy-1.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:b11b39aea6be6764f84360fce6c82211a9db32a7c7de8fa6dd5397cf1d079c3b", size = 218283, upload-time = "2024-08-27T20:53:47.232Z" }, + { url = "https://files.pythonhosted.org/packages/b3/e3/b9f72758adb6ef7397327ceb8b9c39c75711affb220e4f53c745ea1d5a9a/contourpy-1.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a11077e395f67ffc2c44ec2418cfebed032cd6da3022a94fc227b6faf8e2acb8", size = 265518, upload-time = "2024-08-27T20:56:01.333Z" }, + { url = "https://files.pythonhosted.org/packages/ec/22/19f5b948367ab5260fb41d842c7a78dae645603881ea6bc39738bcfcabf6/contourpy-1.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e8134301d7e204c88ed7ab50028ba06c683000040ede1d617298611f9dc6240c", size = 249350, upload-time = "2024-08-27T20:56:05.432Z" }, + { url = "https://files.pythonhosted.org/packages/26/76/0c7d43263dd00ae21a91a24381b7e813d286a3294d95d179ef3a7b9fb1d7/contourpy-1.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e12968fdfd5bb45ffdf6192a590bd8ddd3ba9e58360b29683c6bb71a7b41edca", size = 309167, upload-time = "2024-08-27T20:56:10.034Z" }, + { url = "https://files.pythonhosted.org/packages/96/3b/cadff6773e89f2a5a492c1a8068e21d3fccaf1a1c1df7d65e7c8e3ef60ba/contourpy-1.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fd2a0fc506eccaaa7595b7e1418951f213cf8255be2600f1ea1b61e46a60c55f", size = 348279, upload-time = "2024-08-27T20:56:15.41Z" }, + { url = "https://files.pythonhosted.org/packages/e1/86/158cc43aa549d2081a955ab11c6bdccc7a22caacc2af93186d26f5f48746/contourpy-1.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4cfb5c62ce023dfc410d6059c936dcf96442ba40814aefbfa575425a3a7f19dc", size = 318519, upload-time = "2024-08-27T20:56:21.813Z" }, + { url = "https://files.pythonhosted.org/packages/05/11/57335544a3027e9b96a05948c32e566328e3a2f84b7b99a325b7a06d2b06/contourpy-1.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68a32389b06b82c2fdd68276148d7b9275b5f5cf13e5417e4252f6d1a34f72a2", size = 321922, upload-time = "2024-08-27T20:56:26.983Z" }, + { url = "https://files.pythonhosted.org/packages/0b/e3/02114f96543f4a1b694333b92a6dcd4f8eebbefcc3a5f3bbb1316634178f/contourpy-1.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:94e848a6b83da10898cbf1311a815f770acc9b6a3f2d646f330d57eb4e87592e", size = 1258017, upload-time = "2024-08-27T20:56:42.246Z" }, + { url = "https://files.pythonhosted.org/packages/f3/3b/bfe4c81c6d5881c1c643dde6620be0b42bf8aab155976dd644595cfab95c/contourpy-1.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:d78ab28a03c854a873787a0a42254a0ccb3cb133c672f645c9f9c8f3ae9d0800", size = 1316773, upload-time = "2024-08-27T20:56:58.58Z" }, + { url = "https://files.pythonhosted.org/packages/f1/17/c52d2970784383cafb0bd918b6fb036d98d96bbf0bc1befb5d1e31a07a70/contourpy-1.3.0-cp39-cp39-win32.whl", hash = "sha256:81cb5ed4952aae6014bc9d0421dec7c5835c9c8c31cdf51910b708f548cf58e5", size = 171353, upload-time = "2024-08-27T20:57:02.718Z" }, + { url = "https://files.pythonhosted.org/packages/53/23/db9f69676308e094d3c45f20cc52e12d10d64f027541c995d89c11ad5c75/contourpy-1.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:14e262f67bd7e6eb6880bc564dcda30b15e351a594657e55b7eec94b6ef72843", size = 211817, upload-time = "2024-08-27T20:57:06.328Z" }, + { url = "https://files.pythonhosted.org/packages/d1/09/60e486dc2b64c94ed33e58dcfb6f808192c03dfc5574c016218b9b7680dc/contourpy-1.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fe41b41505a5a33aeaed2a613dccaeaa74e0e3ead6dd6fd3a118fb471644fd6c", size = 261886, upload-time = "2024-08-27T20:57:10.863Z" }, + { url = "https://files.pythonhosted.org/packages/19/20/b57f9f7174fcd439a7789fb47d764974ab646fa34d1790551de386457a8e/contourpy-1.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eca7e17a65f72a5133bdbec9ecf22401c62bcf4821361ef7811faee695799779", size = 311008, upload-time = "2024-08-27T20:57:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/74/fc/5040d42623a1845d4f17a418e590fd7a79ae8cb2bad2b2f83de63c3bdca4/contourpy-1.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:1ec4dc6bf570f5b22ed0d7efba0dfa9c5b9e0431aeea7581aa217542d9e809a4", size = 215690, upload-time = "2024-08-27T20:57:19.321Z" }, + { url = "https://files.pythonhosted.org/packages/2b/24/dc3dcd77ac7460ab7e9d2b01a618cb31406902e50e605a8d6091f0a8f7cc/contourpy-1.3.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:00ccd0dbaad6d804ab259820fa7cb0b8036bda0686ef844d24125d8287178ce0", size = 261894, upload-time = "2024-08-27T20:57:23.873Z" }, + { url = "https://files.pythonhosted.org/packages/b1/db/531642a01cfec39d1682e46b5457b07cf805e3c3c584ec27e2a6223f8f6c/contourpy-1.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ca947601224119117f7c19c9cdf6b3ab54c5726ef1d906aa4a69dfb6dd58102", size = 311099, upload-time = "2024-08-27T20:57:28.58Z" }, + { url = "https://files.pythonhosted.org/packages/38/1e/94bda024d629f254143a134eead69e21c836429a2a6ce82209a00ddcb79a/contourpy-1.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c6ec93afeb848a0845a18989da3beca3eec2c0f852322efe21af1931147d12cb", size = 215838, upload-time = "2024-08-27T20:57:32.913Z" }, ] [[package]] @@ -347,100 +378,100 @@ resolution-markers = [ "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", ] dependencies = [ - { name = "numpy", marker = "python_full_version >= '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/25/c2/fc7193cc5383637ff390a712e88e4ded0452c9fbcf84abe3de5ea3df1866/contourpy-1.3.1.tar.gz", hash = "sha256:dfd97abd83335045a913e3bcc4a09c0ceadbe66580cf573fe961f4a825efa699", size = 13465753 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b2/a3/80937fe3efe0edacf67c9a20b955139a1a622730042c1ea991956f2704ad/contourpy-1.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a045f341a77b77e1c5de31e74e966537bba9f3c4099b35bf4c2e3939dd54cdab", size = 268466 }, - { url = "https://files.pythonhosted.org/packages/82/1d/e3eaebb4aa2d7311528c048350ca8e99cdacfafd99da87bc0a5f8d81f2c2/contourpy-1.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:500360b77259914f7805af7462e41f9cb7ca92ad38e9f94d6c8641b089338124", size = 253314 }, - { url = "https://files.pythonhosted.org/packages/de/f3/d796b22d1a2b587acc8100ba8c07fb7b5e17fde265a7bb05ab967f4c935a/contourpy-1.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2f926efda994cdf3c8d3fdb40b9962f86edbc4457e739277b961eced3d0b4c1", size = 312003 }, - { url = "https://files.pythonhosted.org/packages/bf/f5/0e67902bc4394daee8daa39c81d4f00b50e063ee1a46cb3938cc65585d36/contourpy-1.3.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:adce39d67c0edf383647a3a007de0a45fd1b08dedaa5318404f1a73059c2512b", size = 351896 }, - { url = "https://files.pythonhosted.org/packages/1f/d6/e766395723f6256d45d6e67c13bb638dd1fa9dc10ef912dc7dd3dcfc19de/contourpy-1.3.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abbb49fb7dac584e5abc6636b7b2a7227111c4f771005853e7d25176daaf8453", size = 320814 }, - { url = "https://files.pythonhosted.org/packages/a9/57/86c500d63b3e26e5b73a28b8291a67c5608d4aa87ebd17bd15bb33c178bc/contourpy-1.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0cffcbede75c059f535725c1680dfb17b6ba8753f0c74b14e6a9c68c29d7ea3", size = 324969 }, - { url = "https://files.pythonhosted.org/packages/b8/62/bb146d1289d6b3450bccc4642e7f4413b92ebffd9bf2e91b0404323704a7/contourpy-1.3.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ab29962927945d89d9b293eabd0d59aea28d887d4f3be6c22deaefbb938a7277", size = 1265162 }, - { url = "https://files.pythonhosted.org/packages/18/04/9f7d132ce49a212c8e767042cc80ae390f728060d2eea47058f55b9eff1c/contourpy-1.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:974d8145f8ca354498005b5b981165b74a195abfae9a8129df3e56771961d595", size = 1324328 }, - { url = "https://files.pythonhosted.org/packages/46/23/196813901be3f97c83ababdab1382e13e0edc0bb4e7b49a7bff15fcf754e/contourpy-1.3.1-cp310-cp310-win32.whl", hash = "sha256:ac4578ac281983f63b400f7fe6c101bedc10651650eef012be1ccffcbacf3697", size = 173861 }, - { url = "https://files.pythonhosted.org/packages/e0/82/c372be3fc000a3b2005061ca623a0d1ecd2eaafb10d9e883a2fc8566e951/contourpy-1.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:174e758c66bbc1c8576992cec9599ce8b6672b741b5d336b5c74e35ac382b18e", size = 218566 }, - { url = "https://files.pythonhosted.org/packages/12/bb/11250d2906ee2e8b466b5f93e6b19d525f3e0254ac8b445b56e618527718/contourpy-1.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3e8b974d8db2c5610fb4e76307e265de0edb655ae8169e8b21f41807ccbeec4b", size = 269555 }, - { url = "https://files.pythonhosted.org/packages/67/71/1e6e95aee21a500415f5d2dbf037bf4567529b6a4e986594d7026ec5ae90/contourpy-1.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:20914c8c973f41456337652a6eeca26d2148aa96dd7ac323b74516988bea89fc", size = 254549 }, - { url = "https://files.pythonhosted.org/packages/31/2c/b88986e8d79ac45efe9d8801ae341525f38e087449b6c2f2e6050468a42c/contourpy-1.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19d40d37c1c3a4961b4619dd9d77b12124a453cc3d02bb31a07d58ef684d3d86", size = 313000 }, - { url = "https://files.pythonhosted.org/packages/c4/18/65280989b151fcf33a8352f992eff71e61b968bef7432fbfde3a364f0730/contourpy-1.3.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:113231fe3825ebf6f15eaa8bc1f5b0ddc19d42b733345eae0934cb291beb88b6", size = 352925 }, - { url = "https://files.pythonhosted.org/packages/f5/c7/5fd0146c93220dbfe1a2e0f98969293b86ca9bc041d6c90c0e065f4619ad/contourpy-1.3.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4dbbc03a40f916a8420e420d63e96a1258d3d1b58cbdfd8d1f07b49fcbd38e85", size = 323693 }, - { url = "https://files.pythonhosted.org/packages/85/fc/7fa5d17daf77306840a4e84668a48ddff09e6bc09ba4e37e85ffc8e4faa3/contourpy-1.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a04ecd68acbd77fa2d39723ceca4c3197cb2969633836ced1bea14e219d077c", size = 326184 }, - { url = "https://files.pythonhosted.org/packages/ef/e7/104065c8270c7397c9571620d3ab880558957216f2b5ebb7e040f85eeb22/contourpy-1.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c414fc1ed8ee1dbd5da626cf3710c6013d3d27456651d156711fa24f24bd1291", size = 1268031 }, - { url = "https://files.pythonhosted.org/packages/e2/4a/c788d0bdbf32c8113c2354493ed291f924d4793c4a2e85b69e737a21a658/contourpy-1.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:31c1b55c1f34f80557d3830d3dd93ba722ce7e33a0b472cba0ec3b6535684d8f", size = 1325995 }, - { url = "https://files.pythonhosted.org/packages/a6/e6/a2f351a90d955f8b0564caf1ebe4b1451a3f01f83e5e3a414055a5b8bccb/contourpy-1.3.1-cp311-cp311-win32.whl", hash = "sha256:f611e628ef06670df83fce17805c344710ca5cde01edfdc72751311da8585375", size = 174396 }, - { url = "https://files.pythonhosted.org/packages/a8/7e/cd93cab453720a5d6cb75588cc17dcdc08fc3484b9de98b885924ff61900/contourpy-1.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:b2bdca22a27e35f16794cf585832e542123296b4687f9fd96822db6bae17bfc9", size = 219787 }, - { url = "https://files.pythonhosted.org/packages/37/6b/175f60227d3e7f5f1549fcb374592be311293132207e451c3d7c654c25fb/contourpy-1.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0ffa84be8e0bd33410b17189f7164c3589c229ce5db85798076a3fa136d0e509", size = 271494 }, - { url = "https://files.pythonhosted.org/packages/6b/6a/7833cfae2c1e63d1d8875a50fd23371394f540ce809d7383550681a1fa64/contourpy-1.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:805617228ba7e2cbbfb6c503858e626ab528ac2a32a04a2fe88ffaf6b02c32bc", size = 255444 }, - { url = "https://files.pythonhosted.org/packages/7f/b3/7859efce66eaca5c14ba7619791b084ed02d868d76b928ff56890d2d059d/contourpy-1.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ade08d343436a94e633db932e7e8407fe7de8083967962b46bdfc1b0ced39454", size = 307628 }, - { url = "https://files.pythonhosted.org/packages/48/b2/011415f5e3f0a50b1e285a0bf78eb5d92a4df000553570f0851b6e309076/contourpy-1.3.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:47734d7073fb4590b4a40122b35917cd77be5722d80683b249dac1de266aac80", size = 347271 }, - { url = "https://files.pythonhosted.org/packages/84/7d/ef19b1db0f45b151ac78c65127235239a8cf21a59d1ce8507ce03e89a30b/contourpy-1.3.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2ba94a401342fc0f8b948e57d977557fbf4d515f03c67682dd5c6191cb2d16ec", size = 318906 }, - { url = "https://files.pythonhosted.org/packages/ba/99/6794142b90b853a9155316c8f470d2e4821fe6f086b03e372aca848227dd/contourpy-1.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efa874e87e4a647fd2e4f514d5e91c7d493697127beb95e77d2f7561f6905bd9", size = 323622 }, - { url = "https://files.pythonhosted.org/packages/3c/0f/37d2c84a900cd8eb54e105f4fa9aebd275e14e266736778bb5dccbf3bbbb/contourpy-1.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1bf98051f1045b15c87868dbaea84f92408337d4f81d0e449ee41920ea121d3b", size = 1266699 }, - { url = "https://files.pythonhosted.org/packages/3a/8a/deb5e11dc7d9cc8f0f9c8b29d4f062203f3af230ba83c30a6b161a6effc9/contourpy-1.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:61332c87493b00091423e747ea78200659dc09bdf7fd69edd5e98cef5d3e9a8d", size = 1326395 }, - { url = "https://files.pythonhosted.org/packages/1a/35/7e267ae7c13aaf12322ccc493531f1e7f2eb8fba2927b9d7a05ff615df7a/contourpy-1.3.1-cp312-cp312-win32.whl", hash = "sha256:e914a8cb05ce5c809dd0fe350cfbb4e881bde5e2a38dc04e3afe1b3e58bd158e", size = 175354 }, - { url = "https://files.pythonhosted.org/packages/a1/35/c2de8823211d07e8a79ab018ef03960716c5dff6f4d5bff5af87fd682992/contourpy-1.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:08d9d449a61cf53033612cb368f3a1b26cd7835d9b8cd326647efe43bca7568d", size = 220971 }, - { url = "https://files.pythonhosted.org/packages/3e/4f/e56862e64b52b55b5ddcff4090085521fc228ceb09a88390a2b103dccd1b/contourpy-1.3.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b457d6430833cee8e4b8e9b6f07aa1c161e5e0d52e118dc102c8f9bd7dd060d6", size = 265605 }, - { url = "https://files.pythonhosted.org/packages/b0/2e/52bfeeaa4541889f23d8eadc6386b442ee2470bd3cff9baa67deb2dd5c57/contourpy-1.3.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb76c1a154b83991a3cbbf0dfeb26ec2833ad56f95540b442c73950af2013750", size = 315040 }, - { url = "https://files.pythonhosted.org/packages/52/94/86bfae441707205634d80392e873295652fc313dfd93c233c52c4dc07874/contourpy-1.3.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:44a29502ca9c7b5ba389e620d44f2fbe792b1fb5734e8b931ad307071ec58c53", size = 218221 }, + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/25/c2/fc7193cc5383637ff390a712e88e4ded0452c9fbcf84abe3de5ea3df1866/contourpy-1.3.1.tar.gz", hash = "sha256:dfd97abd83335045a913e3bcc4a09c0ceadbe66580cf573fe961f4a825efa699", size = 13465753, upload-time = "2024-11-12T11:00:59.118Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/a3/80937fe3efe0edacf67c9a20b955139a1a622730042c1ea991956f2704ad/contourpy-1.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a045f341a77b77e1c5de31e74e966537bba9f3c4099b35bf4c2e3939dd54cdab", size = 268466, upload-time = "2024-11-12T10:52:03.706Z" }, + { url = "https://files.pythonhosted.org/packages/82/1d/e3eaebb4aa2d7311528c048350ca8e99cdacfafd99da87bc0a5f8d81f2c2/contourpy-1.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:500360b77259914f7805af7462e41f9cb7ca92ad38e9f94d6c8641b089338124", size = 253314, upload-time = "2024-11-12T10:52:08.721Z" }, + { url = "https://files.pythonhosted.org/packages/de/f3/d796b22d1a2b587acc8100ba8c07fb7b5e17fde265a7bb05ab967f4c935a/contourpy-1.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2f926efda994cdf3c8d3fdb40b9962f86edbc4457e739277b961eced3d0b4c1", size = 312003, upload-time = "2024-11-12T10:52:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f5/0e67902bc4394daee8daa39c81d4f00b50e063ee1a46cb3938cc65585d36/contourpy-1.3.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:adce39d67c0edf383647a3a007de0a45fd1b08dedaa5318404f1a73059c2512b", size = 351896, upload-time = "2024-11-12T10:52:19.513Z" }, + { url = "https://files.pythonhosted.org/packages/1f/d6/e766395723f6256d45d6e67c13bb638dd1fa9dc10ef912dc7dd3dcfc19de/contourpy-1.3.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abbb49fb7dac584e5abc6636b7b2a7227111c4f771005853e7d25176daaf8453", size = 320814, upload-time = "2024-11-12T10:52:25.053Z" }, + { url = "https://files.pythonhosted.org/packages/a9/57/86c500d63b3e26e5b73a28b8291a67c5608d4aa87ebd17bd15bb33c178bc/contourpy-1.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0cffcbede75c059f535725c1680dfb17b6ba8753f0c74b14e6a9c68c29d7ea3", size = 324969, upload-time = "2024-11-12T10:52:30.731Z" }, + { url = "https://files.pythonhosted.org/packages/b8/62/bb146d1289d6b3450bccc4642e7f4413b92ebffd9bf2e91b0404323704a7/contourpy-1.3.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ab29962927945d89d9b293eabd0d59aea28d887d4f3be6c22deaefbb938a7277", size = 1265162, upload-time = "2024-11-12T10:52:46.26Z" }, + { url = "https://files.pythonhosted.org/packages/18/04/9f7d132ce49a212c8e767042cc80ae390f728060d2eea47058f55b9eff1c/contourpy-1.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:974d8145f8ca354498005b5b981165b74a195abfae9a8129df3e56771961d595", size = 1324328, upload-time = "2024-11-12T10:53:03.081Z" }, + { url = "https://files.pythonhosted.org/packages/46/23/196813901be3f97c83ababdab1382e13e0edc0bb4e7b49a7bff15fcf754e/contourpy-1.3.1-cp310-cp310-win32.whl", hash = "sha256:ac4578ac281983f63b400f7fe6c101bedc10651650eef012be1ccffcbacf3697", size = 173861, upload-time = "2024-11-12T10:53:06.283Z" }, + { url = "https://files.pythonhosted.org/packages/e0/82/c372be3fc000a3b2005061ca623a0d1ecd2eaafb10d9e883a2fc8566e951/contourpy-1.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:174e758c66bbc1c8576992cec9599ce8b6672b741b5d336b5c74e35ac382b18e", size = 218566, upload-time = "2024-11-12T10:53:09.798Z" }, + { url = "https://files.pythonhosted.org/packages/12/bb/11250d2906ee2e8b466b5f93e6b19d525f3e0254ac8b445b56e618527718/contourpy-1.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3e8b974d8db2c5610fb4e76307e265de0edb655ae8169e8b21f41807ccbeec4b", size = 269555, upload-time = "2024-11-12T10:53:14.707Z" }, + { url = "https://files.pythonhosted.org/packages/67/71/1e6e95aee21a500415f5d2dbf037bf4567529b6a4e986594d7026ec5ae90/contourpy-1.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:20914c8c973f41456337652a6eeca26d2148aa96dd7ac323b74516988bea89fc", size = 254549, upload-time = "2024-11-12T10:53:19.42Z" }, + { url = "https://files.pythonhosted.org/packages/31/2c/b88986e8d79ac45efe9d8801ae341525f38e087449b6c2f2e6050468a42c/contourpy-1.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19d40d37c1c3a4961b4619dd9d77b12124a453cc3d02bb31a07d58ef684d3d86", size = 313000, upload-time = "2024-11-12T10:53:23.944Z" }, + { url = "https://files.pythonhosted.org/packages/c4/18/65280989b151fcf33a8352f992eff71e61b968bef7432fbfde3a364f0730/contourpy-1.3.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:113231fe3825ebf6f15eaa8bc1f5b0ddc19d42b733345eae0934cb291beb88b6", size = 352925, upload-time = "2024-11-12T10:53:29.719Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c7/5fd0146c93220dbfe1a2e0f98969293b86ca9bc041d6c90c0e065f4619ad/contourpy-1.3.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4dbbc03a40f916a8420e420d63e96a1258d3d1b58cbdfd8d1f07b49fcbd38e85", size = 323693, upload-time = "2024-11-12T10:53:35.046Z" }, + { url = "https://files.pythonhosted.org/packages/85/fc/7fa5d17daf77306840a4e84668a48ddff09e6bc09ba4e37e85ffc8e4faa3/contourpy-1.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a04ecd68acbd77fa2d39723ceca4c3197cb2969633836ced1bea14e219d077c", size = 326184, upload-time = "2024-11-12T10:53:40.261Z" }, + { url = "https://files.pythonhosted.org/packages/ef/e7/104065c8270c7397c9571620d3ab880558957216f2b5ebb7e040f85eeb22/contourpy-1.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c414fc1ed8ee1dbd5da626cf3710c6013d3d27456651d156711fa24f24bd1291", size = 1268031, upload-time = "2024-11-12T10:53:55.876Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/c788d0bdbf32c8113c2354493ed291f924d4793c4a2e85b69e737a21a658/contourpy-1.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:31c1b55c1f34f80557d3830d3dd93ba722ce7e33a0b472cba0ec3b6535684d8f", size = 1325995, upload-time = "2024-11-12T10:54:11.572Z" }, + { url = "https://files.pythonhosted.org/packages/a6/e6/a2f351a90d955f8b0564caf1ebe4b1451a3f01f83e5e3a414055a5b8bccb/contourpy-1.3.1-cp311-cp311-win32.whl", hash = "sha256:f611e628ef06670df83fce17805c344710ca5cde01edfdc72751311da8585375", size = 174396, upload-time = "2024-11-12T10:54:15.358Z" }, + { url = "https://files.pythonhosted.org/packages/a8/7e/cd93cab453720a5d6cb75588cc17dcdc08fc3484b9de98b885924ff61900/contourpy-1.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:b2bdca22a27e35f16794cf585832e542123296b4687f9fd96822db6bae17bfc9", size = 219787, upload-time = "2024-11-12T10:54:18.836Z" }, + { url = "https://files.pythonhosted.org/packages/37/6b/175f60227d3e7f5f1549fcb374592be311293132207e451c3d7c654c25fb/contourpy-1.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0ffa84be8e0bd33410b17189f7164c3589c229ce5db85798076a3fa136d0e509", size = 271494, upload-time = "2024-11-12T10:54:23.6Z" }, + { url = "https://files.pythonhosted.org/packages/6b/6a/7833cfae2c1e63d1d8875a50fd23371394f540ce809d7383550681a1fa64/contourpy-1.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:805617228ba7e2cbbfb6c503858e626ab528ac2a32a04a2fe88ffaf6b02c32bc", size = 255444, upload-time = "2024-11-12T10:54:28.267Z" }, + { url = "https://files.pythonhosted.org/packages/7f/b3/7859efce66eaca5c14ba7619791b084ed02d868d76b928ff56890d2d059d/contourpy-1.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ade08d343436a94e633db932e7e8407fe7de8083967962b46bdfc1b0ced39454", size = 307628, upload-time = "2024-11-12T10:54:33.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/b2/011415f5e3f0a50b1e285a0bf78eb5d92a4df000553570f0851b6e309076/contourpy-1.3.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:47734d7073fb4590b4a40122b35917cd77be5722d80683b249dac1de266aac80", size = 347271, upload-time = "2024-11-12T10:54:38.816Z" }, + { url = "https://files.pythonhosted.org/packages/84/7d/ef19b1db0f45b151ac78c65127235239a8cf21a59d1ce8507ce03e89a30b/contourpy-1.3.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2ba94a401342fc0f8b948e57d977557fbf4d515f03c67682dd5c6191cb2d16ec", size = 318906, upload-time = "2024-11-12T10:54:44.132Z" }, + { url = "https://files.pythonhosted.org/packages/ba/99/6794142b90b853a9155316c8f470d2e4821fe6f086b03e372aca848227dd/contourpy-1.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efa874e87e4a647fd2e4f514d5e91c7d493697127beb95e77d2f7561f6905bd9", size = 323622, upload-time = "2024-11-12T10:54:48.788Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0f/37d2c84a900cd8eb54e105f4fa9aebd275e14e266736778bb5dccbf3bbbb/contourpy-1.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1bf98051f1045b15c87868dbaea84f92408337d4f81d0e449ee41920ea121d3b", size = 1266699, upload-time = "2024-11-12T10:55:04.016Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8a/deb5e11dc7d9cc8f0f9c8b29d4f062203f3af230ba83c30a6b161a6effc9/contourpy-1.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:61332c87493b00091423e747ea78200659dc09bdf7fd69edd5e98cef5d3e9a8d", size = 1326395, upload-time = "2024-11-12T10:55:20.547Z" }, + { url = "https://files.pythonhosted.org/packages/1a/35/7e267ae7c13aaf12322ccc493531f1e7f2eb8fba2927b9d7a05ff615df7a/contourpy-1.3.1-cp312-cp312-win32.whl", hash = "sha256:e914a8cb05ce5c809dd0fe350cfbb4e881bde5e2a38dc04e3afe1b3e58bd158e", size = 175354, upload-time = "2024-11-12T10:55:24.377Z" }, + { url = "https://files.pythonhosted.org/packages/a1/35/c2de8823211d07e8a79ab018ef03960716c5dff6f4d5bff5af87fd682992/contourpy-1.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:08d9d449a61cf53033612cb368f3a1b26cd7835d9b8cd326647efe43bca7568d", size = 220971, upload-time = "2024-11-12T10:55:27.971Z" }, + { url = "https://files.pythonhosted.org/packages/3e/4f/e56862e64b52b55b5ddcff4090085521fc228ceb09a88390a2b103dccd1b/contourpy-1.3.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b457d6430833cee8e4b8e9b6f07aa1c161e5e0d52e118dc102c8f9bd7dd060d6", size = 265605, upload-time = "2024-11-12T10:57:51.188Z" }, + { url = "https://files.pythonhosted.org/packages/b0/2e/52bfeeaa4541889f23d8eadc6386b442ee2470bd3cff9baa67deb2dd5c57/contourpy-1.3.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb76c1a154b83991a3cbbf0dfeb26ec2833ad56f95540b442c73950af2013750", size = 315040, upload-time = "2024-11-12T10:57:56.492Z" }, + { url = "https://files.pythonhosted.org/packages/52/94/86bfae441707205634d80392e873295652fc313dfd93c233c52c4dc07874/contourpy-1.3.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:44a29502ca9c7b5ba389e620d44f2fbe792b1fb5734e8b931ad307071ec58c53", size = 218221, upload-time = "2024-11-12T10:58:00.033Z" }, ] [[package]] name = "cycler" version = "0.12.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615 } +sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321 }, + { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, ] [[package]] name = "cython" version = "3.0.12" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5a/25/886e197c97a4b8e254173002cdc141441e878ff29aaa7d9ba560cd6e4866/cython-3.0.12.tar.gz", hash = "sha256:b988bb297ce76c671e28c97d017b95411010f7c77fa6623dd0bb47eed1aee1bc", size = 2757617 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/52/78/3bcb8ee7b6f5956dbd6bebf85818e075d863419db3661f25189c64cd6c70/Cython-3.0.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ba67eee9413b66dd9fbacd33f0bc2e028a2a120991d77b5fd4b19d0b1e4039b9", size = 3271523 }, - { url = "https://files.pythonhosted.org/packages/dc/21/5b700dac60cc7af4261c7fa2e91f55fe5f38f6c183e1201ced7cc932201b/Cython-3.0.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bee2717e5b5f7d966d0c6e27d2efe3698c357aa4d61bb3201997c7a4f9fe485a", size = 3630244 }, - { url = "https://files.pythonhosted.org/packages/a3/db/a42b8905bde467599927765ba12147f9d6ae3cd10fb33c4cda02011d7be0/Cython-3.0.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7cffc3464f641c8d0dda942c7c53015291beea11ec4d32421bed2f13b386b819", size = 3685089 }, - { url = "https://files.pythonhosted.org/packages/1a/0f/64be4bbdf26679f44fe96e19078c4382800eb8ba9b265373cae73ac94493/Cython-3.0.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d3a8f81980ffbd74e52f9186d8f1654e347d0c44bfea6b5997028977f481a179", size = 3501991 }, - { url = "https://files.pythonhosted.org/packages/d4/7d/e2a48882a4cbb16f0dfbc406879b2a1ea6b5d27c7cbef080e8eb8234a96c/Cython-3.0.12-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8d32856716c369d01f2385ad9177cdd1a11079ac89ea0932dc4882de1aa19174", size = 3713604 }, - { url = "https://files.pythonhosted.org/packages/3b/df/ca69aab33ffd2a184269335f6240b042c21d41dad335d4abb4ead9f22687/Cython-3.0.12-cp310-cp310-win32.whl", hash = "sha256:712c3f31adec140dc60d064a7f84741f50e2c25a8edd7ae746d5eb4d3ef7072a", size = 2578290 }, - { url = "https://files.pythonhosted.org/packages/1f/4c/4f79129407a1e0d540c961835960d811356aa3a666f621aa07cd7a979b0a/Cython-3.0.12-cp310-cp310-win_amd64.whl", hash = "sha256:d6945694c5b9170cfbd5f2c0d00ef7487a2de7aba83713a64ee4ebce7fad9e05", size = 2778456 }, - { url = "https://files.pythonhosted.org/packages/7e/60/3d27abd940f7b80a6aeb69dc093a892f04828e1dd0b243dd81ff87d7b0e9/Cython-3.0.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:feb86122a823937cc06e4c029d80ff69f082ebb0b959ab52a5af6cdd271c5dc3", size = 3277430 }, - { url = "https://files.pythonhosted.org/packages/c7/49/f17b0541b317d11f1d021a580643ee2481685157cded92efb32e2fb4daef/Cython-3.0.12-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfdbea486e702c328338314adb8e80f5f9741f06a0ae83aaec7463bc166d12e8", size = 3444055 }, - { url = "https://files.pythonhosted.org/packages/6b/7f/c57791ba6a1c934b6f1ab51371e894e3b4bfde0bc35e50046c8754a9d215/Cython-3.0.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:563de1728c8e48869d2380a1b76bbc1b1b1d01aba948480d68c1d05e52d20c92", size = 3597874 }, - { url = "https://files.pythonhosted.org/packages/23/24/803a0db3681b3a2ef65a4bebab201e5ae4aef5e6127ae03683476a573aa9/Cython-3.0.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:398d4576c1e1f6316282aa0b4a55139254fbed965cba7813e6d9900d3092b128", size = 3644129 }, - { url = "https://files.pythonhosted.org/packages/27/13/9b53ba8336e083ece441af8d6d182b8ca83ad523e87c07b3190af379ebc3/Cython-3.0.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1e5eadef80143026944ea8f9904715a008f5108d1d644a89f63094cc37351e73", size = 3504936 }, - { url = "https://files.pythonhosted.org/packages/a9/d2/d11104be6992a9fe256860cae6d1a79f7dcf3bdb12ae00116fac591f677d/Cython-3.0.12-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5a93cbda00a5451175b97dea5a9440a3fcee9e54b4cba7a7dbcba9a764b22aec", size = 3713066 }, - { url = "https://files.pythonhosted.org/packages/d9/8c/1fe49135296efa3f460c760a4297f6a5b387f3e69ac5c9dcdbd620295ab3/Cython-3.0.12-cp311-cp311-win32.whl", hash = "sha256:3109e1d44425a2639e9a677b66cd7711721a5b606b65867cb2d8ef7a97e2237b", size = 2579935 }, - { url = "https://files.pythonhosted.org/packages/02/4e/5ac0b5b9a239cd3fdae187dda8ff06b0b812f671e2501bf253712278f0ac/Cython-3.0.12-cp311-cp311-win_amd64.whl", hash = "sha256:d4b70fc339adba1e2111b074ee6119fe9fd6072c957d8597bce9a0dd1c3c6784", size = 2787337 }, - { url = "https://files.pythonhosted.org/packages/e6/6c/3be501a6520a93449b1e7e6f63e598ec56f3b5d1bc7ad14167c72a22ddf7/Cython-3.0.12-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fe030d4a00afb2844f5f70896b7f2a1a0d7da09bf3aa3d884cbe5f73fff5d310", size = 3311717 }, - { url = "https://files.pythonhosted.org/packages/ee/ab/adfeb22c85491de18ae10932165edd5b6f01e4c5e3e363638759d1235015/Cython-3.0.12-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a7fec4f052b8fe173fe70eae75091389955b9a23d5cec3d576d21c5913b49d47", size = 3344337 }, - { url = "https://files.pythonhosted.org/packages/0d/72/743730d7c46b4c85abefb93187cbbcb7aae8de288d7722b990db3d13499e/Cython-3.0.12-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0faa5e39e5c8cdf6f9c3b1c3f24972826e45911e7f5b99cf99453fca5432f45e", size = 3517692 }, - { url = "https://files.pythonhosted.org/packages/09/a1/29a4759a02661f8c8e6b703f62bfbc8285337e6918cc90f55dc0fadb5eb3/Cython-3.0.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2d53de996ed340e9ab0fc85a88aaa8932f2591a2746e1ab1c06e262bd4ec4be7", size = 3577057 }, - { url = "https://files.pythonhosted.org/packages/d6/f8/03d74e98901a7cc2f21f95231b07dd54ec2f69477319bac268b3816fc3a8/Cython-3.0.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ea3a0e19ab77266c738aa110684a753a04da4e709472cadeff487133354d6ab8", size = 3396493 }, - { url = "https://files.pythonhosted.org/packages/50/ea/ac33c5f54f980dbc23dd8f1d5c51afeef26e15ac1a66388e4b8195af83b7/Cython-3.0.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c151082884be468f2f405645858a857298ac7f7592729e5b54788b5c572717ba", size = 3603859 }, - { url = "https://files.pythonhosted.org/packages/a2/4e/91fc1d6b5e678dcf2d1ecd8dce45b014b4b60d2044d376355c605831c873/Cython-3.0.12-cp312-cp312-win32.whl", hash = "sha256:3083465749911ac3b2ce001b6bf17f404ac9dd35d8b08469d19dc7e717f5877a", size = 2610428 }, - { url = "https://files.pythonhosted.org/packages/ff/c3/a7fdec227b9f0bb07edbeb016c7b18ed6a8e6ce884d08b2e397cda2c0168/Cython-3.0.12-cp312-cp312-win_amd64.whl", hash = "sha256:c0b91c7ebace030dd558ea28730de8c580680b50768e5af66db2904a3716c3e3", size = 2794755 }, - { url = "https://files.pythonhosted.org/packages/ff/b8/f7ee49281830663fe5af13a171e05cdba9b11c17723e02c5214a036507f8/Cython-3.0.12-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54115fcc126840926ff3b53cfd2152eae17b3522ae7f74888f8a41413bd32f25", size = 3277061 }, - { url = "https://files.pythonhosted.org/packages/cb/2e/7e6a45bc7a1ff327fca37c3473eb36864ec78abf81ecfb7eed0dab4a9a90/Cython-3.0.12-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:629db614b9c364596d7c975fa3fb3978e8c5349524353dbe11429896a783fc1e", size = 3637036 }, - { url = "https://files.pythonhosted.org/packages/cf/ed/65c9b06e79f6565b0fce5422c8088c73612ff4f7e1d0d465e8ec7df18af5/Cython-3.0.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:af081838b0f9e12a83ec4c3809a00a64c817f489f7c512b0e3ecaf5f90a2a816", size = 3690936 }, - { url = "https://files.pythonhosted.org/packages/30/37/35d6cff743b8778f54e85fb38cde276a0c13732bb31d79a277a8406c4d22/Cython-3.0.12-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:34ce459808f7d8d5d4007bc5486fe50532529096b43957af6cbffcb4d9cc5c8d", size = 3505994 }, - { url = "https://files.pythonhosted.org/packages/30/bc/43c05a2693f96e003039d9bded526460e2663275fc9d6307700f0f755f9a/Cython-3.0.12-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:d6c6cd6a75c8393e6805d17f7126b96a894f310a1a9ea91c47d141fb9341bfa8", size = 3719053 }, - { url = "https://files.pythonhosted.org/packages/47/bd/eb4e6a4a6038c96aba8dd8ce7864644bd657346ad567dd2b384b073a838f/Cython-3.0.12-cp39-cp39-win32.whl", hash = "sha256:a4032e48d4734d2df68235d21920c715c451ac9de15fa14c71b378e8986b83be", size = 2583241 }, - { url = "https://files.pythonhosted.org/packages/6d/c8/66c63e895f6f2fea8e769c3a56ade2140403904fc63ab2e3e5b6443a5e8f/Cython-3.0.12-cp39-cp39-win_amd64.whl", hash = "sha256:dcdc3e5d4ce0e7a4af6903ed580833015641e968d18d528d8371e2435a34132c", size = 2783365 }, - { url = "https://files.pythonhosted.org/packages/27/6b/7c87867d255cbce8167ed99fc65635e9395d2af0f0c915428f5b17ec412d/Cython-3.0.12-py2.py3-none-any.whl", hash = "sha256:0038c9bae46c459669390e53a1ec115f8096b2e4647ae007ff1bf4e6dee92806", size = 1171640 }, +sdist = { url = "https://files.pythonhosted.org/packages/5a/25/886e197c97a4b8e254173002cdc141441e878ff29aaa7d9ba560cd6e4866/cython-3.0.12.tar.gz", hash = "sha256:b988bb297ce76c671e28c97d017b95411010f7c77fa6623dd0bb47eed1aee1bc", size = 2757617, upload-time = "2025-02-11T09:05:50.245Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/78/3bcb8ee7b6f5956dbd6bebf85818e075d863419db3661f25189c64cd6c70/Cython-3.0.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ba67eee9413b66dd9fbacd33f0bc2e028a2a120991d77b5fd4b19d0b1e4039b9", size = 3271523, upload-time = "2025-02-11T09:06:25.991Z" }, + { url = "https://files.pythonhosted.org/packages/dc/21/5b700dac60cc7af4261c7fa2e91f55fe5f38f6c183e1201ced7cc932201b/Cython-3.0.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bee2717e5b5f7d966d0c6e27d2efe3698c357aa4d61bb3201997c7a4f9fe485a", size = 3630244, upload-time = "2025-02-11T09:06:29.081Z" }, + { url = "https://files.pythonhosted.org/packages/a3/db/a42b8905bde467599927765ba12147f9d6ae3cd10fb33c4cda02011d7be0/Cython-3.0.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7cffc3464f641c8d0dda942c7c53015291beea11ec4d32421bed2f13b386b819", size = 3685089, upload-time = "2025-02-11T09:06:31.851Z" }, + { url = "https://files.pythonhosted.org/packages/1a/0f/64be4bbdf26679f44fe96e19078c4382800eb8ba9b265373cae73ac94493/Cython-3.0.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d3a8f81980ffbd74e52f9186d8f1654e347d0c44bfea6b5997028977f481a179", size = 3501991, upload-time = "2025-02-11T09:06:34.694Z" }, + { url = "https://files.pythonhosted.org/packages/d4/7d/e2a48882a4cbb16f0dfbc406879b2a1ea6b5d27c7cbef080e8eb8234a96c/Cython-3.0.12-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8d32856716c369d01f2385ad9177cdd1a11079ac89ea0932dc4882de1aa19174", size = 3713604, upload-time = "2025-02-11T09:06:38.242Z" }, + { url = "https://files.pythonhosted.org/packages/3b/df/ca69aab33ffd2a184269335f6240b042c21d41dad335d4abb4ead9f22687/Cython-3.0.12-cp310-cp310-win32.whl", hash = "sha256:712c3f31adec140dc60d064a7f84741f50e2c25a8edd7ae746d5eb4d3ef7072a", size = 2578290, upload-time = "2025-02-11T09:06:41.627Z" }, + { url = "https://files.pythonhosted.org/packages/1f/4c/4f79129407a1e0d540c961835960d811356aa3a666f621aa07cd7a979b0a/Cython-3.0.12-cp310-cp310-win_amd64.whl", hash = "sha256:d6945694c5b9170cfbd5f2c0d00ef7487a2de7aba83713a64ee4ebce7fad9e05", size = 2778456, upload-time = "2025-02-11T09:06:44.372Z" }, + { url = "https://files.pythonhosted.org/packages/7e/60/3d27abd940f7b80a6aeb69dc093a892f04828e1dd0b243dd81ff87d7b0e9/Cython-3.0.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:feb86122a823937cc06e4c029d80ff69f082ebb0b959ab52a5af6cdd271c5dc3", size = 3277430, upload-time = "2025-02-11T09:06:47.253Z" }, + { url = "https://files.pythonhosted.org/packages/c7/49/f17b0541b317d11f1d021a580643ee2481685157cded92efb32e2fb4daef/Cython-3.0.12-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfdbea486e702c328338314adb8e80f5f9741f06a0ae83aaec7463bc166d12e8", size = 3444055, upload-time = "2025-02-11T09:06:50.807Z" }, + { url = "https://files.pythonhosted.org/packages/6b/7f/c57791ba6a1c934b6f1ab51371e894e3b4bfde0bc35e50046c8754a9d215/Cython-3.0.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:563de1728c8e48869d2380a1b76bbc1b1b1d01aba948480d68c1d05e52d20c92", size = 3597874, upload-time = "2025-02-11T09:06:54.806Z" }, + { url = "https://files.pythonhosted.org/packages/23/24/803a0db3681b3a2ef65a4bebab201e5ae4aef5e6127ae03683476a573aa9/Cython-3.0.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:398d4576c1e1f6316282aa0b4a55139254fbed965cba7813e6d9900d3092b128", size = 3644129, upload-time = "2025-02-11T09:06:58.152Z" }, + { url = "https://files.pythonhosted.org/packages/27/13/9b53ba8336e083ece441af8d6d182b8ca83ad523e87c07b3190af379ebc3/Cython-3.0.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1e5eadef80143026944ea8f9904715a008f5108d1d644a89f63094cc37351e73", size = 3504936, upload-time = "2025-02-11T09:07:01.592Z" }, + { url = "https://files.pythonhosted.org/packages/a9/d2/d11104be6992a9fe256860cae6d1a79f7dcf3bdb12ae00116fac591f677d/Cython-3.0.12-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5a93cbda00a5451175b97dea5a9440a3fcee9e54b4cba7a7dbcba9a764b22aec", size = 3713066, upload-time = "2025-02-11T09:07:03.961Z" }, + { url = "https://files.pythonhosted.org/packages/d9/8c/1fe49135296efa3f460c760a4297f6a5b387f3e69ac5c9dcdbd620295ab3/Cython-3.0.12-cp311-cp311-win32.whl", hash = "sha256:3109e1d44425a2639e9a677b66cd7711721a5b606b65867cb2d8ef7a97e2237b", size = 2579935, upload-time = "2025-02-11T09:07:06.947Z" }, + { url = "https://files.pythonhosted.org/packages/02/4e/5ac0b5b9a239cd3fdae187dda8ff06b0b812f671e2501bf253712278f0ac/Cython-3.0.12-cp311-cp311-win_amd64.whl", hash = "sha256:d4b70fc339adba1e2111b074ee6119fe9fd6072c957d8597bce9a0dd1c3c6784", size = 2787337, upload-time = "2025-02-11T09:07:10.087Z" }, + { url = "https://files.pythonhosted.org/packages/e6/6c/3be501a6520a93449b1e7e6f63e598ec56f3b5d1bc7ad14167c72a22ddf7/Cython-3.0.12-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fe030d4a00afb2844f5f70896b7f2a1a0d7da09bf3aa3d884cbe5f73fff5d310", size = 3311717, upload-time = "2025-02-11T09:07:12.405Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ab/adfeb22c85491de18ae10932165edd5b6f01e4c5e3e363638759d1235015/Cython-3.0.12-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a7fec4f052b8fe173fe70eae75091389955b9a23d5cec3d576d21c5913b49d47", size = 3344337, upload-time = "2025-02-11T09:07:14.979Z" }, + { url = "https://files.pythonhosted.org/packages/0d/72/743730d7c46b4c85abefb93187cbbcb7aae8de288d7722b990db3d13499e/Cython-3.0.12-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0faa5e39e5c8cdf6f9c3b1c3f24972826e45911e7f5b99cf99453fca5432f45e", size = 3517692, upload-time = "2025-02-11T09:07:17.45Z" }, + { url = "https://files.pythonhosted.org/packages/09/a1/29a4759a02661f8c8e6b703f62bfbc8285337e6918cc90f55dc0fadb5eb3/Cython-3.0.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2d53de996ed340e9ab0fc85a88aaa8932f2591a2746e1ab1c06e262bd4ec4be7", size = 3577057, upload-time = "2025-02-11T09:07:22.106Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f8/03d74e98901a7cc2f21f95231b07dd54ec2f69477319bac268b3816fc3a8/Cython-3.0.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ea3a0e19ab77266c738aa110684a753a04da4e709472cadeff487133354d6ab8", size = 3396493, upload-time = "2025-02-11T09:07:25.183Z" }, + { url = "https://files.pythonhosted.org/packages/50/ea/ac33c5f54f980dbc23dd8f1d5c51afeef26e15ac1a66388e4b8195af83b7/Cython-3.0.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c151082884be468f2f405645858a857298ac7f7592729e5b54788b5c572717ba", size = 3603859, upload-time = "2025-02-11T09:07:27.634Z" }, + { url = "https://files.pythonhosted.org/packages/a2/4e/91fc1d6b5e678dcf2d1ecd8dce45b014b4b60d2044d376355c605831c873/Cython-3.0.12-cp312-cp312-win32.whl", hash = "sha256:3083465749911ac3b2ce001b6bf17f404ac9dd35d8b08469d19dc7e717f5877a", size = 2610428, upload-time = "2025-02-11T09:07:30.719Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c3/a7fdec227b9f0bb07edbeb016c7b18ed6a8e6ce884d08b2e397cda2c0168/Cython-3.0.12-cp312-cp312-win_amd64.whl", hash = "sha256:c0b91c7ebace030dd558ea28730de8c580680b50768e5af66db2904a3716c3e3", size = 2794755, upload-time = "2025-02-11T09:07:36.021Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b8/f7ee49281830663fe5af13a171e05cdba9b11c17723e02c5214a036507f8/Cython-3.0.12-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54115fcc126840926ff3b53cfd2152eae17b3522ae7f74888f8a41413bd32f25", size = 3277061, upload-time = "2025-02-11T09:09:09.146Z" }, + { url = "https://files.pythonhosted.org/packages/cb/2e/7e6a45bc7a1ff327fca37c3473eb36864ec78abf81ecfb7eed0dab4a9a90/Cython-3.0.12-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:629db614b9c364596d7c975fa3fb3978e8c5349524353dbe11429896a783fc1e", size = 3637036, upload-time = "2025-02-11T09:09:12.67Z" }, + { url = "https://files.pythonhosted.org/packages/cf/ed/65c9b06e79f6565b0fce5422c8088c73612ff4f7e1d0d465e8ec7df18af5/Cython-3.0.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:af081838b0f9e12a83ec4c3809a00a64c817f489f7c512b0e3ecaf5f90a2a816", size = 3690936, upload-time = "2025-02-11T09:09:15.397Z" }, + { url = "https://files.pythonhosted.org/packages/30/37/35d6cff743b8778f54e85fb38cde276a0c13732bb31d79a277a8406c4d22/Cython-3.0.12-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:34ce459808f7d8d5d4007bc5486fe50532529096b43957af6cbffcb4d9cc5c8d", size = 3505994, upload-time = "2025-02-11T09:09:18.178Z" }, + { url = "https://files.pythonhosted.org/packages/30/bc/43c05a2693f96e003039d9bded526460e2663275fc9d6307700f0f755f9a/Cython-3.0.12-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:d6c6cd6a75c8393e6805d17f7126b96a894f310a1a9ea91c47d141fb9341bfa8", size = 3719053, upload-time = "2025-02-11T09:09:21.697Z" }, + { url = "https://files.pythonhosted.org/packages/47/bd/eb4e6a4a6038c96aba8dd8ce7864644bd657346ad567dd2b384b073a838f/Cython-3.0.12-cp39-cp39-win32.whl", hash = "sha256:a4032e48d4734d2df68235d21920c715c451ac9de15fa14c71b378e8986b83be", size = 2583241, upload-time = "2025-02-11T09:09:24.027Z" }, + { url = "https://files.pythonhosted.org/packages/6d/c8/66c63e895f6f2fea8e769c3a56ade2140403904fc63ab2e3e5b6443a5e8f/Cython-3.0.12-cp39-cp39-win_amd64.whl", hash = "sha256:dcdc3e5d4ce0e7a4af6903ed580833015641e968d18d528d8371e2435a34132c", size = 2783365, upload-time = "2025-02-11T09:09:26.42Z" }, + { url = "https://files.pythonhosted.org/packages/27/6b/7c87867d255cbce8167ed99fc65635e9395d2af0f0c915428f5b17ec412d/Cython-3.0.12-py2.py3-none-any.whl", hash = "sha256:0038c9bae46c459669390e53a1ec115f8096b2e4647ae007ff1bf4e6dee92806", size = 1171640, upload-time = "2025-02-11T09:05:45.648Z" }, ] [[package]] name = "decorator" version = "5.2.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711 } +sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload-time = "2025-02-24T04:41:34.073Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190 }, + { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" }, ] [[package]] @@ -454,18 +485,18 @@ dependencies = [ { name = "requests" }, { name = "tqdm" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/af/38/6ed7bf78e4e48cff902299b806459ac2f0d650bd904522354bf5e8d989fe/deffcode-0.2.6.tar.gz", hash = "sha256:7a3670c82a554316e3aafdcfd5755064fc0331b054817f8e6b7d81c549609692", size = 61403 } +sdist = { url = "https://files.pythonhosted.org/packages/af/38/6ed7bf78e4e48cff902299b806459ac2f0d650bd904522354bf5e8d989fe/deffcode-0.2.6.tar.gz", hash = "sha256:7a3670c82a554316e3aafdcfd5755064fc0331b054817f8e6b7d81c549609692", size = 61403, upload-time = "2024-07-08T17:51:47.535Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/98/808205666fa14d897e853b3c0dfd6f2fd9aa8e5ec7006bbce32877e839b8/deffcode-0.2.6-py3-none-any.whl", hash = "sha256:b832712940b3956e2b9fcf65af32807f5d8e91dfc140a95166c48ea110de33db", size = 41887 }, + { url = "https://files.pythonhosted.org/packages/76/98/808205666fa14d897e853b3c0dfd6f2fd9aa8e5ec7006bbce32877e839b8/deffcode-0.2.6-py3-none-any.whl", hash = "sha256:b832712940b3956e2b9fcf65af32807f5d8e91dfc140a95166c48ea110de33db", size = 41887, upload-time = "2024-07-08T17:51:45.536Z" }, ] [[package]] name = "exceptiongroup" version = "1.2.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 } +sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883, upload-time = "2024-07-12T22:26:00.161Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 }, + { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453, upload-time = "2024-07-12T22:25:58.476Z" }, ] [[package]] @@ -477,9 +508,9 @@ dependencies = [ { name = "pycodestyle" }, { name = "pyflakes" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e7/c4/5842fc9fc94584c455543540af62fd9900faade32511fab650e9891ec225/flake8-7.2.0.tar.gz", hash = "sha256:fa558ae3f6f7dbf2b4f22663e5343b6b6023620461f8d4ff2019ef4b5ee70426", size = 48177 } +sdist = { url = "https://files.pythonhosted.org/packages/e7/c4/5842fc9fc94584c455543540af62fd9900faade32511fab650e9891ec225/flake8-7.2.0.tar.gz", hash = "sha256:fa558ae3f6f7dbf2b4f22663e5343b6b6023620461f8d4ff2019ef4b5ee70426", size = 48177, upload-time = "2025-03-29T20:08:39.329Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/83/5c/0627be4c9976d56b1217cb5187b7504e7fd7d3503f8bfd312a04077bd4f7/flake8-7.2.0-py2.py3-none-any.whl", hash = "sha256:93b92ba5bdb60754a6da14fa3b93a9361fd00a59632ada61fd7b130436c40343", size = 57786 }, + { url = "https://files.pythonhosted.org/packages/83/5c/0627be4c9976d56b1217cb5187b7504e7fd7d3503f8bfd312a04077bd4f7/flake8-7.2.0-py2.py3-none-any.whl", hash = "sha256:93b92ba5bdb60754a6da14fa3b93a9361fd00a59632ada61fd7b130436c40343", size = 57786, upload-time = "2025-03-29T20:08:37.902Z" }, ] [[package]] @@ -490,9 +521,9 @@ dependencies = [ { name = "bandit" }, { name = "flake8" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/77/1c/4f66a7a52a246d6c64312b5c40da3af3630cd60b27af81b137796af3c0bc/flake8_bandit-4.1.1.tar.gz", hash = "sha256:068e09287189cbfd7f986e92605adea2067630b75380c6b5733dab7d87f9a84e", size = 5403 } +sdist = { url = "https://files.pythonhosted.org/packages/77/1c/4f66a7a52a246d6c64312b5c40da3af3630cd60b27af81b137796af3c0bc/flake8_bandit-4.1.1.tar.gz", hash = "sha256:068e09287189cbfd7f986e92605adea2067630b75380c6b5733dab7d87f9a84e", size = 5403, upload-time = "2022-08-29T13:48:41.225Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/5f/55bab0ac89f9ad9f4c6e38087faa80c252daec4ccb7776b4dac216ca9e3f/flake8_bandit-4.1.1-py3-none-any.whl", hash = "sha256:4c8a53eb48f23d4ef1e59293657181a3c989d0077c9952717e98a0eace43e06d", size = 4828 }, + { url = "https://files.pythonhosted.org/packages/e7/5f/55bab0ac89f9ad9f4c6e38087faa80c252daec4ccb7776b4dac216ca9e3f/flake8_bandit-4.1.1-py3-none-any.whl", hash = "sha256:4c8a53eb48f23d4ef1e59293657181a3c989d0077c9952717e98a0eace43e06d", size = 4828, upload-time = "2022-08-29T13:48:39.737Z" }, ] [[package]] @@ -503,59 +534,59 @@ dependencies = [ { name = "attrs" }, { name = "flake8" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c7/25/48ba712ff589b0149f21135234f9bb45c14d6689acc6151b5e2ff8ac2ae9/flake8_bugbear-24.12.12.tar.gz", hash = "sha256:46273cef0a6b6ff48ca2d69e472f41420a42a46e24b2a8972e4f0d6733d12a64", size = 82907 } +sdist = { url = "https://files.pythonhosted.org/packages/c7/25/48ba712ff589b0149f21135234f9bb45c14d6689acc6151b5e2ff8ac2ae9/flake8_bugbear-24.12.12.tar.gz", hash = "sha256:46273cef0a6b6ff48ca2d69e472f41420a42a46e24b2a8972e4f0d6733d12a64", size = 82907, upload-time = "2024-12-12T16:49:26.307Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b9/21/0a875f75fbe4008bd171e2fefa413536258fe6b4cfaaa087986de74588f4/flake8_bugbear-24.12.12-py3-none-any.whl", hash = "sha256:1b6967436f65ca22a42e5373aaa6f2d87966ade9aa38d4baf2a1be550767545e", size = 36664 }, + { url = "https://files.pythonhosted.org/packages/b9/21/0a875f75fbe4008bd171e2fefa413536258fe6b4cfaaa087986de74588f4/flake8_bugbear-24.12.12-py3-none-any.whl", hash = "sha256:1b6967436f65ca22a42e5373aaa6f2d87966ade9aa38d4baf2a1be550767545e", size = 36664, upload-time = "2024-12-12T16:49:23.584Z" }, ] [[package]] name = "fonttools" version = "4.57.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/03/2d/a9a0b6e3a0cf6bd502e64fc16d894269011930cabfc89aee20d1635b1441/fonttools-4.57.0.tar.gz", hash = "sha256:727ece10e065be2f9dd239d15dd5d60a66e17eac11aea47d447f9f03fdbc42de", size = 3492448 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/db/17/3ddfd1881878b3f856065130bb603f5922e81ae8a4eb53bce0ea78f765a8/fonttools-4.57.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:babe8d1eb059a53e560e7bf29f8e8f4accc8b6cfb9b5fd10e485bde77e71ef41", size = 2756260 }, - { url = "https://files.pythonhosted.org/packages/26/2b/6957890c52c030b0bf9e0add53e5badab4682c6ff024fac9a332bb2ae063/fonttools-4.57.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:81aa97669cd726349eb7bd43ca540cf418b279ee3caba5e2e295fb4e8f841c02", size = 2284691 }, - { url = "https://files.pythonhosted.org/packages/cc/8e/c043b4081774e5eb06a834cedfdb7d432b4935bc8c4acf27207bdc34dfc4/fonttools-4.57.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0e9618630edd1910ad4f07f60d77c184b2f572c8ee43305ea3265675cbbfe7e", size = 4566077 }, - { url = "https://files.pythonhosted.org/packages/59/bc/e16ae5d9eee6c70830ce11d1e0b23d6018ddfeb28025fda092cae7889c8b/fonttools-4.57.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:34687a5d21f1d688d7d8d416cb4c5b9c87fca8a1797ec0d74b9fdebfa55c09ab", size = 4608729 }, - { url = "https://files.pythonhosted.org/packages/25/13/e557bf10bb38e4e4c436d3a9627aadf691bc7392ae460910447fda5fad2b/fonttools-4.57.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:69ab81b66ebaa8d430ba56c7a5f9abe0183afefd3a2d6e483060343398b13fb1", size = 4759646 }, - { url = "https://files.pythonhosted.org/packages/bc/c9/5e2952214d4a8e31026bf80beb18187199b7001e60e99a6ce19773249124/fonttools-4.57.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d639397de852f2ccfb3134b152c741406752640a266d9c1365b0f23d7b88077f", size = 4941652 }, - { url = "https://files.pythonhosted.org/packages/df/04/e80242b3d9ec91a1f785d949edc277a13ecfdcfae744de4b170df9ed77d8/fonttools-4.57.0-cp310-cp310-win32.whl", hash = "sha256:cc066cb98b912f525ae901a24cd381a656f024f76203bc85f78fcc9e66ae5aec", size = 2159432 }, - { url = "https://files.pythonhosted.org/packages/33/ba/e858cdca275daf16e03c0362aa43734ea71104c3b356b2100b98543dba1b/fonttools-4.57.0-cp310-cp310-win_amd64.whl", hash = "sha256:7a64edd3ff6a7f711a15bd70b4458611fb240176ec11ad8845ccbab4fe6745db", size = 2203869 }, - { url = "https://files.pythonhosted.org/packages/81/1f/e67c99aa3c6d3d2f93d956627e62a57ae0d35dc42f26611ea2a91053f6d6/fonttools-4.57.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3871349303bdec958360eedb619169a779956503ffb4543bb3e6211e09b647c4", size = 2757392 }, - { url = "https://files.pythonhosted.org/packages/aa/f1/f75770d0ddc67db504850898d96d75adde238c35313409bfcd8db4e4a5fe/fonttools-4.57.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c59375e85126b15a90fcba3443eaac58f3073ba091f02410eaa286da9ad80ed8", size = 2285609 }, - { url = "https://files.pythonhosted.org/packages/f5/d3/bc34e4953cb204bae0c50b527307dce559b810e624a733351a654cfc318e/fonttools-4.57.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:967b65232e104f4b0f6370a62eb33089e00024f2ce143aecbf9755649421c683", size = 4873292 }, - { url = "https://files.pythonhosted.org/packages/41/b8/d5933559303a4ab18c799105f4c91ee0318cc95db4a2a09e300116625e7a/fonttools-4.57.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39acf68abdfc74e19de7485f8f7396fa4d2418efea239b7061d6ed6a2510c746", size = 4902503 }, - { url = "https://files.pythonhosted.org/packages/32/13/acb36bfaa316f481153ce78de1fa3926a8bad42162caa3b049e1afe2408b/fonttools-4.57.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9d077f909f2343daf4495ba22bb0e23b62886e8ec7c109ee8234bdbd678cf344", size = 5077351 }, - { url = "https://files.pythonhosted.org/packages/b5/23/6d383a2ca83b7516d73975d8cca9d81a01acdcaa5e4db8579e4f3de78518/fonttools-4.57.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:46370ac47a1e91895d40e9ad48effbe8e9d9db1a4b80888095bc00e7beaa042f", size = 5275067 }, - { url = "https://files.pythonhosted.org/packages/bc/ca/31b8919c6da0198d5d522f1d26c980201378c087bdd733a359a1e7485769/fonttools-4.57.0-cp311-cp311-win32.whl", hash = "sha256:ca2aed95855506b7ae94e8f1f6217b7673c929e4f4f1217bcaa236253055cb36", size = 2158263 }, - { url = "https://files.pythonhosted.org/packages/13/4c/de2612ea2216eb45cfc8eb91a8501615dd87716feaf5f8fb65cbca576289/fonttools-4.57.0-cp311-cp311-win_amd64.whl", hash = "sha256:17168a4670bbe3775f3f3f72d23ee786bd965395381dfbb70111e25e81505b9d", size = 2204968 }, - { url = "https://files.pythonhosted.org/packages/cb/98/d4bc42d43392982eecaaca117d79845734d675219680cd43070bb001bc1f/fonttools-4.57.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:889e45e976c74abc7256d3064aa7c1295aa283c6bb19810b9f8b604dfe5c7f31", size = 2751824 }, - { url = "https://files.pythonhosted.org/packages/1a/62/7168030eeca3742fecf45f31e63b5ef48969fa230a672216b805f1d61548/fonttools-4.57.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0425c2e052a5f1516c94e5855dbda706ae5a768631e9fcc34e57d074d1b65b92", size = 2283072 }, - { url = "https://files.pythonhosted.org/packages/5d/82/121a26d9646f0986ddb35fbbaf58ef791c25b59ecb63ffea2aab0099044f/fonttools-4.57.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44c26a311be2ac130f40a96769264809d3b0cb297518669db437d1cc82974888", size = 4788020 }, - { url = "https://files.pythonhosted.org/packages/5b/26/e0f2fb662e022d565bbe280a3cfe6dafdaabf58889ff86fdef2d31ff1dde/fonttools-4.57.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84c41ba992df5b8d680b89fd84c6a1f2aca2b9f1ae8a67400c8930cd4ea115f6", size = 4859096 }, - { url = "https://files.pythonhosted.org/packages/9e/44/9075e323347b1891cdece4b3f10a3b84a8f4c42a7684077429d9ce842056/fonttools-4.57.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ea1e9e43ca56b0c12440a7c689b1350066595bebcaa83baad05b8b2675129d98", size = 4964356 }, - { url = "https://files.pythonhosted.org/packages/48/28/caa8df32743462fb966be6de6a79d7f30393859636d7732e82efa09fbbb4/fonttools-4.57.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:84fd56c78d431606332a0627c16e2a63d243d0d8b05521257d77c6529abe14d8", size = 5226546 }, - { url = "https://files.pythonhosted.org/packages/f6/46/95ab0f0d2e33c5b1a4fc1c0efe5e286ba9359602c0a9907adb1faca44175/fonttools-4.57.0-cp312-cp312-win32.whl", hash = "sha256:f4376819c1c778d59e0a31db5dc6ede854e9edf28bbfa5b756604727f7f800ac", size = 2146776 }, - { url = "https://files.pythonhosted.org/packages/06/5d/1be5424bb305880e1113631f49a55ea7c7da3a5fe02608ca7c16a03a21da/fonttools-4.57.0-cp312-cp312-win_amd64.whl", hash = "sha256:57e30241524879ea10cdf79c737037221f77cc126a8cdc8ff2c94d4a522504b9", size = 2193956 }, - { url = "https://files.pythonhosted.org/packages/d2/c7/3bddafbb95447f6fbabdd0b399bf468649321fd4029e356b4f6bd70fbc1b/fonttools-4.57.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7339e6a3283e4b0ade99cade51e97cde3d54cd6d1c3744459e886b66d630c8b3", size = 2758942 }, - { url = "https://files.pythonhosted.org/packages/d4/a2/8dd7771022e365c90e428b1607174c3297d5c0a2cc2cf4cdccb2221945b7/fonttools-4.57.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:05efceb2cb5f6ec92a4180fcb7a64aa8d3385fd49cfbbe459350229d1974f0b1", size = 2285959 }, - { url = "https://files.pythonhosted.org/packages/58/5a/2fd29c5e38b14afe1fae7d472373e66688e7c7a98554252f3cf44371e033/fonttools-4.57.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a97bb05eb24637714a04dee85bdf0ad1941df64fe3b802ee4ac1c284a5f97b7c", size = 4571677 }, - { url = "https://files.pythonhosted.org/packages/bf/30/b77cf81923f1a67ff35d6765a9db4718c0688eb8466c464c96a23a2e28d4/fonttools-4.57.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:541cb48191a19ceb1a2a4b90c1fcebd22a1ff7491010d3cf840dd3a68aebd654", size = 4616644 }, - { url = "https://files.pythonhosted.org/packages/06/33/376605898d8d553134144dff167506a49694cb0e0cf684c14920fbc1e99f/fonttools-4.57.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:cdef9a056c222d0479a1fdb721430f9efd68268014c54e8166133d2643cb05d9", size = 4761314 }, - { url = "https://files.pythonhosted.org/packages/48/e4/e0e48f5bae04bc1a1c6b4fcd7d1ca12b29f1fe74221534b7ff83ed0db8fe/fonttools-4.57.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:3cf97236b192a50a4bf200dc5ba405aa78d4f537a2c6e4c624bb60466d5b03bd", size = 4945563 }, - { url = "https://files.pythonhosted.org/packages/61/98/2dacfc6d70f2d93bde1bbf814286be343cb17f53057130ad3b843144dd00/fonttools-4.57.0-cp39-cp39-win32.whl", hash = "sha256:e952c684274a7714b3160f57ec1d78309f955c6335c04433f07d36c5eb27b1f9", size = 2159997 }, - { url = "https://files.pythonhosted.org/packages/93/fa/e61cc236f40d504532d2becf90c297bfed8e40abc0c8b08375fbb83eff29/fonttools-4.57.0-cp39-cp39-win_amd64.whl", hash = "sha256:a2a722c0e4bfd9966a11ff55c895c817158fcce1b2b6700205a376403b546ad9", size = 2204508 }, - { url = "https://files.pythonhosted.org/packages/90/27/45f8957c3132917f91aaa56b700bcfc2396be1253f685bd5c68529b6f610/fonttools-4.57.0-py3-none-any.whl", hash = "sha256:3122c604a675513c68bd24c6a8f9091f1c2376d18e8f5fe5a101746c81b3e98f", size = 1093605 }, +sdist = { url = "https://files.pythonhosted.org/packages/03/2d/a9a0b6e3a0cf6bd502e64fc16d894269011930cabfc89aee20d1635b1441/fonttools-4.57.0.tar.gz", hash = "sha256:727ece10e065be2f9dd239d15dd5d60a66e17eac11aea47d447f9f03fdbc42de", size = 3492448, upload-time = "2025-04-03T11:07:13.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/17/3ddfd1881878b3f856065130bb603f5922e81ae8a4eb53bce0ea78f765a8/fonttools-4.57.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:babe8d1eb059a53e560e7bf29f8e8f4accc8b6cfb9b5fd10e485bde77e71ef41", size = 2756260, upload-time = "2025-04-03T11:05:28.582Z" }, + { url = "https://files.pythonhosted.org/packages/26/2b/6957890c52c030b0bf9e0add53e5badab4682c6ff024fac9a332bb2ae063/fonttools-4.57.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:81aa97669cd726349eb7bd43ca540cf418b279ee3caba5e2e295fb4e8f841c02", size = 2284691, upload-time = "2025-04-03T11:05:31.526Z" }, + { url = "https://files.pythonhosted.org/packages/cc/8e/c043b4081774e5eb06a834cedfdb7d432b4935bc8c4acf27207bdc34dfc4/fonttools-4.57.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0e9618630edd1910ad4f07f60d77c184b2f572c8ee43305ea3265675cbbfe7e", size = 4566077, upload-time = "2025-04-03T11:05:33.559Z" }, + { url = "https://files.pythonhosted.org/packages/59/bc/e16ae5d9eee6c70830ce11d1e0b23d6018ddfeb28025fda092cae7889c8b/fonttools-4.57.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:34687a5d21f1d688d7d8d416cb4c5b9c87fca8a1797ec0d74b9fdebfa55c09ab", size = 4608729, upload-time = "2025-04-03T11:05:35.49Z" }, + { url = "https://files.pythonhosted.org/packages/25/13/e557bf10bb38e4e4c436d3a9627aadf691bc7392ae460910447fda5fad2b/fonttools-4.57.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:69ab81b66ebaa8d430ba56c7a5f9abe0183afefd3a2d6e483060343398b13fb1", size = 4759646, upload-time = "2025-04-03T11:05:37.963Z" }, + { url = "https://files.pythonhosted.org/packages/bc/c9/5e2952214d4a8e31026bf80beb18187199b7001e60e99a6ce19773249124/fonttools-4.57.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d639397de852f2ccfb3134b152c741406752640a266d9c1365b0f23d7b88077f", size = 4941652, upload-time = "2025-04-03T11:05:40.089Z" }, + { url = "https://files.pythonhosted.org/packages/df/04/e80242b3d9ec91a1f785d949edc277a13ecfdcfae744de4b170df9ed77d8/fonttools-4.57.0-cp310-cp310-win32.whl", hash = "sha256:cc066cb98b912f525ae901a24cd381a656f024f76203bc85f78fcc9e66ae5aec", size = 2159432, upload-time = "2025-04-03T11:05:41.754Z" }, + { url = "https://files.pythonhosted.org/packages/33/ba/e858cdca275daf16e03c0362aa43734ea71104c3b356b2100b98543dba1b/fonttools-4.57.0-cp310-cp310-win_amd64.whl", hash = "sha256:7a64edd3ff6a7f711a15bd70b4458611fb240176ec11ad8845ccbab4fe6745db", size = 2203869, upload-time = "2025-04-03T11:05:43.712Z" }, + { url = "https://files.pythonhosted.org/packages/81/1f/e67c99aa3c6d3d2f93d956627e62a57ae0d35dc42f26611ea2a91053f6d6/fonttools-4.57.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3871349303bdec958360eedb619169a779956503ffb4543bb3e6211e09b647c4", size = 2757392, upload-time = "2025-04-03T11:05:45.715Z" }, + { url = "https://files.pythonhosted.org/packages/aa/f1/f75770d0ddc67db504850898d96d75adde238c35313409bfcd8db4e4a5fe/fonttools-4.57.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c59375e85126b15a90fcba3443eaac58f3073ba091f02410eaa286da9ad80ed8", size = 2285609, upload-time = "2025-04-03T11:05:47.977Z" }, + { url = "https://files.pythonhosted.org/packages/f5/d3/bc34e4953cb204bae0c50b527307dce559b810e624a733351a654cfc318e/fonttools-4.57.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:967b65232e104f4b0f6370a62eb33089e00024f2ce143aecbf9755649421c683", size = 4873292, upload-time = "2025-04-03T11:05:49.921Z" }, + { url = "https://files.pythonhosted.org/packages/41/b8/d5933559303a4ab18c799105f4c91ee0318cc95db4a2a09e300116625e7a/fonttools-4.57.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39acf68abdfc74e19de7485f8f7396fa4d2418efea239b7061d6ed6a2510c746", size = 4902503, upload-time = "2025-04-03T11:05:52.17Z" }, + { url = "https://files.pythonhosted.org/packages/32/13/acb36bfaa316f481153ce78de1fa3926a8bad42162caa3b049e1afe2408b/fonttools-4.57.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9d077f909f2343daf4495ba22bb0e23b62886e8ec7c109ee8234bdbd678cf344", size = 5077351, upload-time = "2025-04-03T11:05:54.162Z" }, + { url = "https://files.pythonhosted.org/packages/b5/23/6d383a2ca83b7516d73975d8cca9d81a01acdcaa5e4db8579e4f3de78518/fonttools-4.57.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:46370ac47a1e91895d40e9ad48effbe8e9d9db1a4b80888095bc00e7beaa042f", size = 5275067, upload-time = "2025-04-03T11:05:57.375Z" }, + { url = "https://files.pythonhosted.org/packages/bc/ca/31b8919c6da0198d5d522f1d26c980201378c087bdd733a359a1e7485769/fonttools-4.57.0-cp311-cp311-win32.whl", hash = "sha256:ca2aed95855506b7ae94e8f1f6217b7673c929e4f4f1217bcaa236253055cb36", size = 2158263, upload-time = "2025-04-03T11:05:59.567Z" }, + { url = "https://files.pythonhosted.org/packages/13/4c/de2612ea2216eb45cfc8eb91a8501615dd87716feaf5f8fb65cbca576289/fonttools-4.57.0-cp311-cp311-win_amd64.whl", hash = "sha256:17168a4670bbe3775f3f3f72d23ee786bd965395381dfbb70111e25e81505b9d", size = 2204968, upload-time = "2025-04-03T11:06:02.16Z" }, + { url = "https://files.pythonhosted.org/packages/cb/98/d4bc42d43392982eecaaca117d79845734d675219680cd43070bb001bc1f/fonttools-4.57.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:889e45e976c74abc7256d3064aa7c1295aa283c6bb19810b9f8b604dfe5c7f31", size = 2751824, upload-time = "2025-04-03T11:06:03.782Z" }, + { url = "https://files.pythonhosted.org/packages/1a/62/7168030eeca3742fecf45f31e63b5ef48969fa230a672216b805f1d61548/fonttools-4.57.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0425c2e052a5f1516c94e5855dbda706ae5a768631e9fcc34e57d074d1b65b92", size = 2283072, upload-time = "2025-04-03T11:06:05.533Z" }, + { url = "https://files.pythonhosted.org/packages/5d/82/121a26d9646f0986ddb35fbbaf58ef791c25b59ecb63ffea2aab0099044f/fonttools-4.57.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44c26a311be2ac130f40a96769264809d3b0cb297518669db437d1cc82974888", size = 4788020, upload-time = "2025-04-03T11:06:07.249Z" }, + { url = "https://files.pythonhosted.org/packages/5b/26/e0f2fb662e022d565bbe280a3cfe6dafdaabf58889ff86fdef2d31ff1dde/fonttools-4.57.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84c41ba992df5b8d680b89fd84c6a1f2aca2b9f1ae8a67400c8930cd4ea115f6", size = 4859096, upload-time = "2025-04-03T11:06:09.469Z" }, + { url = "https://files.pythonhosted.org/packages/9e/44/9075e323347b1891cdece4b3f10a3b84a8f4c42a7684077429d9ce842056/fonttools-4.57.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ea1e9e43ca56b0c12440a7c689b1350066595bebcaa83baad05b8b2675129d98", size = 4964356, upload-time = "2025-04-03T11:06:11.294Z" }, + { url = "https://files.pythonhosted.org/packages/48/28/caa8df32743462fb966be6de6a79d7f30393859636d7732e82efa09fbbb4/fonttools-4.57.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:84fd56c78d431606332a0627c16e2a63d243d0d8b05521257d77c6529abe14d8", size = 5226546, upload-time = "2025-04-03T11:06:13.6Z" }, + { url = "https://files.pythonhosted.org/packages/f6/46/95ab0f0d2e33c5b1a4fc1c0efe5e286ba9359602c0a9907adb1faca44175/fonttools-4.57.0-cp312-cp312-win32.whl", hash = "sha256:f4376819c1c778d59e0a31db5dc6ede854e9edf28bbfa5b756604727f7f800ac", size = 2146776, upload-time = "2025-04-03T11:06:15.643Z" }, + { url = "https://files.pythonhosted.org/packages/06/5d/1be5424bb305880e1113631f49a55ea7c7da3a5fe02608ca7c16a03a21da/fonttools-4.57.0-cp312-cp312-win_amd64.whl", hash = "sha256:57e30241524879ea10cdf79c737037221f77cc126a8cdc8ff2c94d4a522504b9", size = 2193956, upload-time = "2025-04-03T11:06:17.534Z" }, + { url = "https://files.pythonhosted.org/packages/d2/c7/3bddafbb95447f6fbabdd0b399bf468649321fd4029e356b4f6bd70fbc1b/fonttools-4.57.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7339e6a3283e4b0ade99cade51e97cde3d54cd6d1c3744459e886b66d630c8b3", size = 2758942, upload-time = "2025-04-03T11:06:54.679Z" }, + { url = "https://files.pythonhosted.org/packages/d4/a2/8dd7771022e365c90e428b1607174c3297d5c0a2cc2cf4cdccb2221945b7/fonttools-4.57.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:05efceb2cb5f6ec92a4180fcb7a64aa8d3385fd49cfbbe459350229d1974f0b1", size = 2285959, upload-time = "2025-04-03T11:06:56.792Z" }, + { url = "https://files.pythonhosted.org/packages/58/5a/2fd29c5e38b14afe1fae7d472373e66688e7c7a98554252f3cf44371e033/fonttools-4.57.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a97bb05eb24637714a04dee85bdf0ad1941df64fe3b802ee4ac1c284a5f97b7c", size = 4571677, upload-time = "2025-04-03T11:06:59.002Z" }, + { url = "https://files.pythonhosted.org/packages/bf/30/b77cf81923f1a67ff35d6765a9db4718c0688eb8466c464c96a23a2e28d4/fonttools-4.57.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:541cb48191a19ceb1a2a4b90c1fcebd22a1ff7491010d3cf840dd3a68aebd654", size = 4616644, upload-time = "2025-04-03T11:07:01.238Z" }, + { url = "https://files.pythonhosted.org/packages/06/33/376605898d8d553134144dff167506a49694cb0e0cf684c14920fbc1e99f/fonttools-4.57.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:cdef9a056c222d0479a1fdb721430f9efd68268014c54e8166133d2643cb05d9", size = 4761314, upload-time = "2025-04-03T11:07:03.162Z" }, + { url = "https://files.pythonhosted.org/packages/48/e4/e0e48f5bae04bc1a1c6b4fcd7d1ca12b29f1fe74221534b7ff83ed0db8fe/fonttools-4.57.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:3cf97236b192a50a4bf200dc5ba405aa78d4f537a2c6e4c624bb60466d5b03bd", size = 4945563, upload-time = "2025-04-03T11:07:05.313Z" }, + { url = "https://files.pythonhosted.org/packages/61/98/2dacfc6d70f2d93bde1bbf814286be343cb17f53057130ad3b843144dd00/fonttools-4.57.0-cp39-cp39-win32.whl", hash = "sha256:e952c684274a7714b3160f57ec1d78309f955c6335c04433f07d36c5eb27b1f9", size = 2159997, upload-time = "2025-04-03T11:07:07.467Z" }, + { url = "https://files.pythonhosted.org/packages/93/fa/e61cc236f40d504532d2becf90c297bfed8e40abc0c8b08375fbb83eff29/fonttools-4.57.0-cp39-cp39-win_amd64.whl", hash = "sha256:a2a722c0e4bfd9966a11ff55c895c817158fcce1b2b6700205a376403b546ad9", size = 2204508, upload-time = "2025-04-03T11:07:09.632Z" }, + { url = "https://files.pythonhosted.org/packages/90/27/45f8957c3132917f91aaa56b700bcfc2396be1253f685bd5c68529b6f610/fonttools-4.57.0-py3-none-any.whl", hash = "sha256:3122c604a675513c68bd24c6a8f9091f1c2376d18e8f5fe5a101746c81b3e98f", size = 1093605, upload-time = "2025-04-03T11:07:11.341Z" }, ] [[package]] name = "idna" version = "3.10" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, ] [[package]] @@ -563,11 +594,11 @@ name = "importlib-metadata" version = "8.6.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "zipp", marker = "python_full_version < '3.11'" }, + { name = "zipp" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/33/08/c1395a292bb23fd03bdf572a1357c5a733d3eecbab877641ceacab23db6e/importlib_metadata-8.6.1.tar.gz", hash = "sha256:310b41d755445d74569f993ccfc22838295d9fe005425094fad953d7f15c8580", size = 55767 } +sdist = { url = "https://files.pythonhosted.org/packages/33/08/c1395a292bb23fd03bdf572a1357c5a733d3eecbab877641ceacab23db6e/importlib_metadata-8.6.1.tar.gz", hash = "sha256:310b41d755445d74569f993ccfc22838295d9fe005425094fad953d7f15c8580", size = 55767, upload-time = "2025-01-20T22:21:30.429Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/79/9d/0fb148dc4d6fa4a7dd1d8378168d9b4cd8d4560a6fbf6f0121c5fc34eb68/importlib_metadata-8.6.1-py3-none-any.whl", hash = "sha256:02a89390c1e15fdfdc0d7c6b25cb3e62650d0494005c97d6f148bf5b9787525e", size = 26971 }, + { url = "https://files.pythonhosted.org/packages/79/9d/0fb148dc4d6fa4a7dd1d8378168d9b4cd8d4560a6fbf6f0121c5fc34eb68/importlib_metadata-8.6.1-py3-none-any.whl", hash = "sha256:02a89390c1e15fdfdc0d7c6b25cb3e62650d0494005c97d6f148bf5b9787525e", size = 26971, upload-time = "2025-01-20T22:21:29.177Z" }, ] [[package]] @@ -575,38 +606,38 @@ name = "importlib-resources" version = "6.5.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "zipp", marker = "python_full_version < '3.10'" }, + { name = "zipp" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cf/8c/f834fbf984f691b4f7ff60f50b514cc3de5cc08abfc3295564dd89c5e2e7/importlib_resources-6.5.2.tar.gz", hash = "sha256:185f87adef5bcc288449d98fb4fba07cea78bc036455dd44c5fc4a2fe78fed2c", size = 44693 } +sdist = { url = "https://files.pythonhosted.org/packages/cf/8c/f834fbf984f691b4f7ff60f50b514cc3de5cc08abfc3295564dd89c5e2e7/importlib_resources-6.5.2.tar.gz", hash = "sha256:185f87adef5bcc288449d98fb4fba07cea78bc036455dd44c5fc4a2fe78fed2c", size = 44693, upload-time = "2025-01-03T18:51:56.698Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl", hash = "sha256:789cfdc3ed28c78b67a06acb8126751ced69a3d5f79c095a98298cd8a760ccec", size = 37461 }, + { url = "https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl", hash = "sha256:789cfdc3ed28c78b67a06acb8126751ced69a3d5f79c095a98298cd8a760ccec", size = 37461, upload-time = "2025-01-03T18:51:54.306Z" }, ] [[package]] name = "iniconfig" version = "2.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793 } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050 }, + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, ] [[package]] name = "isort" version = "6.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b8/21/1e2a441f74a653a144224d7d21afe8f4169e6c7c20bb13aec3a2dc3815e0/isort-6.0.1.tar.gz", hash = "sha256:1cb5df28dfbc742e490c5e41bad6da41b805b0a8be7bc93cd0fb2a8a890ac450", size = 821955 } +sdist = { url = "https://files.pythonhosted.org/packages/b8/21/1e2a441f74a653a144224d7d21afe8f4169e6c7c20bb13aec3a2dc3815e0/isort-6.0.1.tar.gz", hash = "sha256:1cb5df28dfbc742e490c5e41bad6da41b805b0a8be7bc93cd0fb2a8a890ac450", size = 821955, upload-time = "2025-02-26T21:13:16.955Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/11/114d0a5f4dabbdcedc1125dee0888514c3c3b16d3e9facad87ed96fad97c/isort-6.0.1-py3-none-any.whl", hash = "sha256:2dc5d7f65c9678d94c88dfc29161a320eec67328bc97aad576874cb4be1e9615", size = 94186 }, + { url = "https://files.pythonhosted.org/packages/c1/11/114d0a5f4dabbdcedc1125dee0888514c3c3b16d3e9facad87ed96fad97c/isort-6.0.1-py3-none-any.whl", hash = "sha256:2dc5d7f65c9678d94c88dfc29161a320eec67328bc97aad576874cb4be1e9615", size = 94186, upload-time = "2025-02-26T21:13:14.911Z" }, ] [[package]] name = "joblib" version = "1.4.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/64/33/60135848598c076ce4b231e1b1895170f45fbcaeaa2c9d5e38b04db70c35/joblib-1.4.2.tar.gz", hash = "sha256:2382c5816b2636fbd20a09e0f4e9dad4736765fdfb7dca582943b9c1366b3f0e", size = 2116621 } +sdist = { url = "https://files.pythonhosted.org/packages/64/33/60135848598c076ce4b231e1b1895170f45fbcaeaa2c9d5e38b04db70c35/joblib-1.4.2.tar.gz", hash = "sha256:2382c5816b2636fbd20a09e0f4e9dad4736765fdfb7dca582943b9c1366b3f0e", size = 2116621, upload-time = "2024-05-02T12:15:05.765Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/91/29/df4b9b42f2be0b623cbd5e2140cafcaa2bef0759a00b7b70104dcfe2fb51/joblib-1.4.2-py3-none-any.whl", hash = "sha256:06d478d5674cbc267e7496a410ee875abd68e4340feff4490bcb7afb88060ae6", size = 301817 }, + { url = "https://files.pythonhosted.org/packages/91/29/df4b9b42f2be0b623cbd5e2140cafcaa2bef0759a00b7b70104dcfe2fb51/joblib-1.4.2-py3-none-any.whl", hash = "sha256:06d478d5674cbc267e7496a410ee875abd68e4340feff4490bcb7afb88060ae6", size = 301817, upload-time = "2024-05-02T12:15:00.765Z" }, ] [[package]] @@ -618,84 +649,84 @@ resolution-markers = [ "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", ] -sdist = { url = "https://files.pythonhosted.org/packages/85/4d/2255e1c76304cbd60b48cee302b66d1dde4468dc5b1160e4b7cb43778f2a/kiwisolver-1.4.7.tar.gz", hash = "sha256:9893ff81bd7107f7b685d3017cc6583daadb4fc26e4a888350df530e41980a60", size = 97286 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/97/14/fc943dd65268a96347472b4fbe5dcc2f6f55034516f80576cd0dd3a8930f/kiwisolver-1.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8a9c83f75223d5e48b0bc9cb1bf2776cf01563e00ade8775ffe13b0b6e1af3a6", size = 122440 }, - { url = "https://files.pythonhosted.org/packages/1e/46/e68fed66236b69dd02fcdb506218c05ac0e39745d696d22709498896875d/kiwisolver-1.4.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:58370b1ffbd35407444d57057b57da5d6549d2d854fa30249771775c63b5fe17", size = 65758 }, - { url = "https://files.pythonhosted.org/packages/ef/fa/65de49c85838681fc9cb05de2a68067a683717321e01ddafb5b8024286f0/kiwisolver-1.4.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:aa0abdf853e09aff551db11fce173e2177d00786c688203f52c87ad7fcd91ef9", size = 64311 }, - { url = "https://files.pythonhosted.org/packages/42/9c/cc8d90f6ef550f65443bad5872ffa68f3dee36de4974768628bea7c14979/kiwisolver-1.4.7-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8d53103597a252fb3ab8b5845af04c7a26d5e7ea8122303dd7a021176a87e8b9", size = 1637109 }, - { url = "https://files.pythonhosted.org/packages/55/91/0a57ce324caf2ff5403edab71c508dd8f648094b18cfbb4c8cc0fde4a6ac/kiwisolver-1.4.7-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:88f17c5ffa8e9462fb79f62746428dd57b46eb931698e42e990ad63103f35e6c", size = 1617814 }, - { url = "https://files.pythonhosted.org/packages/12/5d/c36140313f2510e20207708adf36ae4919416d697ee0236b0ddfb6fd1050/kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88a9ca9c710d598fd75ee5de59d5bda2684d9db36a9f50b6125eaea3969c2599", size = 1400881 }, - { url = "https://files.pythonhosted.org/packages/56/d0/786e524f9ed648324a466ca8df86298780ef2b29c25313d9a4f16992d3cf/kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f4d742cb7af1c28303a51b7a27aaee540e71bb8e24f68c736f6f2ffc82f2bf05", size = 1512972 }, - { url = "https://files.pythonhosted.org/packages/67/5a/77851f2f201e6141d63c10a0708e996a1363efaf9e1609ad0441b343763b/kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e28c7fea2196bf4c2f8d46a0415c77a1c480cc0724722f23d7410ffe9842c407", size = 1444787 }, - { url = "https://files.pythonhosted.org/packages/06/5f/1f5eaab84355885e224a6fc8d73089e8713dc7e91c121f00b9a1c58a2195/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e968b84db54f9d42046cf154e02911e39c0435c9801681e3fc9ce8a3c4130278", size = 2199212 }, - { url = "https://files.pythonhosted.org/packages/b5/28/9152a3bfe976a0ae21d445415defc9d1cd8614b2910b7614b30b27a47270/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0c18ec74c0472de033e1bebb2911c3c310eef5649133dd0bedf2a169a1b269e5", size = 2346399 }, - { url = "https://files.pythonhosted.org/packages/26/f6/453d1904c52ac3b400f4d5e240ac5fec25263716723e44be65f4d7149d13/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8f0ea6da6d393d8b2e187e6a5e3fb81f5862010a40c3945e2c6d12ae45cfb2ad", size = 2308688 }, - { url = "https://files.pythonhosted.org/packages/5a/9a/d4968499441b9ae187e81745e3277a8b4d7c60840a52dc9d535a7909fac3/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:f106407dda69ae456dd1227966bf445b157ccc80ba0dff3802bb63f30b74e895", size = 2445493 }, - { url = "https://files.pythonhosted.org/packages/07/c9/032267192e7828520dacb64dfdb1d74f292765f179e467c1cba97687f17d/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:84ec80df401cfee1457063732d90022f93951944b5b58975d34ab56bb150dfb3", size = 2262191 }, - { url = "https://files.pythonhosted.org/packages/6c/ad/db0aedb638a58b2951da46ddaeecf204be8b4f5454df020d850c7fa8dca8/kiwisolver-1.4.7-cp310-cp310-win32.whl", hash = "sha256:71bb308552200fb2c195e35ef05de12f0c878c07fc91c270eb3d6e41698c3bcc", size = 46644 }, - { url = "https://files.pythonhosted.org/packages/12/ca/d0f7b7ffbb0be1e7c2258b53554efec1fd652921f10d7d85045aff93ab61/kiwisolver-1.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:44756f9fd339de0fb6ee4f8c1696cfd19b2422e0d70b4cefc1cc7f1f64045a8c", size = 55877 }, - { url = "https://files.pythonhosted.org/packages/97/6c/cfcc128672f47a3e3c0d918ecb67830600078b025bfc32d858f2e2d5c6a4/kiwisolver-1.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:78a42513018c41c2ffd262eb676442315cbfe3c44eed82385c2ed043bc63210a", size = 48347 }, - { url = "https://files.pythonhosted.org/packages/e9/44/77429fa0a58f941d6e1c58da9efe08597d2e86bf2b2cce6626834f49d07b/kiwisolver-1.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d2b0e12a42fb4e72d509fc994713d099cbb15ebf1103545e8a45f14da2dfca54", size = 122442 }, - { url = "https://files.pythonhosted.org/packages/e5/20/8c75caed8f2462d63c7fd65e16c832b8f76cda331ac9e615e914ee80bac9/kiwisolver-1.4.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2a8781ac3edc42ea4b90bc23e7d37b665d89423818e26eb6df90698aa2287c95", size = 65762 }, - { url = "https://files.pythonhosted.org/packages/f4/98/fe010f15dc7230f45bc4cf367b012d651367fd203caaa992fd1f5963560e/kiwisolver-1.4.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:46707a10836894b559e04b0fd143e343945c97fd170d69a2d26d640b4e297935", size = 64319 }, - { url = "https://files.pythonhosted.org/packages/8b/1b/b5d618f4e58c0675654c1e5051bcf42c776703edb21c02b8c74135541f60/kiwisolver-1.4.7-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef97b8df011141c9b0f6caf23b29379f87dd13183c978a30a3c546d2c47314cb", size = 1334260 }, - { url = "https://files.pythonhosted.org/packages/b8/01/946852b13057a162a8c32c4c8d2e9ed79f0bb5d86569a40c0b5fb103e373/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ab58c12a2cd0fc769089e6d38466c46d7f76aced0a1f54c77652446733d2d02", size = 1426589 }, - { url = "https://files.pythonhosted.org/packages/70/d1/c9f96df26b459e15cf8a965304e6e6f4eb291e0f7a9460b4ad97b047561e/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:803b8e1459341c1bb56d1c5c010406d5edec8a0713a0945851290a7930679b51", size = 1541080 }, - { url = "https://files.pythonhosted.org/packages/d3/73/2686990eb8b02d05f3de759d6a23a4ee7d491e659007dd4c075fede4b5d0/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f9a9e8a507420fe35992ee9ecb302dab68550dedc0da9e2880dd88071c5fb052", size = 1470049 }, - { url = "https://files.pythonhosted.org/packages/a7/4b/2db7af3ed3af7c35f388d5f53c28e155cd402a55432d800c543dc6deb731/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18077b53dc3bb490e330669a99920c5e6a496889ae8c63b58fbc57c3d7f33a18", size = 1426376 }, - { url = "https://files.pythonhosted.org/packages/05/83/2857317d04ea46dc5d115f0df7e676997bbd968ced8e2bd6f7f19cfc8d7f/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6af936f79086a89b3680a280c47ea90b4df7047b5bdf3aa5c524bbedddb9e545", size = 2222231 }, - { url = "https://files.pythonhosted.org/packages/0d/b5/866f86f5897cd4ab6d25d22e403404766a123f138bd6a02ecb2cdde52c18/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:3abc5b19d24af4b77d1598a585b8a719beb8569a71568b66f4ebe1fb0449460b", size = 2368634 }, - { url = "https://files.pythonhosted.org/packages/c1/ee/73de8385403faba55f782a41260210528fe3273d0cddcf6d51648202d6d0/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:933d4de052939d90afbe6e9d5273ae05fb836cc86c15b686edd4b3560cc0ee36", size = 2329024 }, - { url = "https://files.pythonhosted.org/packages/a1/e7/cd101d8cd2cdfaa42dc06c433df17c8303d31129c9fdd16c0ea37672af91/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:65e720d2ab2b53f1f72fb5da5fb477455905ce2c88aaa671ff0a447c2c80e8e3", size = 2468484 }, - { url = "https://files.pythonhosted.org/packages/e1/72/84f09d45a10bc57a40bb58b81b99d8f22b58b2040c912b7eb97ebf625bf2/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3bf1ed55088f214ba6427484c59553123fdd9b218a42bbc8c6496d6754b1e523", size = 2284078 }, - { url = "https://files.pythonhosted.org/packages/d2/d4/71828f32b956612dc36efd7be1788980cb1e66bfb3706e6dec9acad9b4f9/kiwisolver-1.4.7-cp311-cp311-win32.whl", hash = "sha256:4c00336b9dd5ad96d0a558fd18a8b6f711b7449acce4c157e7343ba92dd0cf3d", size = 46645 }, - { url = "https://files.pythonhosted.org/packages/a1/65/d43e9a20aabcf2e798ad1aff6c143ae3a42cf506754bcb6a7ed8259c8425/kiwisolver-1.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:929e294c1ac1e9f615c62a4e4313ca1823ba37326c164ec720a803287c4c499b", size = 56022 }, - { url = "https://files.pythonhosted.org/packages/35/b3/9f75a2e06f1b4ca00b2b192bc2b739334127d27f1d0625627ff8479302ba/kiwisolver-1.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:e33e8fbd440c917106b237ef1a2f1449dfbb9b6f6e1ce17c94cd6a1e0d438376", size = 48536 }, - { url = "https://files.pythonhosted.org/packages/97/9c/0a11c714cf8b6ef91001c8212c4ef207f772dd84540104952c45c1f0a249/kiwisolver-1.4.7-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:5360cc32706dab3931f738d3079652d20982511f7c0ac5711483e6eab08efff2", size = 121808 }, - { url = "https://files.pythonhosted.org/packages/f2/d8/0fe8c5f5d35878ddd135f44f2af0e4e1d379e1c7b0716f97cdcb88d4fd27/kiwisolver-1.4.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:942216596dc64ddb25adb215c3c783215b23626f8d84e8eff8d6d45c3f29f75a", size = 65531 }, - { url = "https://files.pythonhosted.org/packages/80/c5/57fa58276dfdfa612241d640a64ca2f76adc6ffcebdbd135b4ef60095098/kiwisolver-1.4.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:48b571ecd8bae15702e4f22d3ff6a0f13e54d3d00cd25216d5e7f658242065ee", size = 63894 }, - { url = "https://files.pythonhosted.org/packages/8b/e9/26d3edd4c4ad1c5b891d8747a4f81b1b0aba9fb9721de6600a4adc09773b/kiwisolver-1.4.7-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ad42ba922c67c5f219097b28fae965e10045ddf145d2928bfac2eb2e17673640", size = 1369296 }, - { url = "https://files.pythonhosted.org/packages/b6/67/3f4850b5e6cffb75ec40577ddf54f7b82b15269cc5097ff2e968ee32ea7d/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:612a10bdae23404a72941a0fc8fa2660c6ea1217c4ce0dbcab8a8f6543ea9e7f", size = 1461450 }, - { url = "https://files.pythonhosted.org/packages/52/be/86cbb9c9a315e98a8dc6b1d23c43cffd91d97d49318854f9c37b0e41cd68/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9e838bba3a3bac0fe06d849d29772eb1afb9745a59710762e4ba3f4cb8424483", size = 1579168 }, - { url = "https://files.pythonhosted.org/packages/0f/00/65061acf64bd5fd34c1f4ae53f20b43b0a017a541f242a60b135b9d1e301/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:22f499f6157236c19f4bbbd472fa55b063db77a16cd74d49afe28992dff8c258", size = 1507308 }, - { url = "https://files.pythonhosted.org/packages/21/e4/c0b6746fd2eb62fe702118b3ca0cb384ce95e1261cfada58ff693aeec08a/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:693902d433cf585133699972b6d7c42a8b9f8f826ebcaf0132ff55200afc599e", size = 1464186 }, - { url = "https://files.pythonhosted.org/packages/0a/0f/529d0a9fffb4d514f2782c829b0b4b371f7f441d61aa55f1de1c614c4ef3/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4e77f2126c3e0b0d055f44513ed349038ac180371ed9b52fe96a32aa071a5107", size = 2247877 }, - { url = "https://files.pythonhosted.org/packages/d1/e1/66603ad779258843036d45adcbe1af0d1a889a07af4635f8b4ec7dccda35/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:657a05857bda581c3656bfc3b20e353c232e9193eb167766ad2dc58b56504948", size = 2404204 }, - { url = "https://files.pythonhosted.org/packages/8d/61/de5fb1ca7ad1f9ab7970e340a5b833d735df24689047de6ae71ab9d8d0e7/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4bfa75a048c056a411f9705856abfc872558e33c055d80af6a380e3658766038", size = 2352461 }, - { url = "https://files.pythonhosted.org/packages/ba/d2/0edc00a852e369827f7e05fd008275f550353f1f9bcd55db9363d779fc63/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:34ea1de54beef1c104422d210c47c7d2a4999bdecf42c7b5718fbe59a4cac383", size = 2501358 }, - { url = "https://files.pythonhosted.org/packages/84/15/adc15a483506aec6986c01fb7f237c3aec4d9ed4ac10b756e98a76835933/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:90da3b5f694b85231cf93586dad5e90e2d71b9428f9aad96952c99055582f520", size = 2314119 }, - { url = "https://files.pythonhosted.org/packages/36/08/3a5bb2c53c89660863a5aa1ee236912269f2af8762af04a2e11df851d7b2/kiwisolver-1.4.7-cp312-cp312-win32.whl", hash = "sha256:18e0cca3e008e17fe9b164b55735a325140a5a35faad8de92dd80265cd5eb80b", size = 46367 }, - { url = "https://files.pythonhosted.org/packages/19/93/c05f0a6d825c643779fc3c70876bff1ac221f0e31e6f701f0e9578690d70/kiwisolver-1.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:58cb20602b18f86f83a5c87d3ee1c766a79c0d452f8def86d925e6c60fbf7bfb", size = 55884 }, - { url = "https://files.pythonhosted.org/packages/d2/f9/3828d8f21b6de4279f0667fb50a9f5215e6fe57d5ec0d61905914f5b6099/kiwisolver-1.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:f5a8b53bdc0b3961f8b6125e198617c40aeed638b387913bf1ce78afb1b0be2a", size = 48528 }, - { url = "https://files.pythonhosted.org/packages/11/88/37ea0ea64512997b13d69772db8dcdc3bfca5442cda3a5e4bb943652ee3e/kiwisolver-1.4.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3f9362ecfca44c863569d3d3c033dbe8ba452ff8eed6f6b5806382741a1334bd", size = 122449 }, - { url = "https://files.pythonhosted.org/packages/4e/45/5a5c46078362cb3882dcacad687c503089263c017ca1241e0483857791eb/kiwisolver-1.4.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e8df2eb9b2bac43ef8b082e06f750350fbbaf2887534a5be97f6cf07b19d9583", size = 65757 }, - { url = "https://files.pythonhosted.org/packages/8a/be/a6ae58978772f685d48dd2e84460937761c53c4bbd84e42b0336473d9775/kiwisolver-1.4.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f32d6edbc638cde7652bd690c3e728b25332acbadd7cad670cc4a02558d9c417", size = 64312 }, - { url = "https://files.pythonhosted.org/packages/f4/04/18ef6f452d311e1e1eb180c9bf5589187fa1f042db877e6fe443ef10099c/kiwisolver-1.4.7-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e2e6c39bd7b9372b0be21456caab138e8e69cc0fc1190a9dfa92bd45a1e6e904", size = 1626966 }, - { url = "https://files.pythonhosted.org/packages/21/b1/40655f6c3fa11ce740e8a964fa8e4c0479c87d6a7944b95af799c7a55dfe/kiwisolver-1.4.7-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dda56c24d869b1193fcc763f1284b9126550eaf84b88bbc7256e15028f19188a", size = 1607044 }, - { url = "https://files.pythonhosted.org/packages/fd/93/af67dbcfb9b3323bbd2c2db1385a7139d8f77630e4a37bb945b57188eb2d/kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79849239c39b5e1fd906556c474d9b0439ea6792b637511f3fe3a41158d89ca8", size = 1391879 }, - { url = "https://files.pythonhosted.org/packages/40/6f/d60770ef98e77b365d96061d090c0cd9e23418121c55fff188fa4bdf0b54/kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5e3bc157fed2a4c02ec468de4ecd12a6e22818d4f09cde2c31ee3226ffbefab2", size = 1504751 }, - { url = "https://files.pythonhosted.org/packages/fa/3a/5f38667d313e983c432f3fcd86932177519ed8790c724e07d77d1de0188a/kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3da53da805b71e41053dc670f9a820d1157aae77b6b944e08024d17bcd51ef88", size = 1436990 }, - { url = "https://files.pythonhosted.org/packages/cb/3b/1520301a47326e6a6043b502647e42892be33b3f051e9791cc8bb43f1a32/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8705f17dfeb43139a692298cb6637ee2e59c0194538153e83e9ee0c75c2eddde", size = 2191122 }, - { url = "https://files.pythonhosted.org/packages/cf/c4/eb52da300c166239a2233f1f9c4a1b767dfab98fae27681bfb7ea4873cb6/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:82a5c2f4b87c26bb1a0ef3d16b5c4753434633b83d365cc0ddf2770c93829e3c", size = 2338126 }, - { url = "https://files.pythonhosted.org/packages/1a/cb/42b92fd5eadd708dd9107c089e817945500685f3437ce1fd387efebc6d6e/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ce8be0466f4c0d585cdb6c1e2ed07232221df101a4c6f28821d2aa754ca2d9e2", size = 2298313 }, - { url = "https://files.pythonhosted.org/packages/4f/eb/be25aa791fe5fc75a8b1e0c965e00f942496bc04635c9aae8035f6b76dcd/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:409afdfe1e2e90e6ee7fc896f3df9a7fec8e793e58bfa0d052c8a82f99c37abb", size = 2437784 }, - { url = "https://files.pythonhosted.org/packages/c5/22/30a66be7f3368d76ff95689e1c2e28d382383952964ab15330a15d8bfd03/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5b9c3f4ee0b9a439d2415012bd1b1cc2df59e4d6a9939f4d669241d30b414327", size = 2253988 }, - { url = "https://files.pythonhosted.org/packages/35/d3/5f2ecb94b5211c8a04f218a76133cc8d6d153b0f9cd0b45fad79907f0689/kiwisolver-1.4.7-cp39-cp39-win32.whl", hash = "sha256:a79ae34384df2b615eefca647a2873842ac3b596418032bef9a7283675962644", size = 46980 }, - { url = "https://files.pythonhosted.org/packages/ef/17/cd10d020578764ea91740204edc6b3236ed8106228a46f568d716b11feb2/kiwisolver-1.4.7-cp39-cp39-win_amd64.whl", hash = "sha256:cf0438b42121a66a3a667de17e779330fc0f20b0d97d59d2f2121e182b0505e4", size = 55847 }, - { url = "https://files.pythonhosted.org/packages/91/84/32232502020bd78d1d12be7afde15811c64a95ed1f606c10456db4e4c3ac/kiwisolver-1.4.7-cp39-cp39-win_arm64.whl", hash = "sha256:764202cc7e70f767dab49e8df52c7455e8de0df5d858fa801a11aa0d882ccf3f", size = 48494 }, - { url = "https://files.pythonhosted.org/packages/ac/59/741b79775d67ab67ced9bb38552da688c0305c16e7ee24bba7a2be253fb7/kiwisolver-1.4.7-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:94252291e3fe68001b1dd747b4c0b3be12582839b95ad4d1b641924d68fd4643", size = 59491 }, - { url = "https://files.pythonhosted.org/packages/58/cc/fb239294c29a5656e99e3527f7369b174dd9cc7c3ef2dea7cb3c54a8737b/kiwisolver-1.4.7-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5b7dfa3b546da08a9f622bb6becdb14b3e24aaa30adba66749d38f3cc7ea9706", size = 57648 }, - { url = "https://files.pythonhosted.org/packages/3b/ef/2f009ac1f7aab9f81efb2d837301d255279d618d27b6015780115ac64bdd/kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd3de6481f4ed8b734da5df134cd5a6a64fe32124fe83dde1e5b5f29fe30b1e6", size = 84257 }, - { url = "https://files.pythonhosted.org/packages/81/e1/c64f50987f85b68b1c52b464bb5bf73e71570c0f7782d626d1eb283ad620/kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a91b5f9f1205845d488c928e8570dcb62b893372f63b8b6e98b863ebd2368ff2", size = 80906 }, - { url = "https://files.pythonhosted.org/packages/fd/71/1687c5c0a0be2cee39a5c9c389e546f9c6e215e46b691d00d9f646892083/kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40fa14dbd66b8b8f470d5fc79c089a66185619d31645f9b0773b88b19f7223c4", size = 79951 }, - { url = "https://files.pythonhosted.org/packages/ea/8b/d7497df4a1cae9367adf21665dd1f896c2a7aeb8769ad77b662c5e2bcce7/kiwisolver-1.4.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:eb542fe7933aa09d8d8f9d9097ef37532a7df6497819d16efe4359890a2f417a", size = 55715 }, - { url = "https://files.pythonhosted.org/packages/d5/df/ce37d9b26f07ab90880923c94d12a6ff4d27447096b4c849bfc4339ccfdf/kiwisolver-1.4.7-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8b01aac285f91ca889c800042c35ad3b239e704b150cfd3382adfc9dcc780e39", size = 58666 }, - { url = "https://files.pythonhosted.org/packages/b0/d3/e4b04f43bc629ac8e186b77b2b1a251cdfa5b7610fa189dc0db622672ce6/kiwisolver-1.4.7-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:48be928f59a1f5c8207154f935334d374e79f2b5d212826307d072595ad76a2e", size = 57088 }, - { url = "https://files.pythonhosted.org/packages/30/1c/752df58e2d339e670a535514d2db4fe8c842ce459776b8080fbe08ebb98e/kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f37cfe618a117e50d8c240555331160d73d0411422b59b5ee217843d7b693608", size = 84321 }, - { url = "https://files.pythonhosted.org/packages/f0/f8/fe6484e847bc6e238ec9f9828089fb2c0bb53f2f5f3a79351fde5b565e4f/kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:599b5c873c63a1f6ed7eead644a8a380cfbdf5db91dcb6f85707aaab213b1674", size = 80776 }, - { url = "https://files.pythonhosted.org/packages/9b/57/d7163c0379f250ef763aba85330a19feefb5ce6cb541ade853aaba881524/kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:801fa7802e5cfabe3ab0c81a34c323a319b097dfb5004be950482d882f3d7225", size = 79984 }, - { url = "https://files.pythonhosted.org/packages/8c/95/4a103776c265d13b3d2cd24fb0494d4e04ea435a8ef97e1b2c026d43250b/kiwisolver-1.4.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:0c6c43471bc764fad4bc99c5c2d6d16a676b1abf844ca7c8702bdae92df01ee0", size = 55811 }, +sdist = { url = "https://files.pythonhosted.org/packages/85/4d/2255e1c76304cbd60b48cee302b66d1dde4468dc5b1160e4b7cb43778f2a/kiwisolver-1.4.7.tar.gz", hash = "sha256:9893ff81bd7107f7b685d3017cc6583daadb4fc26e4a888350df530e41980a60", size = 97286, upload-time = "2024-09-04T09:39:44.302Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/14/fc943dd65268a96347472b4fbe5dcc2f6f55034516f80576cd0dd3a8930f/kiwisolver-1.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8a9c83f75223d5e48b0bc9cb1bf2776cf01563e00ade8775ffe13b0b6e1af3a6", size = 122440, upload-time = "2024-09-04T09:03:44.9Z" }, + { url = "https://files.pythonhosted.org/packages/1e/46/e68fed66236b69dd02fcdb506218c05ac0e39745d696d22709498896875d/kiwisolver-1.4.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:58370b1ffbd35407444d57057b57da5d6549d2d854fa30249771775c63b5fe17", size = 65758, upload-time = "2024-09-04T09:03:46.582Z" }, + { url = "https://files.pythonhosted.org/packages/ef/fa/65de49c85838681fc9cb05de2a68067a683717321e01ddafb5b8024286f0/kiwisolver-1.4.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:aa0abdf853e09aff551db11fce173e2177d00786c688203f52c87ad7fcd91ef9", size = 64311, upload-time = "2024-09-04T09:03:47.973Z" }, + { url = "https://files.pythonhosted.org/packages/42/9c/cc8d90f6ef550f65443bad5872ffa68f3dee36de4974768628bea7c14979/kiwisolver-1.4.7-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8d53103597a252fb3ab8b5845af04c7a26d5e7ea8122303dd7a021176a87e8b9", size = 1637109, upload-time = "2024-09-04T09:03:49.281Z" }, + { url = "https://files.pythonhosted.org/packages/55/91/0a57ce324caf2ff5403edab71c508dd8f648094b18cfbb4c8cc0fde4a6ac/kiwisolver-1.4.7-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:88f17c5ffa8e9462fb79f62746428dd57b46eb931698e42e990ad63103f35e6c", size = 1617814, upload-time = "2024-09-04T09:03:51.444Z" }, + { url = "https://files.pythonhosted.org/packages/12/5d/c36140313f2510e20207708adf36ae4919416d697ee0236b0ddfb6fd1050/kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88a9ca9c710d598fd75ee5de59d5bda2684d9db36a9f50b6125eaea3969c2599", size = 1400881, upload-time = "2024-09-04T09:03:53.357Z" }, + { url = "https://files.pythonhosted.org/packages/56/d0/786e524f9ed648324a466ca8df86298780ef2b29c25313d9a4f16992d3cf/kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f4d742cb7af1c28303a51b7a27aaee540e71bb8e24f68c736f6f2ffc82f2bf05", size = 1512972, upload-time = "2024-09-04T09:03:55.082Z" }, + { url = "https://files.pythonhosted.org/packages/67/5a/77851f2f201e6141d63c10a0708e996a1363efaf9e1609ad0441b343763b/kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e28c7fea2196bf4c2f8d46a0415c77a1c480cc0724722f23d7410ffe9842c407", size = 1444787, upload-time = "2024-09-04T09:03:56.588Z" }, + { url = "https://files.pythonhosted.org/packages/06/5f/1f5eaab84355885e224a6fc8d73089e8713dc7e91c121f00b9a1c58a2195/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e968b84db54f9d42046cf154e02911e39c0435c9801681e3fc9ce8a3c4130278", size = 2199212, upload-time = "2024-09-04T09:03:58.557Z" }, + { url = "https://files.pythonhosted.org/packages/b5/28/9152a3bfe976a0ae21d445415defc9d1cd8614b2910b7614b30b27a47270/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0c18ec74c0472de033e1bebb2911c3c310eef5649133dd0bedf2a169a1b269e5", size = 2346399, upload-time = "2024-09-04T09:04:00.178Z" }, + { url = "https://files.pythonhosted.org/packages/26/f6/453d1904c52ac3b400f4d5e240ac5fec25263716723e44be65f4d7149d13/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8f0ea6da6d393d8b2e187e6a5e3fb81f5862010a40c3945e2c6d12ae45cfb2ad", size = 2308688, upload-time = "2024-09-04T09:04:02.216Z" }, + { url = "https://files.pythonhosted.org/packages/5a/9a/d4968499441b9ae187e81745e3277a8b4d7c60840a52dc9d535a7909fac3/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:f106407dda69ae456dd1227966bf445b157ccc80ba0dff3802bb63f30b74e895", size = 2445493, upload-time = "2024-09-04T09:04:04.571Z" }, + { url = "https://files.pythonhosted.org/packages/07/c9/032267192e7828520dacb64dfdb1d74f292765f179e467c1cba97687f17d/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:84ec80df401cfee1457063732d90022f93951944b5b58975d34ab56bb150dfb3", size = 2262191, upload-time = "2024-09-04T09:04:05.969Z" }, + { url = "https://files.pythonhosted.org/packages/6c/ad/db0aedb638a58b2951da46ddaeecf204be8b4f5454df020d850c7fa8dca8/kiwisolver-1.4.7-cp310-cp310-win32.whl", hash = "sha256:71bb308552200fb2c195e35ef05de12f0c878c07fc91c270eb3d6e41698c3bcc", size = 46644, upload-time = "2024-09-04T09:04:07.408Z" }, + { url = "https://files.pythonhosted.org/packages/12/ca/d0f7b7ffbb0be1e7c2258b53554efec1fd652921f10d7d85045aff93ab61/kiwisolver-1.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:44756f9fd339de0fb6ee4f8c1696cfd19b2422e0d70b4cefc1cc7f1f64045a8c", size = 55877, upload-time = "2024-09-04T09:04:08.869Z" }, + { url = "https://files.pythonhosted.org/packages/97/6c/cfcc128672f47a3e3c0d918ecb67830600078b025bfc32d858f2e2d5c6a4/kiwisolver-1.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:78a42513018c41c2ffd262eb676442315cbfe3c44eed82385c2ed043bc63210a", size = 48347, upload-time = "2024-09-04T09:04:10.106Z" }, + { url = "https://files.pythonhosted.org/packages/e9/44/77429fa0a58f941d6e1c58da9efe08597d2e86bf2b2cce6626834f49d07b/kiwisolver-1.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d2b0e12a42fb4e72d509fc994713d099cbb15ebf1103545e8a45f14da2dfca54", size = 122442, upload-time = "2024-09-04T09:04:11.432Z" }, + { url = "https://files.pythonhosted.org/packages/e5/20/8c75caed8f2462d63c7fd65e16c832b8f76cda331ac9e615e914ee80bac9/kiwisolver-1.4.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2a8781ac3edc42ea4b90bc23e7d37b665d89423818e26eb6df90698aa2287c95", size = 65762, upload-time = "2024-09-04T09:04:12.468Z" }, + { url = "https://files.pythonhosted.org/packages/f4/98/fe010f15dc7230f45bc4cf367b012d651367fd203caaa992fd1f5963560e/kiwisolver-1.4.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:46707a10836894b559e04b0fd143e343945c97fd170d69a2d26d640b4e297935", size = 64319, upload-time = "2024-09-04T09:04:13.635Z" }, + { url = "https://files.pythonhosted.org/packages/8b/1b/b5d618f4e58c0675654c1e5051bcf42c776703edb21c02b8c74135541f60/kiwisolver-1.4.7-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef97b8df011141c9b0f6caf23b29379f87dd13183c978a30a3c546d2c47314cb", size = 1334260, upload-time = "2024-09-04T09:04:14.878Z" }, + { url = "https://files.pythonhosted.org/packages/b8/01/946852b13057a162a8c32c4c8d2e9ed79f0bb5d86569a40c0b5fb103e373/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ab58c12a2cd0fc769089e6d38466c46d7f76aced0a1f54c77652446733d2d02", size = 1426589, upload-time = "2024-09-04T09:04:16.514Z" }, + { url = "https://files.pythonhosted.org/packages/70/d1/c9f96df26b459e15cf8a965304e6e6f4eb291e0f7a9460b4ad97b047561e/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:803b8e1459341c1bb56d1c5c010406d5edec8a0713a0945851290a7930679b51", size = 1541080, upload-time = "2024-09-04T09:04:18.322Z" }, + { url = "https://files.pythonhosted.org/packages/d3/73/2686990eb8b02d05f3de759d6a23a4ee7d491e659007dd4c075fede4b5d0/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f9a9e8a507420fe35992ee9ecb302dab68550dedc0da9e2880dd88071c5fb052", size = 1470049, upload-time = "2024-09-04T09:04:20.266Z" }, + { url = "https://files.pythonhosted.org/packages/a7/4b/2db7af3ed3af7c35f388d5f53c28e155cd402a55432d800c543dc6deb731/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18077b53dc3bb490e330669a99920c5e6a496889ae8c63b58fbc57c3d7f33a18", size = 1426376, upload-time = "2024-09-04T09:04:22.419Z" }, + { url = "https://files.pythonhosted.org/packages/05/83/2857317d04ea46dc5d115f0df7e676997bbd968ced8e2bd6f7f19cfc8d7f/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6af936f79086a89b3680a280c47ea90b4df7047b5bdf3aa5c524bbedddb9e545", size = 2222231, upload-time = "2024-09-04T09:04:24.526Z" }, + { url = "https://files.pythonhosted.org/packages/0d/b5/866f86f5897cd4ab6d25d22e403404766a123f138bd6a02ecb2cdde52c18/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:3abc5b19d24af4b77d1598a585b8a719beb8569a71568b66f4ebe1fb0449460b", size = 2368634, upload-time = "2024-09-04T09:04:25.899Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ee/73de8385403faba55f782a41260210528fe3273d0cddcf6d51648202d6d0/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:933d4de052939d90afbe6e9d5273ae05fb836cc86c15b686edd4b3560cc0ee36", size = 2329024, upload-time = "2024-09-04T09:04:28.523Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e7/cd101d8cd2cdfaa42dc06c433df17c8303d31129c9fdd16c0ea37672af91/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:65e720d2ab2b53f1f72fb5da5fb477455905ce2c88aaa671ff0a447c2c80e8e3", size = 2468484, upload-time = "2024-09-04T09:04:30.547Z" }, + { url = "https://files.pythonhosted.org/packages/e1/72/84f09d45a10bc57a40bb58b81b99d8f22b58b2040c912b7eb97ebf625bf2/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3bf1ed55088f214ba6427484c59553123fdd9b218a42bbc8c6496d6754b1e523", size = 2284078, upload-time = "2024-09-04T09:04:33.218Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d4/71828f32b956612dc36efd7be1788980cb1e66bfb3706e6dec9acad9b4f9/kiwisolver-1.4.7-cp311-cp311-win32.whl", hash = "sha256:4c00336b9dd5ad96d0a558fd18a8b6f711b7449acce4c157e7343ba92dd0cf3d", size = 46645, upload-time = "2024-09-04T09:04:34.371Z" }, + { url = "https://files.pythonhosted.org/packages/a1/65/d43e9a20aabcf2e798ad1aff6c143ae3a42cf506754bcb6a7ed8259c8425/kiwisolver-1.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:929e294c1ac1e9f615c62a4e4313ca1823ba37326c164ec720a803287c4c499b", size = 56022, upload-time = "2024-09-04T09:04:35.786Z" }, + { url = "https://files.pythonhosted.org/packages/35/b3/9f75a2e06f1b4ca00b2b192bc2b739334127d27f1d0625627ff8479302ba/kiwisolver-1.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:e33e8fbd440c917106b237ef1a2f1449dfbb9b6f6e1ce17c94cd6a1e0d438376", size = 48536, upload-time = "2024-09-04T09:04:37.525Z" }, + { url = "https://files.pythonhosted.org/packages/97/9c/0a11c714cf8b6ef91001c8212c4ef207f772dd84540104952c45c1f0a249/kiwisolver-1.4.7-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:5360cc32706dab3931f738d3079652d20982511f7c0ac5711483e6eab08efff2", size = 121808, upload-time = "2024-09-04T09:04:38.637Z" }, + { url = "https://files.pythonhosted.org/packages/f2/d8/0fe8c5f5d35878ddd135f44f2af0e4e1d379e1c7b0716f97cdcb88d4fd27/kiwisolver-1.4.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:942216596dc64ddb25adb215c3c783215b23626f8d84e8eff8d6d45c3f29f75a", size = 65531, upload-time = "2024-09-04T09:04:39.694Z" }, + { url = "https://files.pythonhosted.org/packages/80/c5/57fa58276dfdfa612241d640a64ca2f76adc6ffcebdbd135b4ef60095098/kiwisolver-1.4.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:48b571ecd8bae15702e4f22d3ff6a0f13e54d3d00cd25216d5e7f658242065ee", size = 63894, upload-time = "2024-09-04T09:04:41.6Z" }, + { url = "https://files.pythonhosted.org/packages/8b/e9/26d3edd4c4ad1c5b891d8747a4f81b1b0aba9fb9721de6600a4adc09773b/kiwisolver-1.4.7-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ad42ba922c67c5f219097b28fae965e10045ddf145d2928bfac2eb2e17673640", size = 1369296, upload-time = "2024-09-04T09:04:42.886Z" }, + { url = "https://files.pythonhosted.org/packages/b6/67/3f4850b5e6cffb75ec40577ddf54f7b82b15269cc5097ff2e968ee32ea7d/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:612a10bdae23404a72941a0fc8fa2660c6ea1217c4ce0dbcab8a8f6543ea9e7f", size = 1461450, upload-time = "2024-09-04T09:04:46.284Z" }, + { url = "https://files.pythonhosted.org/packages/52/be/86cbb9c9a315e98a8dc6b1d23c43cffd91d97d49318854f9c37b0e41cd68/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9e838bba3a3bac0fe06d849d29772eb1afb9745a59710762e4ba3f4cb8424483", size = 1579168, upload-time = "2024-09-04T09:04:47.91Z" }, + { url = "https://files.pythonhosted.org/packages/0f/00/65061acf64bd5fd34c1f4ae53f20b43b0a017a541f242a60b135b9d1e301/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:22f499f6157236c19f4bbbd472fa55b063db77a16cd74d49afe28992dff8c258", size = 1507308, upload-time = "2024-09-04T09:04:49.465Z" }, + { url = "https://files.pythonhosted.org/packages/21/e4/c0b6746fd2eb62fe702118b3ca0cb384ce95e1261cfada58ff693aeec08a/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:693902d433cf585133699972b6d7c42a8b9f8f826ebcaf0132ff55200afc599e", size = 1464186, upload-time = "2024-09-04T09:04:50.949Z" }, + { url = "https://files.pythonhosted.org/packages/0a/0f/529d0a9fffb4d514f2782c829b0b4b371f7f441d61aa55f1de1c614c4ef3/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4e77f2126c3e0b0d055f44513ed349038ac180371ed9b52fe96a32aa071a5107", size = 2247877, upload-time = "2024-09-04T09:04:52.388Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e1/66603ad779258843036d45adcbe1af0d1a889a07af4635f8b4ec7dccda35/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:657a05857bda581c3656bfc3b20e353c232e9193eb167766ad2dc58b56504948", size = 2404204, upload-time = "2024-09-04T09:04:54.385Z" }, + { url = "https://files.pythonhosted.org/packages/8d/61/de5fb1ca7ad1f9ab7970e340a5b833d735df24689047de6ae71ab9d8d0e7/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4bfa75a048c056a411f9705856abfc872558e33c055d80af6a380e3658766038", size = 2352461, upload-time = "2024-09-04T09:04:56.307Z" }, + { url = "https://files.pythonhosted.org/packages/ba/d2/0edc00a852e369827f7e05fd008275f550353f1f9bcd55db9363d779fc63/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:34ea1de54beef1c104422d210c47c7d2a4999bdecf42c7b5718fbe59a4cac383", size = 2501358, upload-time = "2024-09-04T09:04:57.922Z" }, + { url = "https://files.pythonhosted.org/packages/84/15/adc15a483506aec6986c01fb7f237c3aec4d9ed4ac10b756e98a76835933/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:90da3b5f694b85231cf93586dad5e90e2d71b9428f9aad96952c99055582f520", size = 2314119, upload-time = "2024-09-04T09:04:59.332Z" }, + { url = "https://files.pythonhosted.org/packages/36/08/3a5bb2c53c89660863a5aa1ee236912269f2af8762af04a2e11df851d7b2/kiwisolver-1.4.7-cp312-cp312-win32.whl", hash = "sha256:18e0cca3e008e17fe9b164b55735a325140a5a35faad8de92dd80265cd5eb80b", size = 46367, upload-time = "2024-09-04T09:05:00.804Z" }, + { url = "https://files.pythonhosted.org/packages/19/93/c05f0a6d825c643779fc3c70876bff1ac221f0e31e6f701f0e9578690d70/kiwisolver-1.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:58cb20602b18f86f83a5c87d3ee1c766a79c0d452f8def86d925e6c60fbf7bfb", size = 55884, upload-time = "2024-09-04T09:05:01.924Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f9/3828d8f21b6de4279f0667fb50a9f5215e6fe57d5ec0d61905914f5b6099/kiwisolver-1.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:f5a8b53bdc0b3961f8b6125e198617c40aeed638b387913bf1ce78afb1b0be2a", size = 48528, upload-time = "2024-09-04T09:05:02.983Z" }, + { url = "https://files.pythonhosted.org/packages/11/88/37ea0ea64512997b13d69772db8dcdc3bfca5442cda3a5e4bb943652ee3e/kiwisolver-1.4.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3f9362ecfca44c863569d3d3c033dbe8ba452ff8eed6f6b5806382741a1334bd", size = 122449, upload-time = "2024-09-04T09:05:55.311Z" }, + { url = "https://files.pythonhosted.org/packages/4e/45/5a5c46078362cb3882dcacad687c503089263c017ca1241e0483857791eb/kiwisolver-1.4.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e8df2eb9b2bac43ef8b082e06f750350fbbaf2887534a5be97f6cf07b19d9583", size = 65757, upload-time = "2024-09-04T09:05:56.906Z" }, + { url = "https://files.pythonhosted.org/packages/8a/be/a6ae58978772f685d48dd2e84460937761c53c4bbd84e42b0336473d9775/kiwisolver-1.4.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f32d6edbc638cde7652bd690c3e728b25332acbadd7cad670cc4a02558d9c417", size = 64312, upload-time = "2024-09-04T09:05:58.384Z" }, + { url = "https://files.pythonhosted.org/packages/f4/04/18ef6f452d311e1e1eb180c9bf5589187fa1f042db877e6fe443ef10099c/kiwisolver-1.4.7-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e2e6c39bd7b9372b0be21456caab138e8e69cc0fc1190a9dfa92bd45a1e6e904", size = 1626966, upload-time = "2024-09-04T09:05:59.855Z" }, + { url = "https://files.pythonhosted.org/packages/21/b1/40655f6c3fa11ce740e8a964fa8e4c0479c87d6a7944b95af799c7a55dfe/kiwisolver-1.4.7-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dda56c24d869b1193fcc763f1284b9126550eaf84b88bbc7256e15028f19188a", size = 1607044, upload-time = "2024-09-04T09:06:02.16Z" }, + { url = "https://files.pythonhosted.org/packages/fd/93/af67dbcfb9b3323bbd2c2db1385a7139d8f77630e4a37bb945b57188eb2d/kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79849239c39b5e1fd906556c474d9b0439ea6792b637511f3fe3a41158d89ca8", size = 1391879, upload-time = "2024-09-04T09:06:03.908Z" }, + { url = "https://files.pythonhosted.org/packages/40/6f/d60770ef98e77b365d96061d090c0cd9e23418121c55fff188fa4bdf0b54/kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5e3bc157fed2a4c02ec468de4ecd12a6e22818d4f09cde2c31ee3226ffbefab2", size = 1504751, upload-time = "2024-09-04T09:06:05.58Z" }, + { url = "https://files.pythonhosted.org/packages/fa/3a/5f38667d313e983c432f3fcd86932177519ed8790c724e07d77d1de0188a/kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3da53da805b71e41053dc670f9a820d1157aae77b6b944e08024d17bcd51ef88", size = 1436990, upload-time = "2024-09-04T09:06:08.126Z" }, + { url = "https://files.pythonhosted.org/packages/cb/3b/1520301a47326e6a6043b502647e42892be33b3f051e9791cc8bb43f1a32/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8705f17dfeb43139a692298cb6637ee2e59c0194538153e83e9ee0c75c2eddde", size = 2191122, upload-time = "2024-09-04T09:06:10.345Z" }, + { url = "https://files.pythonhosted.org/packages/cf/c4/eb52da300c166239a2233f1f9c4a1b767dfab98fae27681bfb7ea4873cb6/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:82a5c2f4b87c26bb1a0ef3d16b5c4753434633b83d365cc0ddf2770c93829e3c", size = 2338126, upload-time = "2024-09-04T09:06:12.321Z" }, + { url = "https://files.pythonhosted.org/packages/1a/cb/42b92fd5eadd708dd9107c089e817945500685f3437ce1fd387efebc6d6e/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ce8be0466f4c0d585cdb6c1e2ed07232221df101a4c6f28821d2aa754ca2d9e2", size = 2298313, upload-time = "2024-09-04T09:06:14.562Z" }, + { url = "https://files.pythonhosted.org/packages/4f/eb/be25aa791fe5fc75a8b1e0c965e00f942496bc04635c9aae8035f6b76dcd/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:409afdfe1e2e90e6ee7fc896f3df9a7fec8e793e58bfa0d052c8a82f99c37abb", size = 2437784, upload-time = "2024-09-04T09:06:16.767Z" }, + { url = "https://files.pythonhosted.org/packages/c5/22/30a66be7f3368d76ff95689e1c2e28d382383952964ab15330a15d8bfd03/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5b9c3f4ee0b9a439d2415012bd1b1cc2df59e4d6a9939f4d669241d30b414327", size = 2253988, upload-time = "2024-09-04T09:06:18.705Z" }, + { url = "https://files.pythonhosted.org/packages/35/d3/5f2ecb94b5211c8a04f218a76133cc8d6d153b0f9cd0b45fad79907f0689/kiwisolver-1.4.7-cp39-cp39-win32.whl", hash = "sha256:a79ae34384df2b615eefca647a2873842ac3b596418032bef9a7283675962644", size = 46980, upload-time = "2024-09-04T09:06:20.106Z" }, + { url = "https://files.pythonhosted.org/packages/ef/17/cd10d020578764ea91740204edc6b3236ed8106228a46f568d716b11feb2/kiwisolver-1.4.7-cp39-cp39-win_amd64.whl", hash = "sha256:cf0438b42121a66a3a667de17e779330fc0f20b0d97d59d2f2121e182b0505e4", size = 55847, upload-time = "2024-09-04T09:06:21.407Z" }, + { url = "https://files.pythonhosted.org/packages/91/84/32232502020bd78d1d12be7afde15811c64a95ed1f606c10456db4e4c3ac/kiwisolver-1.4.7-cp39-cp39-win_arm64.whl", hash = "sha256:764202cc7e70f767dab49e8df52c7455e8de0df5d858fa801a11aa0d882ccf3f", size = 48494, upload-time = "2024-09-04T09:06:22.648Z" }, + { url = "https://files.pythonhosted.org/packages/ac/59/741b79775d67ab67ced9bb38552da688c0305c16e7ee24bba7a2be253fb7/kiwisolver-1.4.7-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:94252291e3fe68001b1dd747b4c0b3be12582839b95ad4d1b641924d68fd4643", size = 59491, upload-time = "2024-09-04T09:06:24.188Z" }, + { url = "https://files.pythonhosted.org/packages/58/cc/fb239294c29a5656e99e3527f7369b174dd9cc7c3ef2dea7cb3c54a8737b/kiwisolver-1.4.7-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5b7dfa3b546da08a9f622bb6becdb14b3e24aaa30adba66749d38f3cc7ea9706", size = 57648, upload-time = "2024-09-04T09:06:25.559Z" }, + { url = "https://files.pythonhosted.org/packages/3b/ef/2f009ac1f7aab9f81efb2d837301d255279d618d27b6015780115ac64bdd/kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd3de6481f4ed8b734da5df134cd5a6a64fe32124fe83dde1e5b5f29fe30b1e6", size = 84257, upload-time = "2024-09-04T09:06:27.038Z" }, + { url = "https://files.pythonhosted.org/packages/81/e1/c64f50987f85b68b1c52b464bb5bf73e71570c0f7782d626d1eb283ad620/kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a91b5f9f1205845d488c928e8570dcb62b893372f63b8b6e98b863ebd2368ff2", size = 80906, upload-time = "2024-09-04T09:06:28.48Z" }, + { url = "https://files.pythonhosted.org/packages/fd/71/1687c5c0a0be2cee39a5c9c389e546f9c6e215e46b691d00d9f646892083/kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40fa14dbd66b8b8f470d5fc79c089a66185619d31645f9b0773b88b19f7223c4", size = 79951, upload-time = "2024-09-04T09:06:29.966Z" }, + { url = "https://files.pythonhosted.org/packages/ea/8b/d7497df4a1cae9367adf21665dd1f896c2a7aeb8769ad77b662c5e2bcce7/kiwisolver-1.4.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:eb542fe7933aa09d8d8f9d9097ef37532a7df6497819d16efe4359890a2f417a", size = 55715, upload-time = "2024-09-04T09:06:31.489Z" }, + { url = "https://files.pythonhosted.org/packages/d5/df/ce37d9b26f07ab90880923c94d12a6ff4d27447096b4c849bfc4339ccfdf/kiwisolver-1.4.7-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8b01aac285f91ca889c800042c35ad3b239e704b150cfd3382adfc9dcc780e39", size = 58666, upload-time = "2024-09-04T09:06:43.756Z" }, + { url = "https://files.pythonhosted.org/packages/b0/d3/e4b04f43bc629ac8e186b77b2b1a251cdfa5b7610fa189dc0db622672ce6/kiwisolver-1.4.7-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:48be928f59a1f5c8207154f935334d374e79f2b5d212826307d072595ad76a2e", size = 57088, upload-time = "2024-09-04T09:06:45.406Z" }, + { url = "https://files.pythonhosted.org/packages/30/1c/752df58e2d339e670a535514d2db4fe8c842ce459776b8080fbe08ebb98e/kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f37cfe618a117e50d8c240555331160d73d0411422b59b5ee217843d7b693608", size = 84321, upload-time = "2024-09-04T09:06:47.557Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f8/fe6484e847bc6e238ec9f9828089fb2c0bb53f2f5f3a79351fde5b565e4f/kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:599b5c873c63a1f6ed7eead644a8a380cfbdf5db91dcb6f85707aaab213b1674", size = 80776, upload-time = "2024-09-04T09:06:49.235Z" }, + { url = "https://files.pythonhosted.org/packages/9b/57/d7163c0379f250ef763aba85330a19feefb5ce6cb541ade853aaba881524/kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:801fa7802e5cfabe3ab0c81a34c323a319b097dfb5004be950482d882f3d7225", size = 79984, upload-time = "2024-09-04T09:06:51.336Z" }, + { url = "https://files.pythonhosted.org/packages/8c/95/4a103776c265d13b3d2cd24fb0494d4e04ea435a8ef97e1b2c026d43250b/kiwisolver-1.4.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:0c6c43471bc764fad4bc99c5c2d6d16a676b1abf844ca7c8702bdae92df01ee0", size = 55811, upload-time = "2024-09-04T09:06:53.078Z" }, ] [[package]] @@ -710,59 +741,59 @@ resolution-markers = [ "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", ] -sdist = { url = "https://files.pythonhosted.org/packages/82/59/7c91426a8ac292e1cdd53a63b6d9439abd573c875c3f92c146767dd33faf/kiwisolver-1.4.8.tar.gz", hash = "sha256:23d5f023bdc8c7e54eb65f03ca5d5bb25b601eac4d7f1a042888a1f45237987e", size = 97538 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/47/5f/4d8e9e852d98ecd26cdf8eaf7ed8bc33174033bba5e07001b289f07308fd/kiwisolver-1.4.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88c6f252f6816a73b1f8c904f7bbe02fd67c09a69f7cb8a0eecdbf5ce78e63db", size = 124623 }, - { url = "https://files.pythonhosted.org/packages/1d/70/7f5af2a18a76fe92ea14675f8bd88ce53ee79e37900fa5f1a1d8e0b42998/kiwisolver-1.4.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c72941acb7b67138f35b879bbe85be0f6c6a70cab78fe3ef6db9c024d9223e5b", size = 66720 }, - { url = "https://files.pythonhosted.org/packages/c6/13/e15f804a142353aefd089fadc8f1d985561a15358c97aca27b0979cb0785/kiwisolver-1.4.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce2cf1e5688edcb727fdf7cd1bbd0b6416758996826a8be1d958f91880d0809d", size = 65413 }, - { url = "https://files.pythonhosted.org/packages/ce/6d/67d36c4d2054e83fb875c6b59d0809d5c530de8148846b1370475eeeece9/kiwisolver-1.4.8-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c8bf637892dc6e6aad2bc6d4d69d08764166e5e3f69d469e55427b6ac001b19d", size = 1650826 }, - { url = "https://files.pythonhosted.org/packages/de/c6/7b9bb8044e150d4d1558423a1568e4f227193662a02231064e3824f37e0a/kiwisolver-1.4.8-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:034d2c891f76bd3edbdb3ea11140d8510dca675443da7304205a2eaa45d8334c", size = 1628231 }, - { url = "https://files.pythonhosted.org/packages/b6/38/ad10d437563063eaaedbe2c3540a71101fc7fb07a7e71f855e93ea4de605/kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d47b28d1dfe0793d5e96bce90835e17edf9a499b53969b03c6c47ea5985844c3", size = 1408938 }, - { url = "https://files.pythonhosted.org/packages/52/ce/c0106b3bd7f9e665c5f5bc1e07cc95b5dabd4e08e3dad42dbe2faad467e7/kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb158fe28ca0c29f2260cca8c43005329ad58452c36f0edf298204de32a9a3ed", size = 1422799 }, - { url = "https://files.pythonhosted.org/packages/d0/87/efb704b1d75dc9758087ba374c0f23d3254505edaedd09cf9d247f7878b9/kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5536185fce131780ebd809f8e623bf4030ce1b161353166c49a3c74c287897f", size = 1354362 }, - { url = "https://files.pythonhosted.org/packages/eb/b3/fd760dc214ec9a8f208b99e42e8f0130ff4b384eca8b29dd0efc62052176/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:369b75d40abedc1da2c1f4de13f3482cb99e3237b38726710f4a793432b1c5ff", size = 2222695 }, - { url = "https://files.pythonhosted.org/packages/a2/09/a27fb36cca3fc01700687cc45dae7a6a5f8eeb5f657b9f710f788748e10d/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:641f2ddf9358c80faa22e22eb4c9f54bd3f0e442e038728f500e3b978d00aa7d", size = 2370802 }, - { url = "https://files.pythonhosted.org/packages/3d/c3/ba0a0346db35fe4dc1f2f2cf8b99362fbb922d7562e5f911f7ce7a7b60fa/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d561d2d8883e0819445cfe58d7ddd673e4015c3c57261d7bdcd3710d0d14005c", size = 2334646 }, - { url = "https://files.pythonhosted.org/packages/41/52/942cf69e562f5ed253ac67d5c92a693745f0bed3c81f49fc0cbebe4d6b00/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:1732e065704b47c9afca7ffa272f845300a4eb959276bf6970dc07265e73b605", size = 2467260 }, - { url = "https://files.pythonhosted.org/packages/32/26/2d9668f30d8a494b0411d4d7d4ea1345ba12deb6a75274d58dd6ea01e951/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bcb1ebc3547619c3b58a39e2448af089ea2ef44b37988caf432447374941574e", size = 2288633 }, - { url = "https://files.pythonhosted.org/packages/98/99/0dd05071654aa44fe5d5e350729961e7bb535372935a45ac89a8924316e6/kiwisolver-1.4.8-cp310-cp310-win_amd64.whl", hash = "sha256:89c107041f7b27844179ea9c85d6da275aa55ecf28413e87624d033cf1f6b751", size = 71885 }, - { url = "https://files.pythonhosted.org/packages/6c/fc/822e532262a97442989335394d441cd1d0448c2e46d26d3e04efca84df22/kiwisolver-1.4.8-cp310-cp310-win_arm64.whl", hash = "sha256:b5773efa2be9eb9fcf5415ea3ab70fc785d598729fd6057bea38d539ead28271", size = 65175 }, - { url = "https://files.pythonhosted.org/packages/da/ed/c913ee28936c371418cb167b128066ffb20bbf37771eecc2c97edf8a6e4c/kiwisolver-1.4.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a4d3601908c560bdf880f07d94f31d734afd1bb71e96585cace0e38ef44c6d84", size = 124635 }, - { url = "https://files.pythonhosted.org/packages/4c/45/4a7f896f7467aaf5f56ef093d1f329346f3b594e77c6a3c327b2d415f521/kiwisolver-1.4.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:856b269c4d28a5c0d5e6c1955ec36ebfd1651ac00e1ce0afa3e28da95293b561", size = 66717 }, - { url = "https://files.pythonhosted.org/packages/5f/b4/c12b3ac0852a3a68f94598d4c8d569f55361beef6159dce4e7b624160da2/kiwisolver-1.4.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c2b9a96e0f326205af81a15718a9073328df1173a2619a68553decb7097fd5d7", size = 65413 }, - { url = "https://files.pythonhosted.org/packages/a9/98/1df4089b1ed23d83d410adfdc5947245c753bddfbe06541c4aae330e9e70/kiwisolver-1.4.8-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5020c83e8553f770cb3b5fc13faac40f17e0b205bd237aebd21d53d733adb03", size = 1343994 }, - { url = "https://files.pythonhosted.org/packages/8d/bf/b4b169b050c8421a7c53ea1ea74e4ef9c335ee9013216c558a047f162d20/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dace81d28c787956bfbfbbfd72fdcef014f37d9b48830829e488fdb32b49d954", size = 1434804 }, - { url = "https://files.pythonhosted.org/packages/66/5a/e13bd341fbcf73325ea60fdc8af752addf75c5079867af2e04cc41f34434/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:11e1022b524bd48ae56c9b4f9296bce77e15a2e42a502cceba602f804b32bb79", size = 1450690 }, - { url = "https://files.pythonhosted.org/packages/9b/4f/5955dcb376ba4a830384cc6fab7d7547bd6759fe75a09564910e9e3bb8ea/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b9b4d2892fefc886f30301cdd80debd8bb01ecdf165a449eb6e78f79f0fabd6", size = 1376839 }, - { url = "https://files.pythonhosted.org/packages/3a/97/5edbed69a9d0caa2e4aa616ae7df8127e10f6586940aa683a496c2c280b9/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a96c0e790ee875d65e340ab383700e2b4891677b7fcd30a699146f9384a2bb0", size = 1435109 }, - { url = "https://files.pythonhosted.org/packages/13/fc/e756382cb64e556af6c1809a1bbb22c141bbc2445049f2da06b420fe52bf/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:23454ff084b07ac54ca8be535f4174170c1094a4cff78fbae4f73a4bcc0d4dab", size = 2245269 }, - { url = "https://files.pythonhosted.org/packages/76/15/e59e45829d7f41c776d138245cabae6515cb4eb44b418f6d4109c478b481/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:87b287251ad6488e95b4f0b4a79a6d04d3ea35fde6340eb38fbd1ca9cd35bbbc", size = 2393468 }, - { url = "https://files.pythonhosted.org/packages/e9/39/483558c2a913ab8384d6e4b66a932406f87c95a6080112433da5ed668559/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b21dbe165081142b1232a240fc6383fd32cdd877ca6cc89eab93e5f5883e1c25", size = 2355394 }, - { url = "https://files.pythonhosted.org/packages/01/aa/efad1fbca6570a161d29224f14b082960c7e08268a133fe5dc0f6906820e/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:768cade2c2df13db52475bd28d3a3fac8c9eff04b0e9e2fda0f3760f20b3f7fc", size = 2490901 }, - { url = "https://files.pythonhosted.org/packages/c9/4f/15988966ba46bcd5ab9d0c8296914436720dd67fca689ae1a75b4ec1c72f/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d47cfb2650f0e103d4bf68b0b5804c68da97272c84bb12850d877a95c056bd67", size = 2312306 }, - { url = "https://files.pythonhosted.org/packages/2d/27/bdf1c769c83f74d98cbc34483a972f221440703054894a37d174fba8aa68/kiwisolver-1.4.8-cp311-cp311-win_amd64.whl", hash = "sha256:ed33ca2002a779a2e20eeb06aea7721b6e47f2d4b8a8ece979d8ba9e2a167e34", size = 71966 }, - { url = "https://files.pythonhosted.org/packages/4a/c9/9642ea855604aeb2968a8e145fc662edf61db7632ad2e4fb92424be6b6c0/kiwisolver-1.4.8-cp311-cp311-win_arm64.whl", hash = "sha256:16523b40aab60426ffdebe33ac374457cf62863e330a90a0383639ce14bf44b2", size = 65311 }, - { url = "https://files.pythonhosted.org/packages/fc/aa/cea685c4ab647f349c3bc92d2daf7ae34c8e8cf405a6dcd3a497f58a2ac3/kiwisolver-1.4.8-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d6af5e8815fd02997cb6ad9bbed0ee1e60014438ee1a5c2444c96f87b8843502", size = 124152 }, - { url = "https://files.pythonhosted.org/packages/c5/0b/8db6d2e2452d60d5ebc4ce4b204feeb16176a851fd42462f66ade6808084/kiwisolver-1.4.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bade438f86e21d91e0cf5dd7c0ed00cda0f77c8c1616bd83f9fc157fa6760d31", size = 66555 }, - { url = "https://files.pythonhosted.org/packages/60/26/d6a0db6785dd35d3ba5bf2b2df0aedc5af089962c6eb2cbf67a15b81369e/kiwisolver-1.4.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b83dc6769ddbc57613280118fb4ce3cd08899cc3369f7d0e0fab518a7cf37fdb", size = 65067 }, - { url = "https://files.pythonhosted.org/packages/c9/ed/1d97f7e3561e09757a196231edccc1bcf59d55ddccefa2afc9c615abd8e0/kiwisolver-1.4.8-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:111793b232842991be367ed828076b03d96202c19221b5ebab421ce8bcad016f", size = 1378443 }, - { url = "https://files.pythonhosted.org/packages/29/61/39d30b99954e6b46f760e6289c12fede2ab96a254c443639052d1b573fbc/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:257af1622860e51b1a9d0ce387bf5c2c4f36a90594cb9514f55b074bcc787cfc", size = 1472728 }, - { url = "https://files.pythonhosted.org/packages/0c/3e/804163b932f7603ef256e4a715e5843a9600802bb23a68b4e08c8c0ff61d/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:69b5637c3f316cab1ec1c9a12b8c5f4750a4c4b71af9157645bf32830e39c03a", size = 1478388 }, - { url = "https://files.pythonhosted.org/packages/8a/9e/60eaa75169a154700be74f875a4d9961b11ba048bef315fbe89cb6999056/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:782bb86f245ec18009890e7cb8d13a5ef54dcf2ebe18ed65f795e635a96a1c6a", size = 1413849 }, - { url = "https://files.pythonhosted.org/packages/bc/b3/9458adb9472e61a998c8c4d95cfdfec91c73c53a375b30b1428310f923e4/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc978a80a0db3a66d25767b03688f1147a69e6237175c0f4ffffaaedf744055a", size = 1475533 }, - { url = "https://files.pythonhosted.org/packages/e4/7a/0a42d9571e35798de80aef4bb43a9b672aa7f8e58643d7bd1950398ffb0a/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:36dbbfd34838500a31f52c9786990d00150860e46cd5041386f217101350f0d3", size = 2268898 }, - { url = "https://files.pythonhosted.org/packages/d9/07/1255dc8d80271400126ed8db35a1795b1a2c098ac3a72645075d06fe5c5d/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:eaa973f1e05131de5ff3569bbba7f5fd07ea0595d3870ed4a526d486fe57fa1b", size = 2425605 }, - { url = "https://files.pythonhosted.org/packages/84/df/5a3b4cf13780ef6f6942df67b138b03b7e79e9f1f08f57c49957d5867f6e/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a66f60f8d0c87ab7f59b6fb80e642ebb29fec354a4dfad687ca4092ae69d04f4", size = 2375801 }, - { url = "https://files.pythonhosted.org/packages/8f/10/2348d068e8b0f635c8c86892788dac7a6b5c0cb12356620ab575775aad89/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858416b7fb777a53f0c59ca08190ce24e9abbd3cffa18886a5781b8e3e26f65d", size = 2520077 }, - { url = "https://files.pythonhosted.org/packages/32/d8/014b89fee5d4dce157d814303b0fce4d31385a2af4c41fed194b173b81ac/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:085940635c62697391baafaaeabdf3dd7a6c3643577dde337f4d66eba021b2b8", size = 2338410 }, - { url = "https://files.pythonhosted.org/packages/bd/72/dfff0cc97f2a0776e1c9eb5bef1ddfd45f46246c6533b0191887a427bca5/kiwisolver-1.4.8-cp312-cp312-win_amd64.whl", hash = "sha256:01c3d31902c7db5fb6182832713d3b4122ad9317c2c5877d0539227d96bb2e50", size = 71853 }, - { url = "https://files.pythonhosted.org/packages/dc/85/220d13d914485c0948a00f0b9eb419efaf6da81b7d72e88ce2391f7aed8d/kiwisolver-1.4.8-cp312-cp312-win_arm64.whl", hash = "sha256:a3c44cb68861de93f0c4a8175fbaa691f0aa22550c331fefef02b618a9dcb476", size = 65424 }, - { url = "https://files.pythonhosted.org/packages/1f/f9/ae81c47a43e33b93b0a9819cac6723257f5da2a5a60daf46aa5c7226ea85/kiwisolver-1.4.8-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e7a019419b7b510f0f7c9dceff8c5eae2392037eae483a7f9162625233802b0a", size = 60403 }, - { url = "https://files.pythonhosted.org/packages/58/ca/f92b5cb6f4ce0c1ebfcfe3e2e42b96917e16f7090e45b21102941924f18f/kiwisolver-1.4.8-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:286b18e86682fd2217a48fc6be6b0f20c1d0ed10958d8dc53453ad58d7be0bf8", size = 58657 }, - { url = "https://files.pythonhosted.org/packages/80/28/ae0240f732f0484d3a4dc885d055653c47144bdf59b670aae0ec3c65a7c8/kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4191ee8dfd0be1c3666ccbac178c5a05d5f8d689bbe3fc92f3c4abec817f8fe0", size = 84948 }, - { url = "https://files.pythonhosted.org/packages/5d/eb/78d50346c51db22c7203c1611f9b513075f35c4e0e4877c5dde378d66043/kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7cd2785b9391f2873ad46088ed7599a6a71e762e1ea33e87514b1a441ed1da1c", size = 81186 }, - { url = "https://files.pythonhosted.org/packages/43/f8/7259f18c77adca88d5f64f9a522792e178b2691f3748817a8750c2d216ef/kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c07b29089b7ba090b6f1a669f1411f27221c3662b3a1b7010e67b59bb5a6f10b", size = 80279 }, - { url = "https://files.pythonhosted.org/packages/3a/1d/50ad811d1c5dae091e4cf046beba925bcae0a610e79ae4c538f996f63ed5/kiwisolver-1.4.8-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:65ea09a5a3faadd59c2ce96dc7bf0f364986a315949dc6374f04396b0d60e09b", size = 71762 }, +sdist = { url = "https://files.pythonhosted.org/packages/82/59/7c91426a8ac292e1cdd53a63b6d9439abd573c875c3f92c146767dd33faf/kiwisolver-1.4.8.tar.gz", hash = "sha256:23d5f023bdc8c7e54eb65f03ca5d5bb25b601eac4d7f1a042888a1f45237987e", size = 97538, upload-time = "2024-12-24T18:30:51.519Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/5f/4d8e9e852d98ecd26cdf8eaf7ed8bc33174033bba5e07001b289f07308fd/kiwisolver-1.4.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88c6f252f6816a73b1f8c904f7bbe02fd67c09a69f7cb8a0eecdbf5ce78e63db", size = 124623, upload-time = "2024-12-24T18:28:17.687Z" }, + { url = "https://files.pythonhosted.org/packages/1d/70/7f5af2a18a76fe92ea14675f8bd88ce53ee79e37900fa5f1a1d8e0b42998/kiwisolver-1.4.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c72941acb7b67138f35b879bbe85be0f6c6a70cab78fe3ef6db9c024d9223e5b", size = 66720, upload-time = "2024-12-24T18:28:19.158Z" }, + { url = "https://files.pythonhosted.org/packages/c6/13/e15f804a142353aefd089fadc8f1d985561a15358c97aca27b0979cb0785/kiwisolver-1.4.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce2cf1e5688edcb727fdf7cd1bbd0b6416758996826a8be1d958f91880d0809d", size = 65413, upload-time = "2024-12-24T18:28:20.064Z" }, + { url = "https://files.pythonhosted.org/packages/ce/6d/67d36c4d2054e83fb875c6b59d0809d5c530de8148846b1370475eeeece9/kiwisolver-1.4.8-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c8bf637892dc6e6aad2bc6d4d69d08764166e5e3f69d469e55427b6ac001b19d", size = 1650826, upload-time = "2024-12-24T18:28:21.203Z" }, + { url = "https://files.pythonhosted.org/packages/de/c6/7b9bb8044e150d4d1558423a1568e4f227193662a02231064e3824f37e0a/kiwisolver-1.4.8-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:034d2c891f76bd3edbdb3ea11140d8510dca675443da7304205a2eaa45d8334c", size = 1628231, upload-time = "2024-12-24T18:28:23.851Z" }, + { url = "https://files.pythonhosted.org/packages/b6/38/ad10d437563063eaaedbe2c3540a71101fc7fb07a7e71f855e93ea4de605/kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d47b28d1dfe0793d5e96bce90835e17edf9a499b53969b03c6c47ea5985844c3", size = 1408938, upload-time = "2024-12-24T18:28:26.687Z" }, + { url = "https://files.pythonhosted.org/packages/52/ce/c0106b3bd7f9e665c5f5bc1e07cc95b5dabd4e08e3dad42dbe2faad467e7/kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb158fe28ca0c29f2260cca8c43005329ad58452c36f0edf298204de32a9a3ed", size = 1422799, upload-time = "2024-12-24T18:28:30.538Z" }, + { url = "https://files.pythonhosted.org/packages/d0/87/efb704b1d75dc9758087ba374c0f23d3254505edaedd09cf9d247f7878b9/kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5536185fce131780ebd809f8e623bf4030ce1b161353166c49a3c74c287897f", size = 1354362, upload-time = "2024-12-24T18:28:32.943Z" }, + { url = "https://files.pythonhosted.org/packages/eb/b3/fd760dc214ec9a8f208b99e42e8f0130ff4b384eca8b29dd0efc62052176/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:369b75d40abedc1da2c1f4de13f3482cb99e3237b38726710f4a793432b1c5ff", size = 2222695, upload-time = "2024-12-24T18:28:35.641Z" }, + { url = "https://files.pythonhosted.org/packages/a2/09/a27fb36cca3fc01700687cc45dae7a6a5f8eeb5f657b9f710f788748e10d/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:641f2ddf9358c80faa22e22eb4c9f54bd3f0e442e038728f500e3b978d00aa7d", size = 2370802, upload-time = "2024-12-24T18:28:38.357Z" }, + { url = "https://files.pythonhosted.org/packages/3d/c3/ba0a0346db35fe4dc1f2f2cf8b99362fbb922d7562e5f911f7ce7a7b60fa/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d561d2d8883e0819445cfe58d7ddd673e4015c3c57261d7bdcd3710d0d14005c", size = 2334646, upload-time = "2024-12-24T18:28:40.941Z" }, + { url = "https://files.pythonhosted.org/packages/41/52/942cf69e562f5ed253ac67d5c92a693745f0bed3c81f49fc0cbebe4d6b00/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:1732e065704b47c9afca7ffa272f845300a4eb959276bf6970dc07265e73b605", size = 2467260, upload-time = "2024-12-24T18:28:42.273Z" }, + { url = "https://files.pythonhosted.org/packages/32/26/2d9668f30d8a494b0411d4d7d4ea1345ba12deb6a75274d58dd6ea01e951/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bcb1ebc3547619c3b58a39e2448af089ea2ef44b37988caf432447374941574e", size = 2288633, upload-time = "2024-12-24T18:28:44.87Z" }, + { url = "https://files.pythonhosted.org/packages/98/99/0dd05071654aa44fe5d5e350729961e7bb535372935a45ac89a8924316e6/kiwisolver-1.4.8-cp310-cp310-win_amd64.whl", hash = "sha256:89c107041f7b27844179ea9c85d6da275aa55ecf28413e87624d033cf1f6b751", size = 71885, upload-time = "2024-12-24T18:28:47.346Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fc/822e532262a97442989335394d441cd1d0448c2e46d26d3e04efca84df22/kiwisolver-1.4.8-cp310-cp310-win_arm64.whl", hash = "sha256:b5773efa2be9eb9fcf5415ea3ab70fc785d598729fd6057bea38d539ead28271", size = 65175, upload-time = "2024-12-24T18:28:49.651Z" }, + { url = "https://files.pythonhosted.org/packages/da/ed/c913ee28936c371418cb167b128066ffb20bbf37771eecc2c97edf8a6e4c/kiwisolver-1.4.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a4d3601908c560bdf880f07d94f31d734afd1bb71e96585cace0e38ef44c6d84", size = 124635, upload-time = "2024-12-24T18:28:51.826Z" }, + { url = "https://files.pythonhosted.org/packages/4c/45/4a7f896f7467aaf5f56ef093d1f329346f3b594e77c6a3c327b2d415f521/kiwisolver-1.4.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:856b269c4d28a5c0d5e6c1955ec36ebfd1651ac00e1ce0afa3e28da95293b561", size = 66717, upload-time = "2024-12-24T18:28:54.256Z" }, + { url = "https://files.pythonhosted.org/packages/5f/b4/c12b3ac0852a3a68f94598d4c8d569f55361beef6159dce4e7b624160da2/kiwisolver-1.4.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c2b9a96e0f326205af81a15718a9073328df1173a2619a68553decb7097fd5d7", size = 65413, upload-time = "2024-12-24T18:28:55.184Z" }, + { url = "https://files.pythonhosted.org/packages/a9/98/1df4089b1ed23d83d410adfdc5947245c753bddfbe06541c4aae330e9e70/kiwisolver-1.4.8-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5020c83e8553f770cb3b5fc13faac40f17e0b205bd237aebd21d53d733adb03", size = 1343994, upload-time = "2024-12-24T18:28:57.493Z" }, + { url = "https://files.pythonhosted.org/packages/8d/bf/b4b169b050c8421a7c53ea1ea74e4ef9c335ee9013216c558a047f162d20/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dace81d28c787956bfbfbbfd72fdcef014f37d9b48830829e488fdb32b49d954", size = 1434804, upload-time = "2024-12-24T18:29:00.077Z" }, + { url = "https://files.pythonhosted.org/packages/66/5a/e13bd341fbcf73325ea60fdc8af752addf75c5079867af2e04cc41f34434/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:11e1022b524bd48ae56c9b4f9296bce77e15a2e42a502cceba602f804b32bb79", size = 1450690, upload-time = "2024-12-24T18:29:01.401Z" }, + { url = "https://files.pythonhosted.org/packages/9b/4f/5955dcb376ba4a830384cc6fab7d7547bd6759fe75a09564910e9e3bb8ea/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b9b4d2892fefc886f30301cdd80debd8bb01ecdf165a449eb6e78f79f0fabd6", size = 1376839, upload-time = "2024-12-24T18:29:02.685Z" }, + { url = "https://files.pythonhosted.org/packages/3a/97/5edbed69a9d0caa2e4aa616ae7df8127e10f6586940aa683a496c2c280b9/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a96c0e790ee875d65e340ab383700e2b4891677b7fcd30a699146f9384a2bb0", size = 1435109, upload-time = "2024-12-24T18:29:04.113Z" }, + { url = "https://files.pythonhosted.org/packages/13/fc/e756382cb64e556af6c1809a1bbb22c141bbc2445049f2da06b420fe52bf/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:23454ff084b07ac54ca8be535f4174170c1094a4cff78fbae4f73a4bcc0d4dab", size = 2245269, upload-time = "2024-12-24T18:29:05.488Z" }, + { url = "https://files.pythonhosted.org/packages/76/15/e59e45829d7f41c776d138245cabae6515cb4eb44b418f6d4109c478b481/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:87b287251ad6488e95b4f0b4a79a6d04d3ea35fde6340eb38fbd1ca9cd35bbbc", size = 2393468, upload-time = "2024-12-24T18:29:06.79Z" }, + { url = "https://files.pythonhosted.org/packages/e9/39/483558c2a913ab8384d6e4b66a932406f87c95a6080112433da5ed668559/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b21dbe165081142b1232a240fc6383fd32cdd877ca6cc89eab93e5f5883e1c25", size = 2355394, upload-time = "2024-12-24T18:29:08.24Z" }, + { url = "https://files.pythonhosted.org/packages/01/aa/efad1fbca6570a161d29224f14b082960c7e08268a133fe5dc0f6906820e/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:768cade2c2df13db52475bd28d3a3fac8c9eff04b0e9e2fda0f3760f20b3f7fc", size = 2490901, upload-time = "2024-12-24T18:29:09.653Z" }, + { url = "https://files.pythonhosted.org/packages/c9/4f/15988966ba46bcd5ab9d0c8296914436720dd67fca689ae1a75b4ec1c72f/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d47cfb2650f0e103d4bf68b0b5804c68da97272c84bb12850d877a95c056bd67", size = 2312306, upload-time = "2024-12-24T18:29:12.644Z" }, + { url = "https://files.pythonhosted.org/packages/2d/27/bdf1c769c83f74d98cbc34483a972f221440703054894a37d174fba8aa68/kiwisolver-1.4.8-cp311-cp311-win_amd64.whl", hash = "sha256:ed33ca2002a779a2e20eeb06aea7721b6e47f2d4b8a8ece979d8ba9e2a167e34", size = 71966, upload-time = "2024-12-24T18:29:14.089Z" }, + { url = "https://files.pythonhosted.org/packages/4a/c9/9642ea855604aeb2968a8e145fc662edf61db7632ad2e4fb92424be6b6c0/kiwisolver-1.4.8-cp311-cp311-win_arm64.whl", hash = "sha256:16523b40aab60426ffdebe33ac374457cf62863e330a90a0383639ce14bf44b2", size = 65311, upload-time = "2024-12-24T18:29:15.892Z" }, + { url = "https://files.pythonhosted.org/packages/fc/aa/cea685c4ab647f349c3bc92d2daf7ae34c8e8cf405a6dcd3a497f58a2ac3/kiwisolver-1.4.8-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d6af5e8815fd02997cb6ad9bbed0ee1e60014438ee1a5c2444c96f87b8843502", size = 124152, upload-time = "2024-12-24T18:29:16.85Z" }, + { url = "https://files.pythonhosted.org/packages/c5/0b/8db6d2e2452d60d5ebc4ce4b204feeb16176a851fd42462f66ade6808084/kiwisolver-1.4.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bade438f86e21d91e0cf5dd7c0ed00cda0f77c8c1616bd83f9fc157fa6760d31", size = 66555, upload-time = "2024-12-24T18:29:19.146Z" }, + { url = "https://files.pythonhosted.org/packages/60/26/d6a0db6785dd35d3ba5bf2b2df0aedc5af089962c6eb2cbf67a15b81369e/kiwisolver-1.4.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b83dc6769ddbc57613280118fb4ce3cd08899cc3369f7d0e0fab518a7cf37fdb", size = 65067, upload-time = "2024-12-24T18:29:20.096Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ed/1d97f7e3561e09757a196231edccc1bcf59d55ddccefa2afc9c615abd8e0/kiwisolver-1.4.8-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:111793b232842991be367ed828076b03d96202c19221b5ebab421ce8bcad016f", size = 1378443, upload-time = "2024-12-24T18:29:22.843Z" }, + { url = "https://files.pythonhosted.org/packages/29/61/39d30b99954e6b46f760e6289c12fede2ab96a254c443639052d1b573fbc/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:257af1622860e51b1a9d0ce387bf5c2c4f36a90594cb9514f55b074bcc787cfc", size = 1472728, upload-time = "2024-12-24T18:29:24.463Z" }, + { url = "https://files.pythonhosted.org/packages/0c/3e/804163b932f7603ef256e4a715e5843a9600802bb23a68b4e08c8c0ff61d/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:69b5637c3f316cab1ec1c9a12b8c5f4750a4c4b71af9157645bf32830e39c03a", size = 1478388, upload-time = "2024-12-24T18:29:25.776Z" }, + { url = "https://files.pythonhosted.org/packages/8a/9e/60eaa75169a154700be74f875a4d9961b11ba048bef315fbe89cb6999056/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:782bb86f245ec18009890e7cb8d13a5ef54dcf2ebe18ed65f795e635a96a1c6a", size = 1413849, upload-time = "2024-12-24T18:29:27.202Z" }, + { url = "https://files.pythonhosted.org/packages/bc/b3/9458adb9472e61a998c8c4d95cfdfec91c73c53a375b30b1428310f923e4/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc978a80a0db3a66d25767b03688f1147a69e6237175c0f4ffffaaedf744055a", size = 1475533, upload-time = "2024-12-24T18:29:28.638Z" }, + { url = "https://files.pythonhosted.org/packages/e4/7a/0a42d9571e35798de80aef4bb43a9b672aa7f8e58643d7bd1950398ffb0a/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:36dbbfd34838500a31f52c9786990d00150860e46cd5041386f217101350f0d3", size = 2268898, upload-time = "2024-12-24T18:29:30.368Z" }, + { url = "https://files.pythonhosted.org/packages/d9/07/1255dc8d80271400126ed8db35a1795b1a2c098ac3a72645075d06fe5c5d/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:eaa973f1e05131de5ff3569bbba7f5fd07ea0595d3870ed4a526d486fe57fa1b", size = 2425605, upload-time = "2024-12-24T18:29:33.151Z" }, + { url = "https://files.pythonhosted.org/packages/84/df/5a3b4cf13780ef6f6942df67b138b03b7e79e9f1f08f57c49957d5867f6e/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a66f60f8d0c87ab7f59b6fb80e642ebb29fec354a4dfad687ca4092ae69d04f4", size = 2375801, upload-time = "2024-12-24T18:29:34.584Z" }, + { url = "https://files.pythonhosted.org/packages/8f/10/2348d068e8b0f635c8c86892788dac7a6b5c0cb12356620ab575775aad89/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858416b7fb777a53f0c59ca08190ce24e9abbd3cffa18886a5781b8e3e26f65d", size = 2520077, upload-time = "2024-12-24T18:29:36.138Z" }, + { url = "https://files.pythonhosted.org/packages/32/d8/014b89fee5d4dce157d814303b0fce4d31385a2af4c41fed194b173b81ac/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:085940635c62697391baafaaeabdf3dd7a6c3643577dde337f4d66eba021b2b8", size = 2338410, upload-time = "2024-12-24T18:29:39.991Z" }, + { url = "https://files.pythonhosted.org/packages/bd/72/dfff0cc97f2a0776e1c9eb5bef1ddfd45f46246c6533b0191887a427bca5/kiwisolver-1.4.8-cp312-cp312-win_amd64.whl", hash = "sha256:01c3d31902c7db5fb6182832713d3b4122ad9317c2c5877d0539227d96bb2e50", size = 71853, upload-time = "2024-12-24T18:29:42.006Z" }, + { url = "https://files.pythonhosted.org/packages/dc/85/220d13d914485c0948a00f0b9eb419efaf6da81b7d72e88ce2391f7aed8d/kiwisolver-1.4.8-cp312-cp312-win_arm64.whl", hash = "sha256:a3c44cb68861de93f0c4a8175fbaa691f0aa22550c331fefef02b618a9dcb476", size = 65424, upload-time = "2024-12-24T18:29:44.38Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f9/ae81c47a43e33b93b0a9819cac6723257f5da2a5a60daf46aa5c7226ea85/kiwisolver-1.4.8-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e7a019419b7b510f0f7c9dceff8c5eae2392037eae483a7f9162625233802b0a", size = 60403, upload-time = "2024-12-24T18:30:41.372Z" }, + { url = "https://files.pythonhosted.org/packages/58/ca/f92b5cb6f4ce0c1ebfcfe3e2e42b96917e16f7090e45b21102941924f18f/kiwisolver-1.4.8-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:286b18e86682fd2217a48fc6be6b0f20c1d0ed10958d8dc53453ad58d7be0bf8", size = 58657, upload-time = "2024-12-24T18:30:42.392Z" }, + { url = "https://files.pythonhosted.org/packages/80/28/ae0240f732f0484d3a4dc885d055653c47144bdf59b670aae0ec3c65a7c8/kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4191ee8dfd0be1c3666ccbac178c5a05d5f8d689bbe3fc92f3c4abec817f8fe0", size = 84948, upload-time = "2024-12-24T18:30:44.703Z" }, + { url = "https://files.pythonhosted.org/packages/5d/eb/78d50346c51db22c7203c1611f9b513075f35c4e0e4877c5dde378d66043/kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7cd2785b9391f2873ad46088ed7599a6a71e762e1ea33e87514b1a441ed1da1c", size = 81186, upload-time = "2024-12-24T18:30:45.654Z" }, + { url = "https://files.pythonhosted.org/packages/43/f8/7259f18c77adca88d5f64f9a522792e178b2691f3748817a8750c2d216ef/kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c07b29089b7ba090b6f1a669f1411f27221c3662b3a1b7010e67b59bb5a6f10b", size = 80279, upload-time = "2024-12-24T18:30:47.951Z" }, + { url = "https://files.pythonhosted.org/packages/3a/1d/50ad811d1c5dae091e4cf046beba925bcae0a610e79ae4c538f996f63ed5/kiwisolver-1.4.8-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:65ea09a5a3faadd59c2ce96dc7bf0f364986a315949dc6374f04396b0d60e09b", size = 71762, upload-time = "2024-12-24T18:30:48.903Z" }, ] [[package]] @@ -772,18 +803,18 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6f/6b/c875b30a1ba490860c93da4cabf479e03f584eba06fe5963f6f6644653d8/lazy_loader-0.4.tar.gz", hash = "sha256:47c75182589b91a4e1a85a136c074285a5ad4d9f39c63e0d7fb76391c4574cd1", size = 15431 } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6b/c875b30a1ba490860c93da4cabf479e03f584eba06fe5963f6f6644653d8/lazy_loader-0.4.tar.gz", hash = "sha256:47c75182589b91a4e1a85a136c074285a5ad4d9f39c63e0d7fb76391c4574cd1", size = 15431, upload-time = "2024-04-05T13:03:12.261Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/83/60/d497a310bde3f01cb805196ac61b7ad6dc5dcf8dce66634dc34364b20b4f/lazy_loader-0.4-py3-none-any.whl", hash = "sha256:342aa8e14d543a154047afb4ba8ef17f5563baad3fc610d7b15b213b0f119efc", size = 12097 }, + { url = "https://files.pythonhosted.org/packages/83/60/d497a310bde3f01cb805196ac61b7ad6dc5dcf8dce66634dc34364b20b4f/lazy_loader-0.4-py3-none-any.whl", hash = "sha256:342aa8e14d543a154047afb4ba8ef17f5563baad3fc610d7b15b213b0f119efc", size = 12097, upload-time = "2024-04-05T13:03:10.514Z" }, ] [[package]] name = "lexid" version = "2021.1006" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/60/0b/28a3f9abc75abbf1fa996eb2dd77e1e33a5d1aac62566e3f60a8ec8b8a22/lexid-2021.1006.tar.gz", hash = "sha256:509a3a4cc926d3dbf22b203b18a4c66c25e6473fb7c0e0d30374533ac28bafe5", size = 11525 } +sdist = { url = "https://files.pythonhosted.org/packages/60/0b/28a3f9abc75abbf1fa996eb2dd77e1e33a5d1aac62566e3f60a8ec8b8a22/lexid-2021.1006.tar.gz", hash = "sha256:509a3a4cc926d3dbf22b203b18a4c66c25e6473fb7c0e0d30374533ac28bafe5", size = 11525, upload-time = "2021-04-02T20:18:34.668Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cf/e3/35764404a4b7e2021be1f88f42264c2e92e0c4720273559a62461ce64a47/lexid-2021.1006-py2.py3-none-any.whl", hash = "sha256:5526bb5606fd74c7add23320da5f02805bddd7c77916f2dc1943e6bada8605ed", size = 7587 }, + { url = "https://files.pythonhosted.org/packages/cf/e3/35764404a4b7e2021be1f88f42264c2e92e0c4720273559a62461ce64a47/lexid-2021.1006-py2.py3-none-any.whl", hash = "sha256:5526bb5606fd74c7add23320da5f02805bddd7c77916f2dc1943e6bada8605ed", size = 7587, upload-time = "2021-04-02T20:18:33.129Z" }, ] [[package]] @@ -806,9 +837,9 @@ dependencies = [ { name = "soxr" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9a/c4/22a644b91098223d653993388daaf9af28175f2f39073269efa6f7c71caf/librosa-0.10.1.tar.gz", hash = "sha256:832f7d150d6dd08ed2aa08c0567a4be58330635c32ddd2208de9bc91300802c7", size = 311110 } +sdist = { url = "https://files.pythonhosted.org/packages/9a/c4/22a644b91098223d653993388daaf9af28175f2f39073269efa6f7c71caf/librosa-0.10.1.tar.gz", hash = "sha256:832f7d150d6dd08ed2aa08c0567a4be58330635c32ddd2208de9bc91300802c7", size = 311110, upload-time = "2023-08-16T13:52:20.7Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e2/a2/4f639c1168d7aada749a896afb4892a831e2041bebdcf636aebfe9e86556/librosa-0.10.1-py3-none-any.whl", hash = "sha256:7ab91d9f5fcb75ea14848a05d3b1f825cf8d0c42ca160d19ae6874f2de2d8223", size = 253710 }, + { url = "https://files.pythonhosted.org/packages/e2/a2/4f639c1168d7aada749a896afb4892a831e2041bebdcf636aebfe9e86556/librosa-0.10.1-py3-none-any.whl", hash = "sha256:7ab91d9f5fcb75ea14848a05d3b1f825cf8d0c42ca160d19ae6874f2de2d8223", size = 253710, upload-time = "2023-08-16T13:52:19.141Z" }, ] [[package]] @@ -820,28 +851,28 @@ resolution-markers = [ "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", ] -sdist = { url = "https://files.pythonhosted.org/packages/9f/3d/f513755f285db51ab363a53e898b85562e950f79a2e6767a364530c2f645/llvmlite-0.43.0.tar.gz", hash = "sha256:ae2b5b5c3ef67354824fb75517c8db5fbe93bc02cd9671f3c62271626bc041d5", size = 157069 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/23/ff/6ca7e98998b573b4bd6566f15c35e5c8bea829663a6df0c7aa55ab559da9/llvmlite-0.43.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a289af9a1687c6cf463478f0fa8e8aa3b6fb813317b0d70bf1ed0759eab6f761", size = 31064408 }, - { url = "https://files.pythonhosted.org/packages/ca/5c/a27f9257f86f0cda3f764ff21d9f4217b9f6a0d45e7a39ecfa7905f524ce/llvmlite-0.43.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d4fd101f571a31acb1559ae1af30f30b1dc4b3186669f92ad780e17c81e91bc", size = 28793153 }, - { url = "https://files.pythonhosted.org/packages/7e/3c/4410f670ad0a911227ea2ecfcba9f672a77cf1924df5280c4562032ec32d/llvmlite-0.43.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7d434ec7e2ce3cc8f452d1cd9a28591745de022f931d67be688a737320dfcead", size = 42857276 }, - { url = "https://files.pythonhosted.org/packages/c6/21/2ffbab5714e72f2483207b4a1de79b2eecd9debbf666ff4e7067bcc5c134/llvmlite-0.43.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6912a87782acdff6eb8bf01675ed01d60ca1f2551f8176a300a886f09e836a6a", size = 43871781 }, - { url = "https://files.pythonhosted.org/packages/f2/26/b5478037c453554a61625ef1125f7e12bb1429ae11c6376f47beba9b0179/llvmlite-0.43.0-cp310-cp310-win_amd64.whl", hash = "sha256:14f0e4bf2fd2d9a75a3534111e8ebeb08eda2f33e9bdd6dfa13282afacdde0ed", size = 28123487 }, - { url = "https://files.pythonhosted.org/packages/95/8c/de3276d773ab6ce3ad676df5fab5aac19696b2956319d65d7dd88fb10f19/llvmlite-0.43.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3e8d0618cb9bfe40ac38a9633f2493d4d4e9fcc2f438d39a4e854f39cc0f5f98", size = 31064409 }, - { url = "https://files.pythonhosted.org/packages/ee/e1/38deed89ced4cf378c61e232265cfe933ccde56ae83c901aa68b477d14b1/llvmlite-0.43.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0a9a1a39d4bf3517f2af9d23d479b4175ead205c592ceeb8b89af48a327ea57", size = 28793149 }, - { url = "https://files.pythonhosted.org/packages/2f/b2/4429433eb2dc8379e2cb582502dca074c23837f8fd009907f78a24de4c25/llvmlite-0.43.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1da416ab53e4f7f3bc8d4eeba36d801cc1894b9fbfbf2022b29b6bad34a7df2", size = 42857277 }, - { url = "https://files.pythonhosted.org/packages/6b/99/5d00a7d671b1ba1751fc9f19d3b36f3300774c6eebe2bcdb5f6191763eb4/llvmlite-0.43.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:977525a1e5f4059316b183fb4fd34fa858c9eade31f165427a3977c95e3ee749", size = 43871781 }, - { url = "https://files.pythonhosted.org/packages/20/ab/ed5ed3688c6ba4f0b8d789da19fd8e30a9cf7fc5852effe311bc5aefe73e/llvmlite-0.43.0-cp311-cp311-win_amd64.whl", hash = "sha256:d5bd550001d26450bd90777736c69d68c487d17bf371438f975229b2b8241a91", size = 28107433 }, - { url = "https://files.pythonhosted.org/packages/0b/67/9443509e5d2b6d8587bae3ede5598fa8bd586b1c7701696663ea8af15b5b/llvmlite-0.43.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f99b600aa7f65235a5a05d0b9a9f31150c390f31261f2a0ba678e26823ec38f7", size = 31064409 }, - { url = "https://files.pythonhosted.org/packages/a2/9c/24139d3712d2d352e300c39c0e00d167472c08b3bd350c3c33d72c88ff8d/llvmlite-0.43.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:35d80d61d0cda2d767f72de99450766250560399edc309da16937b93d3b676e7", size = 28793145 }, - { url = "https://files.pythonhosted.org/packages/bf/f1/4c205a48488e574ee9f6505d50e84370a978c90f08dab41a42d8f2c576b6/llvmlite-0.43.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eccce86bba940bae0d8d48ed925f21dbb813519169246e2ab292b5092aba121f", size = 42857276 }, - { url = "https://files.pythonhosted.org/packages/00/5f/323c4d56e8401c50185fd0e875fcf06b71bf825a863699be1eb10aa2a9cb/llvmlite-0.43.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df6509e1507ca0760787a199d19439cc887bfd82226f5af746d6977bd9f66844", size = 43871781 }, - { url = "https://files.pythonhosted.org/packages/c6/94/dea10e263655ce78d777e78d904903faae39d1fc440762be4a9dc46bed49/llvmlite-0.43.0-cp312-cp312-win_amd64.whl", hash = "sha256:7a2872ee80dcf6b5dbdc838763d26554c2a18aa833d31a2635bff16aafefb9c9", size = 28107442 }, - { url = "https://files.pythonhosted.org/packages/2a/73/12925b1bbb3c2beb6d96f892ef5b4d742c34f00ddb9f4a125e9e87b22f52/llvmlite-0.43.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9cd2a7376f7b3367019b664c21f0c61766219faa3b03731113ead75107f3b66c", size = 31064410 }, - { url = "https://files.pythonhosted.org/packages/cc/61/58c70aa0808a8cba825a7d98cc65bef4801b99328fba80837bfcb5fc767f/llvmlite-0.43.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:18e9953c748b105668487b7c81a3e97b046d8abf95c4ddc0cd3c94f4e4651ae8", size = 28793145 }, - { url = "https://files.pythonhosted.org/packages/c8/c6/9324eb5de2ba9d99cbed853d85ba7a318652a48e077797bec27cf40f911d/llvmlite-0.43.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74937acd22dc11b33946b67dca7680e6d103d6e90eeaaaf932603bec6fe7b03a", size = 42857276 }, - { url = "https://files.pythonhosted.org/packages/e0/d0/889e9705107db7b1ec0767b03f15d7b95b4c4f9fdf91928ab1c7e9ffacf6/llvmlite-0.43.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc9efc739cc6ed760f795806f67889923f7274276f0eb45092a1473e40d9b867", size = 43871777 }, - { url = "https://files.pythonhosted.org/packages/df/41/73cc26a2634b538cfe813f618c91e7e9960b8c163f8f0c94a2b0f008b9da/llvmlite-0.43.0-cp39-cp39-win_amd64.whl", hash = "sha256:47e147cdda9037f94b399bf03bfd8a6b6b1f2f90be94a454e3386f006455a9b4", size = 28123489 }, +sdist = { url = "https://files.pythonhosted.org/packages/9f/3d/f513755f285db51ab363a53e898b85562e950f79a2e6767a364530c2f645/llvmlite-0.43.0.tar.gz", hash = "sha256:ae2b5b5c3ef67354824fb75517c8db5fbe93bc02cd9671f3c62271626bc041d5", size = 157069, upload-time = "2024-06-13T18:09:32.641Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/ff/6ca7e98998b573b4bd6566f15c35e5c8bea829663a6df0c7aa55ab559da9/llvmlite-0.43.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a289af9a1687c6cf463478f0fa8e8aa3b6fb813317b0d70bf1ed0759eab6f761", size = 31064408, upload-time = "2024-06-13T18:08:13.462Z" }, + { url = "https://files.pythonhosted.org/packages/ca/5c/a27f9257f86f0cda3f764ff21d9f4217b9f6a0d45e7a39ecfa7905f524ce/llvmlite-0.43.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d4fd101f571a31acb1559ae1af30f30b1dc4b3186669f92ad780e17c81e91bc", size = 28793153, upload-time = "2024-06-13T18:08:17.336Z" }, + { url = "https://files.pythonhosted.org/packages/7e/3c/4410f670ad0a911227ea2ecfcba9f672a77cf1924df5280c4562032ec32d/llvmlite-0.43.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7d434ec7e2ce3cc8f452d1cd9a28591745de022f931d67be688a737320dfcead", size = 42857276, upload-time = "2024-06-13T18:08:21.071Z" }, + { url = "https://files.pythonhosted.org/packages/c6/21/2ffbab5714e72f2483207b4a1de79b2eecd9debbf666ff4e7067bcc5c134/llvmlite-0.43.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6912a87782acdff6eb8bf01675ed01d60ca1f2551f8176a300a886f09e836a6a", size = 43871781, upload-time = "2024-06-13T18:08:26.32Z" }, + { url = "https://files.pythonhosted.org/packages/f2/26/b5478037c453554a61625ef1125f7e12bb1429ae11c6376f47beba9b0179/llvmlite-0.43.0-cp310-cp310-win_amd64.whl", hash = "sha256:14f0e4bf2fd2d9a75a3534111e8ebeb08eda2f33e9bdd6dfa13282afacdde0ed", size = 28123487, upload-time = "2024-06-13T18:08:30.348Z" }, + { url = "https://files.pythonhosted.org/packages/95/8c/de3276d773ab6ce3ad676df5fab5aac19696b2956319d65d7dd88fb10f19/llvmlite-0.43.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3e8d0618cb9bfe40ac38a9633f2493d4d4e9fcc2f438d39a4e854f39cc0f5f98", size = 31064409, upload-time = "2024-06-13T18:08:34.006Z" }, + { url = "https://files.pythonhosted.org/packages/ee/e1/38deed89ced4cf378c61e232265cfe933ccde56ae83c901aa68b477d14b1/llvmlite-0.43.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0a9a1a39d4bf3517f2af9d23d479b4175ead205c592ceeb8b89af48a327ea57", size = 28793149, upload-time = "2024-06-13T18:08:37.42Z" }, + { url = "https://files.pythonhosted.org/packages/2f/b2/4429433eb2dc8379e2cb582502dca074c23837f8fd009907f78a24de4c25/llvmlite-0.43.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1da416ab53e4f7f3bc8d4eeba36d801cc1894b9fbfbf2022b29b6bad34a7df2", size = 42857277, upload-time = "2024-06-13T18:08:40.822Z" }, + { url = "https://files.pythonhosted.org/packages/6b/99/5d00a7d671b1ba1751fc9f19d3b36f3300774c6eebe2bcdb5f6191763eb4/llvmlite-0.43.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:977525a1e5f4059316b183fb4fd34fa858c9eade31f165427a3977c95e3ee749", size = 43871781, upload-time = "2024-06-13T18:08:46.41Z" }, + { url = "https://files.pythonhosted.org/packages/20/ab/ed5ed3688c6ba4f0b8d789da19fd8e30a9cf7fc5852effe311bc5aefe73e/llvmlite-0.43.0-cp311-cp311-win_amd64.whl", hash = "sha256:d5bd550001d26450bd90777736c69d68c487d17bf371438f975229b2b8241a91", size = 28107433, upload-time = "2024-06-13T18:08:50.834Z" }, + { url = "https://files.pythonhosted.org/packages/0b/67/9443509e5d2b6d8587bae3ede5598fa8bd586b1c7701696663ea8af15b5b/llvmlite-0.43.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f99b600aa7f65235a5a05d0b9a9f31150c390f31261f2a0ba678e26823ec38f7", size = 31064409, upload-time = "2024-06-13T18:08:54.375Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9c/24139d3712d2d352e300c39c0e00d167472c08b3bd350c3c33d72c88ff8d/llvmlite-0.43.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:35d80d61d0cda2d767f72de99450766250560399edc309da16937b93d3b676e7", size = 28793145, upload-time = "2024-06-13T18:08:57.953Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f1/4c205a48488e574ee9f6505d50e84370a978c90f08dab41a42d8f2c576b6/llvmlite-0.43.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eccce86bba940bae0d8d48ed925f21dbb813519169246e2ab292b5092aba121f", size = 42857276, upload-time = "2024-06-13T18:09:02.067Z" }, + { url = "https://files.pythonhosted.org/packages/00/5f/323c4d56e8401c50185fd0e875fcf06b71bf825a863699be1eb10aa2a9cb/llvmlite-0.43.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df6509e1507ca0760787a199d19439cc887bfd82226f5af746d6977bd9f66844", size = 43871781, upload-time = "2024-06-13T18:09:06.667Z" }, + { url = "https://files.pythonhosted.org/packages/c6/94/dea10e263655ce78d777e78d904903faae39d1fc440762be4a9dc46bed49/llvmlite-0.43.0-cp312-cp312-win_amd64.whl", hash = "sha256:7a2872ee80dcf6b5dbdc838763d26554c2a18aa833d31a2635bff16aafefb9c9", size = 28107442, upload-time = "2024-06-13T18:09:10.709Z" }, + { url = "https://files.pythonhosted.org/packages/2a/73/12925b1bbb3c2beb6d96f892ef5b4d742c34f00ddb9f4a125e9e87b22f52/llvmlite-0.43.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9cd2a7376f7b3367019b664c21f0c61766219faa3b03731113ead75107f3b66c", size = 31064410, upload-time = "2024-06-13T18:09:14.091Z" }, + { url = "https://files.pythonhosted.org/packages/cc/61/58c70aa0808a8cba825a7d98cc65bef4801b99328fba80837bfcb5fc767f/llvmlite-0.43.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:18e9953c748b105668487b7c81a3e97b046d8abf95c4ddc0cd3c94f4e4651ae8", size = 28793145, upload-time = "2024-06-13T18:09:17.531Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c6/9324eb5de2ba9d99cbed853d85ba7a318652a48e077797bec27cf40f911d/llvmlite-0.43.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74937acd22dc11b33946b67dca7680e6d103d6e90eeaaaf932603bec6fe7b03a", size = 42857276, upload-time = "2024-06-13T18:09:21.377Z" }, + { url = "https://files.pythonhosted.org/packages/e0/d0/889e9705107db7b1ec0767b03f15d7b95b4c4f9fdf91928ab1c7e9ffacf6/llvmlite-0.43.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc9efc739cc6ed760f795806f67889923f7274276f0eb45092a1473e40d9b867", size = 43871777, upload-time = "2024-06-13T18:09:25.76Z" }, + { url = "https://files.pythonhosted.org/packages/df/41/73cc26a2634b538cfe813f618c91e7e9960b8c163f8f0c94a2b0f008b9da/llvmlite-0.43.0-cp39-cp39-win_amd64.whl", hash = "sha256:47e147cdda9037f94b399bf03bfd8a6b6b1f2f90be94a454e3386f006455a9b4", size = 28123489, upload-time = "2024-06-13T18:09:29.78Z" }, ] [[package]] @@ -856,23 +887,23 @@ resolution-markers = [ "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", ] -sdist = { url = "https://files.pythonhosted.org/packages/89/6a/95a3d3610d5c75293d5dbbb2a76480d5d4eeba641557b69fe90af6c5b84e/llvmlite-0.44.0.tar.gz", hash = "sha256:07667d66a5d150abed9157ab6c0b9393c9356f229784a4385c02f99e94fc94d4", size = 171880 } +sdist = { url = "https://files.pythonhosted.org/packages/89/6a/95a3d3610d5c75293d5dbbb2a76480d5d4eeba641557b69fe90af6c5b84e/llvmlite-0.44.0.tar.gz", hash = "sha256:07667d66a5d150abed9157ab6c0b9393c9356f229784a4385c02f99e94fc94d4", size = 171880, upload-time = "2025-01-20T11:14:41.342Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/41/75/d4863ddfd8ab5f6e70f4504cf8cc37f4e986ec6910f4ef8502bb7d3c1c71/llvmlite-0.44.0-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:9fbadbfba8422123bab5535b293da1cf72f9f478a65645ecd73e781f962ca614", size = 28132306 }, - { url = "https://files.pythonhosted.org/packages/37/d9/6e8943e1515d2f1003e8278819ec03e4e653e2eeb71e4d00de6cfe59424e/llvmlite-0.44.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cccf8eb28f24840f2689fb1a45f9c0f7e582dd24e088dcf96e424834af11f791", size = 26201096 }, - { url = "https://files.pythonhosted.org/packages/aa/46/8ffbc114def88cc698906bf5acab54ca9fdf9214fe04aed0e71731fb3688/llvmlite-0.44.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7202b678cdf904823c764ee0fe2dfe38a76981f4c1e51715b4cb5abb6cf1d9e8", size = 42361859 }, - { url = "https://files.pythonhosted.org/packages/30/1c/9366b29ab050a726af13ebaae8d0dff00c3c58562261c79c635ad4f5eb71/llvmlite-0.44.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40526fb5e313d7b96bda4cbb2c85cd5374e04d80732dd36a282d72a560bb6408", size = 41184199 }, - { url = "https://files.pythonhosted.org/packages/69/07/35e7c594b021ecb1938540f5bce543ddd8713cff97f71d81f021221edc1b/llvmlite-0.44.0-cp310-cp310-win_amd64.whl", hash = "sha256:41e3839150db4330e1b2716c0be3b5c4672525b4c9005e17c7597f835f351ce2", size = 30332381 }, - { url = "https://files.pythonhosted.org/packages/b5/e2/86b245397052386595ad726f9742e5223d7aea999b18c518a50e96c3aca4/llvmlite-0.44.0-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:eed7d5f29136bda63b6d7804c279e2b72e08c952b7c5df61f45db408e0ee52f3", size = 28132305 }, - { url = "https://files.pythonhosted.org/packages/ff/ec/506902dc6870249fbe2466d9cf66d531265d0f3a1157213c8f986250c033/llvmlite-0.44.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ace564d9fa44bb91eb6e6d8e7754977783c68e90a471ea7ce913bff30bd62427", size = 26201090 }, - { url = "https://files.pythonhosted.org/packages/99/fe/d030f1849ebb1f394bb3f7adad5e729b634fb100515594aca25c354ffc62/llvmlite-0.44.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c5d22c3bfc842668168a786af4205ec8e3ad29fb1bc03fd11fd48460d0df64c1", size = 42361858 }, - { url = "https://files.pythonhosted.org/packages/d7/7a/ce6174664b9077fc673d172e4c888cb0b128e707e306bc33fff8c2035f0d/llvmlite-0.44.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f01a394e9c9b7b1d4e63c327b096d10f6f0ed149ef53d38a09b3749dcf8c9610", size = 41184200 }, - { url = "https://files.pythonhosted.org/packages/5f/c6/258801143975a6d09a373f2641237992496e15567b907a4d401839d671b8/llvmlite-0.44.0-cp311-cp311-win_amd64.whl", hash = "sha256:d8489634d43c20cd0ad71330dde1d5bc7b9966937a263ff1ec1cebb90dc50955", size = 30331193 }, - { url = "https://files.pythonhosted.org/packages/15/86/e3c3195b92e6e492458f16d233e58a1a812aa2bfbef9bdd0fbafcec85c60/llvmlite-0.44.0-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:1d671a56acf725bf1b531d5ef76b86660a5ab8ef19bb6a46064a705c6ca80aad", size = 28132297 }, - { url = "https://files.pythonhosted.org/packages/d6/53/373b6b8be67b9221d12b24125fd0ec56b1078b660eeae266ec388a6ac9a0/llvmlite-0.44.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5f79a728e0435493611c9f405168682bb75ffd1fbe6fc360733b850c80a026db", size = 26201105 }, - { url = "https://files.pythonhosted.org/packages/cb/da/8341fd3056419441286c8e26bf436923021005ece0bff5f41906476ae514/llvmlite-0.44.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0143a5ef336da14deaa8ec26c5449ad5b6a2b564df82fcef4be040b9cacfea9", size = 42361901 }, - { url = "https://files.pythonhosted.org/packages/53/ad/d79349dc07b8a395a99153d7ce8b01d6fcdc9f8231355a5df55ded649b61/llvmlite-0.44.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d752f89e31b66db6f8da06df8b39f9b91e78c5feea1bf9e8c1fba1d1c24c065d", size = 41184247 }, - { url = "https://files.pythonhosted.org/packages/e2/3b/a9a17366af80127bd09decbe2a54d8974b6d8b274b39bf47fbaedeec6307/llvmlite-0.44.0-cp312-cp312-win_amd64.whl", hash = "sha256:eae7e2d4ca8f88f89d315b48c6b741dcb925d6a1042da694aa16ab3dd4cbd3a1", size = 30332380 }, + { url = "https://files.pythonhosted.org/packages/41/75/d4863ddfd8ab5f6e70f4504cf8cc37f4e986ec6910f4ef8502bb7d3c1c71/llvmlite-0.44.0-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:9fbadbfba8422123bab5535b293da1cf72f9f478a65645ecd73e781f962ca614", size = 28132306, upload-time = "2025-01-20T11:12:18.634Z" }, + { url = "https://files.pythonhosted.org/packages/37/d9/6e8943e1515d2f1003e8278819ec03e4e653e2eeb71e4d00de6cfe59424e/llvmlite-0.44.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cccf8eb28f24840f2689fb1a45f9c0f7e582dd24e088dcf96e424834af11f791", size = 26201096, upload-time = "2025-01-20T11:12:24.544Z" }, + { url = "https://files.pythonhosted.org/packages/aa/46/8ffbc114def88cc698906bf5acab54ca9fdf9214fe04aed0e71731fb3688/llvmlite-0.44.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7202b678cdf904823c764ee0fe2dfe38a76981f4c1e51715b4cb5abb6cf1d9e8", size = 42361859, upload-time = "2025-01-20T11:12:31.839Z" }, + { url = "https://files.pythonhosted.org/packages/30/1c/9366b29ab050a726af13ebaae8d0dff00c3c58562261c79c635ad4f5eb71/llvmlite-0.44.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40526fb5e313d7b96bda4cbb2c85cd5374e04d80732dd36a282d72a560bb6408", size = 41184199, upload-time = "2025-01-20T11:12:40.049Z" }, + { url = "https://files.pythonhosted.org/packages/69/07/35e7c594b021ecb1938540f5bce543ddd8713cff97f71d81f021221edc1b/llvmlite-0.44.0-cp310-cp310-win_amd64.whl", hash = "sha256:41e3839150db4330e1b2716c0be3b5c4672525b4c9005e17c7597f835f351ce2", size = 30332381, upload-time = "2025-01-20T11:12:47.054Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e2/86b245397052386595ad726f9742e5223d7aea999b18c518a50e96c3aca4/llvmlite-0.44.0-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:eed7d5f29136bda63b6d7804c279e2b72e08c952b7c5df61f45db408e0ee52f3", size = 28132305, upload-time = "2025-01-20T11:12:53.936Z" }, + { url = "https://files.pythonhosted.org/packages/ff/ec/506902dc6870249fbe2466d9cf66d531265d0f3a1157213c8f986250c033/llvmlite-0.44.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ace564d9fa44bb91eb6e6d8e7754977783c68e90a471ea7ce913bff30bd62427", size = 26201090, upload-time = "2025-01-20T11:12:59.847Z" }, + { url = "https://files.pythonhosted.org/packages/99/fe/d030f1849ebb1f394bb3f7adad5e729b634fb100515594aca25c354ffc62/llvmlite-0.44.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c5d22c3bfc842668168a786af4205ec8e3ad29fb1bc03fd11fd48460d0df64c1", size = 42361858, upload-time = "2025-01-20T11:13:07.623Z" }, + { url = "https://files.pythonhosted.org/packages/d7/7a/ce6174664b9077fc673d172e4c888cb0b128e707e306bc33fff8c2035f0d/llvmlite-0.44.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f01a394e9c9b7b1d4e63c327b096d10f6f0ed149ef53d38a09b3749dcf8c9610", size = 41184200, upload-time = "2025-01-20T11:13:20.058Z" }, + { url = "https://files.pythonhosted.org/packages/5f/c6/258801143975a6d09a373f2641237992496e15567b907a4d401839d671b8/llvmlite-0.44.0-cp311-cp311-win_amd64.whl", hash = "sha256:d8489634d43c20cd0ad71330dde1d5bc7b9966937a263ff1ec1cebb90dc50955", size = 30331193, upload-time = "2025-01-20T11:13:26.976Z" }, + { url = "https://files.pythonhosted.org/packages/15/86/e3c3195b92e6e492458f16d233e58a1a812aa2bfbef9bdd0fbafcec85c60/llvmlite-0.44.0-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:1d671a56acf725bf1b531d5ef76b86660a5ab8ef19bb6a46064a705c6ca80aad", size = 28132297, upload-time = "2025-01-20T11:13:32.57Z" }, + { url = "https://files.pythonhosted.org/packages/d6/53/373b6b8be67b9221d12b24125fd0ec56b1078b660eeae266ec388a6ac9a0/llvmlite-0.44.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5f79a728e0435493611c9f405168682bb75ffd1fbe6fc360733b850c80a026db", size = 26201105, upload-time = "2025-01-20T11:13:38.744Z" }, + { url = "https://files.pythonhosted.org/packages/cb/da/8341fd3056419441286c8e26bf436923021005ece0bff5f41906476ae514/llvmlite-0.44.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0143a5ef336da14deaa8ec26c5449ad5b6a2b564df82fcef4be040b9cacfea9", size = 42361901, upload-time = "2025-01-20T11:13:46.711Z" }, + { url = "https://files.pythonhosted.org/packages/53/ad/d79349dc07b8a395a99153d7ce8b01d6fcdc9f8231355a5df55ded649b61/llvmlite-0.44.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d752f89e31b66db6f8da06df8b39f9b91e78c5feea1bf9e8c1fba1d1c24c065d", size = 41184247, upload-time = "2025-01-20T11:13:56.159Z" }, + { url = "https://files.pythonhosted.org/packages/e2/3b/a9a17366af80127bd09decbe2a54d8974b6d8b274b39bf47fbaedeec6307/llvmlite-0.44.0-cp312-cp312-win_amd64.whl", hash = "sha256:eae7e2d4ca8f88f89d315b48c6b741dcb925d6a1042da694aa16ab3dd4cbd3a1", size = 30332380, upload-time = "2025-01-20T11:14:02.442Z" }, ] [[package]] @@ -882,9 +913,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mdurl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, ] [[package]] @@ -905,114 +936,114 @@ dependencies = [ { name = "pyparsing" }, { name = "python-dateutil" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fb/ab/38a0e94cb01dacb50f06957c2bed1c83b8f9dac6618988a37b2487862944/matplotlib-3.8.2.tar.gz", hash = "sha256:01a978b871b881ee76017152f1f1a0cbf6bd5f7b8ff8c96df0df1bd57d8755a1", size = 35866957 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/92/d0/fc5f6796a1956f5b9a33555611d01a3cec038f000c3d70ecb051b1631ac4/matplotlib-3.8.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:09796f89fb71a0c0e1e2f4bdaf63fb2cefc84446bb963ecdeb40dfee7dfa98c7", size = 7590640 }, - { url = "https://files.pythonhosted.org/packages/57/44/007b592809f50883c910db9ec4b81b16dfa0136407250fb581824daabf03/matplotlib-3.8.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6f9c6976748a25e8b9be51ea028df49b8e561eed7809146da7a47dbecebab367", size = 7484350 }, - { url = "https://files.pythonhosted.org/packages/01/87/c7b24f3048234fe10184560263be2173311376dc3d1fa329de7f012d6ce5/matplotlib-3.8.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b78e4f2cedf303869b782071b55fdde5987fda3038e9d09e58c91cc261b5ad18", size = 11382388 }, - { url = "https://files.pythonhosted.org/packages/19/e5/a4ea514515f270224435c69359abb7a3d152ed31b9ee3ba5e63017461945/matplotlib-3.8.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e208f46cf6576a7624195aa047cb344a7f802e113bb1a06cfd4bee431de5e31", size = 11611959 }, - { url = "https://files.pythonhosted.org/packages/09/23/ab5a562c9acb81e351b084bea39f65b153918417fb434619cf5a19f44a55/matplotlib-3.8.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:46a569130ff53798ea5f50afce7406e91fdc471ca1e0e26ba976a8c734c9427a", size = 9536938 }, - { url = "https://files.pythonhosted.org/packages/46/37/b5e27ab30ecc0a3694c8a78287b5ef35dad0c3095c144fcc43081170bfd6/matplotlib-3.8.2-cp310-cp310-win_amd64.whl", hash = "sha256:830f00640c965c5b7f6bc32f0d4ce0c36dfe0379f7dd65b07a00c801713ec40a", size = 7643836 }, - { url = "https://files.pythonhosted.org/packages/a9/0d/53afb186adafc7326d093b8333e8a79974c495095771659f4304626c4bc7/matplotlib-3.8.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d86593ccf546223eb75a39b44c32788e6f6440d13cfc4750c1c15d0fcb850b63", size = 7593458 }, - { url = "https://files.pythonhosted.org/packages/ce/25/a557ee10ac9dce1300850024707ce1850a6958f1673a9194be878b99d631/matplotlib-3.8.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9a5430836811b7652991939012f43d2808a2db9b64ee240387e8c43e2e5578c8", size = 7486840 }, - { url = "https://files.pythonhosted.org/packages/e7/3d/72712b3895ee180f6e342638a8591c31912fbcc09ce9084cc256da16d0a0/matplotlib-3.8.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9576723858a78751d5aacd2497b8aef29ffea6d1c95981505877f7ac28215c6", size = 11387332 }, - { url = "https://files.pythonhosted.org/packages/92/1a/cd3e0c90d1a763ad90073e13b189b4702f11becf4e71dbbad70a7a149811/matplotlib-3.8.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ba9cbd8ac6cf422f3102622b20f8552d601bf8837e49a3afed188d560152788", size = 11616911 }, - { url = "https://files.pythonhosted.org/packages/78/4a/bad239071477305a3758eb4810615e310a113399cddd7682998be9f01e97/matplotlib-3.8.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:03f9d160a29e0b65c0790bb07f4f45d6a181b1ac33eb1bb0dd225986450148f0", size = 9549260 }, - { url = "https://files.pythonhosted.org/packages/26/5a/27fd341e4510257789f19a4b4be8bb90d1113b8f176c3dab562b4f21466e/matplotlib-3.8.2-cp311-cp311-win_amd64.whl", hash = "sha256:3773002da767f0a9323ba1a9b9b5d00d6257dbd2a93107233167cfb581f64717", size = 7645742 }, - { url = "https://files.pythonhosted.org/packages/e4/1b/864d28d5a72d586ac137f4ca54d5afc8b869720e30d508dbd9adcce4d231/matplotlib-3.8.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:4c318c1e95e2f5926fba326f68177dee364aa791d6df022ceb91b8221bd0a627", size = 7590988 }, - { url = "https://files.pythonhosted.org/packages/9a/b0/dd2b60f2dd90fbc21d1d3129c36a453c322d7995d5e3589f5b3c59ee528d/matplotlib-3.8.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:091275d18d942cf1ee9609c830a1bc36610607d8223b1b981c37d5c9fc3e46a4", size = 7483594 }, - { url = "https://files.pythonhosted.org/packages/33/da/9942533ad9f96753bde0e5a5d48eacd6c21de8ea1ad16570e31bda8a017f/matplotlib-3.8.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b0f3b8ea0e99e233a4bcc44590f01604840d833c280ebb8fe5554fd3e6cfe8d", size = 11380843 }, - { url = "https://files.pythonhosted.org/packages/fc/52/bfd36eb4745a3b21b3946c2c3a15679b620e14574fe2b98e9451b65ef578/matplotlib-3.8.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7b1704a530395aaf73912be741c04d181f82ca78084fbd80bc737be04848331", size = 11604608 }, - { url = "https://files.pythonhosted.org/packages/6d/8c/0cdfbf604d4ea3dfa77435176c51e233cc408ad8f3efbf8d2c9f57cbdafb/matplotlib-3.8.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:533b0e3b0c6768eef8cbe4b583731ce25a91ab54a22f830db2b031e83cca9213", size = 9545252 }, - { url = "https://files.pythonhosted.org/packages/2e/51/c77a14869b7eb9d6fb440e811b754fc3950d6868c38ace57d0632b674415/matplotlib-3.8.2-cp312-cp312-win_amd64.whl", hash = "sha256:0f4fc5d72b75e2c18e55eb32292659cf731d9d5b312a6eb036506304f4675630", size = 7645067 }, - { url = "https://files.pythonhosted.org/packages/00/43/2642a6da71e6cd6159641f997701b610e2a6f685c159b72994a71bc551da/matplotlib-3.8.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:deaed9ad4da0b1aea77fe0aa0cebb9ef611c70b3177be936a95e5d01fa05094f", size = 7591097 }, - { url = "https://files.pythonhosted.org/packages/5c/98/211647fc6aa89355c11b028f37ceae95d7e1bbb99ac363cbf4c4297ef4d7/matplotlib-3.8.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:172f4d0fbac3383d39164c6caafd3255ce6fa58f08fc392513a0b1d3b89c4f89", size = 7484604 }, - { url = "https://files.pythonhosted.org/packages/ca/e7/7e965b6388e3d1bf893802f5c8010e0f35752cc9054ed7b23fe2b761bfee/matplotlib-3.8.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7d36c2209d9136cd8e02fab1c0ddc185ce79bc914c45054a9f514e44c787917", size = 11379339 }, - { url = "https://files.pythonhosted.org/packages/53/1f/653d60d2ec81a6095fa3e571cf2de57742bab8a51a5c01de26730ce3dc53/matplotlib-3.8.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5864bdd7da445e4e5e011b199bb67168cdad10b501750367c496420f2ad00843", size = 11609330 }, - { url = "https://files.pythonhosted.org/packages/95/50/d8a159cdf0f1e01234c7f29070a909231c193d9d3d3451f65c5759323915/matplotlib-3.8.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ef8345b48e95cee45ff25192ed1f4857273117917a4dcd48e3905619bcd9c9b8", size = 9536094 }, - { url = "https://files.pythonhosted.org/packages/5e/4b/f228e012312120c8b4ef02c43230499e9df7d3075fd1e965086e529a0f49/matplotlib-3.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:7c48d9e221b637c017232e3760ed30b4e8d5dfd081daf327e829bf2a72c731b4", size = 7645039 }, - { url = "https://files.pythonhosted.org/packages/1a/ec/762d285617fad89beadd1a0b12bdd129686b0083d2c3cfd1e47b8c45d9b0/matplotlib-3.8.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:aa11b3c6928a1e496c1a79917d51d4cd5d04f8a2e75f21df4949eeefdf697f4b", size = 7549568 }, - { url = "https://files.pythonhosted.org/packages/4f/70/e6b3e2d5c3414d5878d5ab44c5f8ad9ad525ae8c8300128b95b691a2df4a/matplotlib-3.8.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1095fecf99eeb7384dabad4bf44b965f929a5f6079654b681193edf7169ec20", size = 7691379 }, - { url = "https://files.pythonhosted.org/packages/6d/5b/b400181761fbe42b0e1d13d84d3312fef999c0eed39ccd02427710489255/matplotlib-3.8.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:bddfb1db89bfaa855912261c805bd0e10218923cc262b9159a49c29a7a1c1afa", size = 7644537 }, +sdist = { url = "https://files.pythonhosted.org/packages/fb/ab/38a0e94cb01dacb50f06957c2bed1c83b8f9dac6618988a37b2487862944/matplotlib-3.8.2.tar.gz", hash = "sha256:01a978b871b881ee76017152f1f1a0cbf6bd5f7b8ff8c96df0df1bd57d8755a1", size = 35866957, upload-time = "2023-11-17T21:16:40.15Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/d0/fc5f6796a1956f5b9a33555611d01a3cec038f000c3d70ecb051b1631ac4/matplotlib-3.8.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:09796f89fb71a0c0e1e2f4bdaf63fb2cefc84446bb963ecdeb40dfee7dfa98c7", size = 7590640, upload-time = "2023-11-17T21:17:02.834Z" }, + { url = "https://files.pythonhosted.org/packages/57/44/007b592809f50883c910db9ec4b81b16dfa0136407250fb581824daabf03/matplotlib-3.8.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6f9c6976748a25e8b9be51ea028df49b8e561eed7809146da7a47dbecebab367", size = 7484350, upload-time = "2023-11-17T21:17:12.281Z" }, + { url = "https://files.pythonhosted.org/packages/01/87/c7b24f3048234fe10184560263be2173311376dc3d1fa329de7f012d6ce5/matplotlib-3.8.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b78e4f2cedf303869b782071b55fdde5987fda3038e9d09e58c91cc261b5ad18", size = 11382388, upload-time = "2023-11-17T21:17:26.461Z" }, + { url = "https://files.pythonhosted.org/packages/19/e5/a4ea514515f270224435c69359abb7a3d152ed31b9ee3ba5e63017461945/matplotlib-3.8.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e208f46cf6576a7624195aa047cb344a7f802e113bb1a06cfd4bee431de5e31", size = 11611959, upload-time = "2023-11-17T21:17:40.541Z" }, + { url = "https://files.pythonhosted.org/packages/09/23/ab5a562c9acb81e351b084bea39f65b153918417fb434619cf5a19f44a55/matplotlib-3.8.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:46a569130ff53798ea5f50afce7406e91fdc471ca1e0e26ba976a8c734c9427a", size = 9536938, upload-time = "2023-11-17T21:17:49.925Z" }, + { url = "https://files.pythonhosted.org/packages/46/37/b5e27ab30ecc0a3694c8a78287b5ef35dad0c3095c144fcc43081170bfd6/matplotlib-3.8.2-cp310-cp310-win_amd64.whl", hash = "sha256:830f00640c965c5b7f6bc32f0d4ce0c36dfe0379f7dd65b07a00c801713ec40a", size = 7643836, upload-time = "2023-11-17T21:17:58.379Z" }, + { url = "https://files.pythonhosted.org/packages/a9/0d/53afb186adafc7326d093b8333e8a79974c495095771659f4304626c4bc7/matplotlib-3.8.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d86593ccf546223eb75a39b44c32788e6f6440d13cfc4750c1c15d0fcb850b63", size = 7593458, upload-time = "2023-11-17T21:18:06.141Z" }, + { url = "https://files.pythonhosted.org/packages/ce/25/a557ee10ac9dce1300850024707ce1850a6958f1673a9194be878b99d631/matplotlib-3.8.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9a5430836811b7652991939012f43d2808a2db9b64ee240387e8c43e2e5578c8", size = 7486840, upload-time = "2023-11-17T21:18:13.706Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3d/72712b3895ee180f6e342638a8591c31912fbcc09ce9084cc256da16d0a0/matplotlib-3.8.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9576723858a78751d5aacd2497b8aef29ffea6d1c95981505877f7ac28215c6", size = 11387332, upload-time = "2023-11-17T21:18:23.699Z" }, + { url = "https://files.pythonhosted.org/packages/92/1a/cd3e0c90d1a763ad90073e13b189b4702f11becf4e71dbbad70a7a149811/matplotlib-3.8.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ba9cbd8ac6cf422f3102622b20f8552d601bf8837e49a3afed188d560152788", size = 11616911, upload-time = "2023-11-17T21:18:35.27Z" }, + { url = "https://files.pythonhosted.org/packages/78/4a/bad239071477305a3758eb4810615e310a113399cddd7682998be9f01e97/matplotlib-3.8.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:03f9d160a29e0b65c0790bb07f4f45d6a181b1ac33eb1bb0dd225986450148f0", size = 9549260, upload-time = "2023-11-17T21:18:44.836Z" }, + { url = "https://files.pythonhosted.org/packages/26/5a/27fd341e4510257789f19a4b4be8bb90d1113b8f176c3dab562b4f21466e/matplotlib-3.8.2-cp311-cp311-win_amd64.whl", hash = "sha256:3773002da767f0a9323ba1a9b9b5d00d6257dbd2a93107233167cfb581f64717", size = 7645742, upload-time = "2023-11-17T21:18:53.448Z" }, + { url = "https://files.pythonhosted.org/packages/e4/1b/864d28d5a72d586ac137f4ca54d5afc8b869720e30d508dbd9adcce4d231/matplotlib-3.8.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:4c318c1e95e2f5926fba326f68177dee364aa791d6df022ceb91b8221bd0a627", size = 7590988, upload-time = "2023-11-17T21:19:01.119Z" }, + { url = "https://files.pythonhosted.org/packages/9a/b0/dd2b60f2dd90fbc21d1d3129c36a453c322d7995d5e3589f5b3c59ee528d/matplotlib-3.8.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:091275d18d942cf1ee9609c830a1bc36610607d8223b1b981c37d5c9fc3e46a4", size = 7483594, upload-time = "2023-11-17T21:19:09.865Z" }, + { url = "https://files.pythonhosted.org/packages/33/da/9942533ad9f96753bde0e5a5d48eacd6c21de8ea1ad16570e31bda8a017f/matplotlib-3.8.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b0f3b8ea0e99e233a4bcc44590f01604840d833c280ebb8fe5554fd3e6cfe8d", size = 11380843, upload-time = "2023-11-17T21:19:20.46Z" }, + { url = "https://files.pythonhosted.org/packages/fc/52/bfd36eb4745a3b21b3946c2c3a15679b620e14574fe2b98e9451b65ef578/matplotlib-3.8.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7b1704a530395aaf73912be741c04d181f82ca78084fbd80bc737be04848331", size = 11604608, upload-time = "2023-11-17T21:19:31.363Z" }, + { url = "https://files.pythonhosted.org/packages/6d/8c/0cdfbf604d4ea3dfa77435176c51e233cc408ad8f3efbf8d2c9f57cbdafb/matplotlib-3.8.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:533b0e3b0c6768eef8cbe4b583731ce25a91ab54a22f830db2b031e83cca9213", size = 9545252, upload-time = "2023-11-17T21:19:42.271Z" }, + { url = "https://files.pythonhosted.org/packages/2e/51/c77a14869b7eb9d6fb440e811b754fc3950d6868c38ace57d0632b674415/matplotlib-3.8.2-cp312-cp312-win_amd64.whl", hash = "sha256:0f4fc5d72b75e2c18e55eb32292659cf731d9d5b312a6eb036506304f4675630", size = 7645067, upload-time = "2023-11-17T21:19:50.091Z" }, + { url = "https://files.pythonhosted.org/packages/00/43/2642a6da71e6cd6159641f997701b610e2a6f685c159b72994a71bc551da/matplotlib-3.8.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:deaed9ad4da0b1aea77fe0aa0cebb9ef611c70b3177be936a95e5d01fa05094f", size = 7591097, upload-time = "2023-11-17T21:19:58.233Z" }, + { url = "https://files.pythonhosted.org/packages/5c/98/211647fc6aa89355c11b028f37ceae95d7e1bbb99ac363cbf4c4297ef4d7/matplotlib-3.8.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:172f4d0fbac3383d39164c6caafd3255ce6fa58f08fc392513a0b1d3b89c4f89", size = 7484604, upload-time = "2023-11-17T21:20:06.377Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e7/7e965b6388e3d1bf893802f5c8010e0f35752cc9054ed7b23fe2b761bfee/matplotlib-3.8.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7d36c2209d9136cd8e02fab1c0ddc185ce79bc914c45054a9f514e44c787917", size = 11379339, upload-time = "2023-11-17T21:20:18.179Z" }, + { url = "https://files.pythonhosted.org/packages/53/1f/653d60d2ec81a6095fa3e571cf2de57742bab8a51a5c01de26730ce3dc53/matplotlib-3.8.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5864bdd7da445e4e5e011b199bb67168cdad10b501750367c496420f2ad00843", size = 11609330, upload-time = "2023-11-17T21:20:29.105Z" }, + { url = "https://files.pythonhosted.org/packages/95/50/d8a159cdf0f1e01234c7f29070a909231c193d9d3d3451f65c5759323915/matplotlib-3.8.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ef8345b48e95cee45ff25192ed1f4857273117917a4dcd48e3905619bcd9c9b8", size = 9536094, upload-time = "2023-11-17T21:20:38.581Z" }, + { url = "https://files.pythonhosted.org/packages/5e/4b/f228e012312120c8b4ef02c43230499e9df7d3075fd1e965086e529a0f49/matplotlib-3.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:7c48d9e221b637c017232e3760ed30b4e8d5dfd081daf327e829bf2a72c731b4", size = 7645039, upload-time = "2023-11-17T21:20:46.917Z" }, + { url = "https://files.pythonhosted.org/packages/1a/ec/762d285617fad89beadd1a0b12bdd129686b0083d2c3cfd1e47b8c45d9b0/matplotlib-3.8.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:aa11b3c6928a1e496c1a79917d51d4cd5d04f8a2e75f21df4949eeefdf697f4b", size = 7549568, upload-time = "2023-11-17T21:20:54.797Z" }, + { url = "https://files.pythonhosted.org/packages/4f/70/e6b3e2d5c3414d5878d5ab44c5f8ad9ad525ae8c8300128b95b691a2df4a/matplotlib-3.8.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1095fecf99eeb7384dabad4bf44b965f929a5f6079654b681193edf7169ec20", size = 7691379, upload-time = "2023-11-17T21:21:02.391Z" }, + { url = "https://files.pythonhosted.org/packages/6d/5b/b400181761fbe42b0e1d13d84d3312fef999c0eed39ccd02427710489255/matplotlib-3.8.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:bddfb1db89bfaa855912261c805bd0e10218923cc262b9159a49c29a7a1c1afa", size = 7644537, upload-time = "2023-11-17T21:21:13.009Z" }, ] [[package]] name = "mccabe" version = "0.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658 } +sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658, upload-time = "2022-01-24T01:14:51.113Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350 }, + { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350, upload-time = "2022-01-24T01:14:49.62Z" }, ] [[package]] name = "mdurl" version = "0.1.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] [[package]] name = "msgpack" version = "1.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cb/d0/7555686ae7ff5731205df1012ede15dd9d927f6227ea151e901c7406af4f/msgpack-1.1.0.tar.gz", hash = "sha256:dd432ccc2c72b914e4cb77afce64aab761c1137cc698be3984eee260bcb2896e", size = 167260 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4b/f9/a892a6038c861fa849b11a2bb0502c07bc698ab6ea53359e5771397d883b/msgpack-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7ad442d527a7e358a469faf43fda45aaf4ac3249c8310a82f0ccff9164e5dccd", size = 150428 }, - { url = "https://files.pythonhosted.org/packages/df/7a/d174cc6a3b6bb85556e6a046d3193294a92f9a8e583cdbd46dc8a1d7e7f4/msgpack-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:74bed8f63f8f14d75eec75cf3d04ad581da6b914001b474a5d3cd3372c8cc27d", size = 84131 }, - { url = "https://files.pythonhosted.org/packages/08/52/bf4fbf72f897a23a56b822997a72c16de07d8d56d7bf273242f884055682/msgpack-1.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:914571a2a5b4e7606997e169f64ce53a8b1e06f2cf2c3a7273aa106236d43dd5", size = 81215 }, - { url = "https://files.pythonhosted.org/packages/02/95/dc0044b439b518236aaf012da4677c1b8183ce388411ad1b1e63c32d8979/msgpack-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c921af52214dcbb75e6bdf6a661b23c3e6417f00c603dd2070bccb5c3ef499f5", size = 371229 }, - { url = "https://files.pythonhosted.org/packages/ff/75/09081792db60470bef19d9c2be89f024d366b1e1973c197bb59e6aabc647/msgpack-1.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8ce0b22b890be5d252de90d0e0d119f363012027cf256185fc3d474c44b1b9e", size = 378034 }, - { url = "https://files.pythonhosted.org/packages/32/d3/c152e0c55fead87dd948d4b29879b0f14feeeec92ef1fd2ec21b107c3f49/msgpack-1.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:73322a6cc57fcee3c0c57c4463d828e9428275fb85a27aa2aa1a92fdc42afd7b", size = 363070 }, - { url = "https://files.pythonhosted.org/packages/d9/2c/82e73506dd55f9e43ac8aa007c9dd088c6f0de2aa19e8f7330e6a65879fc/msgpack-1.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e1f3c3d21f7cf67bcf2da8e494d30a75e4cf60041d98b3f79875afb5b96f3a3f", size = 359863 }, - { url = "https://files.pythonhosted.org/packages/cb/a0/3d093b248837094220e1edc9ec4337de3443b1cfeeb6e0896af8ccc4cc7a/msgpack-1.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:64fc9068d701233effd61b19efb1485587560b66fe57b3e50d29c5d78e7fef68", size = 368166 }, - { url = "https://files.pythonhosted.org/packages/e4/13/7646f14f06838b406cf5a6ddbb7e8dc78b4996d891ab3b93c33d1ccc8678/msgpack-1.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:42f754515e0f683f9c79210a5d1cad631ec3d06cea5172214d2176a42e67e19b", size = 370105 }, - { url = "https://files.pythonhosted.org/packages/67/fa/dbbd2443e4578e165192dabbc6a22c0812cda2649261b1264ff515f19f15/msgpack-1.1.0-cp310-cp310-win32.whl", hash = "sha256:3df7e6b05571b3814361e8464f9304c42d2196808e0119f55d0d3e62cd5ea044", size = 68513 }, - { url = "https://files.pythonhosted.org/packages/24/ce/c2c8fbf0ded750cb63cbcbb61bc1f2dfd69e16dca30a8af8ba80ec182dcd/msgpack-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:685ec345eefc757a7c8af44a3032734a739f8c45d1b0ac45efc5d8977aa4720f", size = 74687 }, - { url = "https://files.pythonhosted.org/packages/b7/5e/a4c7154ba65d93be91f2f1e55f90e76c5f91ccadc7efc4341e6f04c8647f/msgpack-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3d364a55082fb2a7416f6c63ae383fbd903adb5a6cf78c5b96cc6316dc1cedc7", size = 150803 }, - { url = "https://files.pythonhosted.org/packages/60/c2/687684164698f1d51c41778c838d854965dd284a4b9d3a44beba9265c931/msgpack-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:79ec007767b9b56860e0372085f8504db5d06bd6a327a335449508bbee9648fa", size = 84343 }, - { url = "https://files.pythonhosted.org/packages/42/ae/d3adea9bb4a1342763556078b5765e666f8fdf242e00f3f6657380920972/msgpack-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6ad622bf7756d5a497d5b6836e7fc3752e2dd6f4c648e24b1803f6048596f701", size = 81408 }, - { url = "https://files.pythonhosted.org/packages/dc/17/6313325a6ff40ce9c3207293aee3ba50104aed6c2c1559d20d09e5c1ff54/msgpack-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e59bca908d9ca0de3dc8684f21ebf9a690fe47b6be93236eb40b99af28b6ea6", size = 396096 }, - { url = "https://files.pythonhosted.org/packages/a8/a1/ad7b84b91ab5a324e707f4c9761633e357820b011a01e34ce658c1dda7cc/msgpack-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e1da8f11a3dd397f0a32c76165cf0c4eb95b31013a94f6ecc0b280c05c91b59", size = 403671 }, - { url = "https://files.pythonhosted.org/packages/bb/0b/fd5b7c0b308bbf1831df0ca04ec76fe2f5bf6319833646b0a4bd5e9dc76d/msgpack-1.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:452aff037287acb1d70a804ffd022b21fa2bb7c46bee884dbc864cc9024128a0", size = 387414 }, - { url = "https://files.pythonhosted.org/packages/f0/03/ff8233b7c6e9929a1f5da3c7860eccd847e2523ca2de0d8ef4878d354cfa/msgpack-1.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8da4bf6d54ceed70e8861f833f83ce0814a2b72102e890cbdfe4b34764cdd66e", size = 383759 }, - { url = "https://files.pythonhosted.org/packages/1f/1b/eb82e1fed5a16dddd9bc75f0854b6e2fe86c0259c4353666d7fab37d39f4/msgpack-1.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:41c991beebf175faf352fb940bf2af9ad1fb77fd25f38d9142053914947cdbf6", size = 394405 }, - { url = "https://files.pythonhosted.org/packages/90/2e/962c6004e373d54ecf33d695fb1402f99b51832631e37c49273cc564ffc5/msgpack-1.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a52a1f3a5af7ba1c9ace055b659189f6c669cf3657095b50f9602af3a3ba0fe5", size = 396041 }, - { url = "https://files.pythonhosted.org/packages/f8/20/6e03342f629474414860c48aeffcc2f7f50ddaf351d95f20c3f1c67399a8/msgpack-1.1.0-cp311-cp311-win32.whl", hash = "sha256:58638690ebd0a06427c5fe1a227bb6b8b9fdc2bd07701bec13c2335c82131a88", size = 68538 }, - { url = "https://files.pythonhosted.org/packages/aa/c4/5a582fc9a87991a3e6f6800e9bb2f3c82972912235eb9539954f3e9997c7/msgpack-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fd2906780f25c8ed5d7b323379f6138524ba793428db5d0e9d226d3fa6aa1788", size = 74871 }, - { url = "https://files.pythonhosted.org/packages/e1/d6/716b7ca1dbde63290d2973d22bbef1b5032ca634c3ff4384a958ec3f093a/msgpack-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:d46cf9e3705ea9485687aa4001a76e44748b609d260af21c4ceea7f2212a501d", size = 152421 }, - { url = "https://files.pythonhosted.org/packages/70/da/5312b067f6773429cec2f8f08b021c06af416bba340c912c2ec778539ed6/msgpack-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5dbad74103df937e1325cc4bfeaf57713be0b4f15e1c2da43ccdd836393e2ea2", size = 85277 }, - { url = "https://files.pythonhosted.org/packages/28/51/da7f3ae4462e8bb98af0d5bdf2707f1b8c65a0d4f496e46b6afb06cbc286/msgpack-1.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:58dfc47f8b102da61e8949708b3eafc3504509a5728f8b4ddef84bd9e16ad420", size = 82222 }, - { url = "https://files.pythonhosted.org/packages/33/af/dc95c4b2a49cff17ce47611ca9ba218198806cad7796c0b01d1e332c86bb/msgpack-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4676e5be1b472909b2ee6356ff425ebedf5142427842aa06b4dfd5117d1ca8a2", size = 392971 }, - { url = "https://files.pythonhosted.org/packages/f1/54/65af8de681fa8255402c80eda2a501ba467921d5a7a028c9c22a2c2eedb5/msgpack-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17fb65dd0bec285907f68b15734a993ad3fc94332b5bb21b0435846228de1f39", size = 401403 }, - { url = "https://files.pythonhosted.org/packages/97/8c/e333690777bd33919ab7024269dc3c41c76ef5137b211d776fbb404bfead/msgpack-1.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a51abd48c6d8ac89e0cfd4fe177c61481aca2d5e7ba42044fd218cfd8ea9899f", size = 385356 }, - { url = "https://files.pythonhosted.org/packages/57/52/406795ba478dc1c890559dd4e89280fa86506608a28ccf3a72fbf45df9f5/msgpack-1.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2137773500afa5494a61b1208619e3871f75f27b03bcfca7b3a7023284140247", size = 383028 }, - { url = "https://files.pythonhosted.org/packages/e7/69/053b6549bf90a3acadcd8232eae03e2fefc87f066a5b9fbb37e2e608859f/msgpack-1.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:398b713459fea610861c8a7b62a6fec1882759f308ae0795b5413ff6a160cf3c", size = 391100 }, - { url = "https://files.pythonhosted.org/packages/23/f0/d4101d4da054f04274995ddc4086c2715d9b93111eb9ed49686c0f7ccc8a/msgpack-1.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:06f5fd2f6bb2a7914922d935d3b8bb4a7fff3a9a91cfce6d06c13bc42bec975b", size = 394254 }, - { url = "https://files.pythonhosted.org/packages/1c/12/cf07458f35d0d775ff3a2dc5559fa2e1fcd06c46f1ef510e594ebefdca01/msgpack-1.1.0-cp312-cp312-win32.whl", hash = "sha256:ad33e8400e4ec17ba782f7b9cf868977d867ed784a1f5f2ab46e7ba53b6e1e1b", size = 69085 }, - { url = "https://files.pythonhosted.org/packages/73/80/2708a4641f7d553a63bc934a3eb7214806b5b39d200133ca7f7afb0a53e8/msgpack-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:115a7af8ee9e8cddc10f87636767857e7e3717b7a2e97379dc2054712693e90f", size = 75347 }, - { url = "https://files.pythonhosted.org/packages/f7/3b/544a5c5886042b80e1f4847a4757af3430f60d106d8d43bb7be72c9e9650/msgpack-1.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:53258eeb7a80fc46f62fd59c876957a2d0e15e6449a9e71842b6d24419d88ca1", size = 150713 }, - { url = "https://files.pythonhosted.org/packages/93/af/d63f25bcccd3d6f06fd518ba4a321f34a4370c67b579ca5c70b4a37721b4/msgpack-1.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7e7b853bbc44fb03fbdba34feb4bd414322180135e2cb5164f20ce1c9795ee48", size = 84277 }, - { url = "https://files.pythonhosted.org/packages/92/9b/5c0dfb0009b9f96328664fecb9f8e4e9c8a1ae919e6d53986c1b813cb493/msgpack-1.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3e9b4936df53b970513eac1758f3882c88658a220b58dcc1e39606dccaaf01c", size = 81357 }, - { url = "https://files.pythonhosted.org/packages/d1/7c/3a9ee6ec9fc3e47681ad39b4d344ee04ff20a776b594fba92d88d8b68356/msgpack-1.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46c34e99110762a76e3911fc923222472c9d681f1094096ac4102c18319e6468", size = 371256 }, - { url = "https://files.pythonhosted.org/packages/f7/0a/8a213cecea7b731c540f25212ba5f9a818f358237ac51a44d448bd753690/msgpack-1.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a706d1e74dd3dea05cb54580d9bd8b2880e9264856ce5068027eed09680aa74", size = 377868 }, - { url = "https://files.pythonhosted.org/packages/1b/94/a82b0db0981e9586ed5af77d6cfb343da05d7437dceaae3b35d346498110/msgpack-1.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:534480ee5690ab3cbed89d4c8971a5c631b69a8c0883ecfea96c19118510c846", size = 363370 }, - { url = "https://files.pythonhosted.org/packages/93/fc/6c7f0dcc1c913e14861e16eaf494c07fc1dde454ec726ff8cebcf348ae53/msgpack-1.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8cf9e8c3a2153934a23ac160cc4cba0ec035f6867c8013cc6077a79823370346", size = 358970 }, - { url = "https://files.pythonhosted.org/packages/1f/c6/e4a04c0089deace870dabcdef5c9f12798f958e2e81d5012501edaff342f/msgpack-1.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3180065ec2abbe13a4ad37688b61b99d7f9e012a535b930e0e683ad6bc30155b", size = 366358 }, - { url = "https://files.pythonhosted.org/packages/b6/54/7d8317dac590cf16b3e08e3fb74d2081e5af44eb396f0effa13f17777f30/msgpack-1.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c5a91481a3cc573ac8c0d9aace09345d989dc4a0202b7fcb312c88c26d4e71a8", size = 370336 }, - { url = "https://files.pythonhosted.org/packages/dc/6f/a5a1f43b6566831e9630e5bc5d86034a8884386297302be128402555dde1/msgpack-1.1.0-cp39-cp39-win32.whl", hash = "sha256:f80bc7d47f76089633763f952e67f8214cb7b3ee6bfa489b3cb6a84cfac114cd", size = 68683 }, - { url = "https://files.pythonhosted.org/packages/5f/e8/2162621e18dbc36e2bc8492fd0e97b3975f5d89fe0472ae6d5f7fbdd8cf7/msgpack-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:4d1b7ff2d6146e16e8bd665ac726a89c74163ef8cd39fa8c1087d4e52d3a2325", size = 74787 }, +sdist = { url = "https://files.pythonhosted.org/packages/cb/d0/7555686ae7ff5731205df1012ede15dd9d927f6227ea151e901c7406af4f/msgpack-1.1.0.tar.gz", hash = "sha256:dd432ccc2c72b914e4cb77afce64aab761c1137cc698be3984eee260bcb2896e", size = 167260, upload-time = "2024-09-10T04:25:52.197Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/f9/a892a6038c861fa849b11a2bb0502c07bc698ab6ea53359e5771397d883b/msgpack-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7ad442d527a7e358a469faf43fda45aaf4ac3249c8310a82f0ccff9164e5dccd", size = 150428, upload-time = "2024-09-10T04:25:43.089Z" }, + { url = "https://files.pythonhosted.org/packages/df/7a/d174cc6a3b6bb85556e6a046d3193294a92f9a8e583cdbd46dc8a1d7e7f4/msgpack-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:74bed8f63f8f14d75eec75cf3d04ad581da6b914001b474a5d3cd3372c8cc27d", size = 84131, upload-time = "2024-09-10T04:25:30.22Z" }, + { url = "https://files.pythonhosted.org/packages/08/52/bf4fbf72f897a23a56b822997a72c16de07d8d56d7bf273242f884055682/msgpack-1.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:914571a2a5b4e7606997e169f64ce53a8b1e06f2cf2c3a7273aa106236d43dd5", size = 81215, upload-time = "2024-09-10T04:24:54.329Z" }, + { url = "https://files.pythonhosted.org/packages/02/95/dc0044b439b518236aaf012da4677c1b8183ce388411ad1b1e63c32d8979/msgpack-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c921af52214dcbb75e6bdf6a661b23c3e6417f00c603dd2070bccb5c3ef499f5", size = 371229, upload-time = "2024-09-10T04:25:50.907Z" }, + { url = "https://files.pythonhosted.org/packages/ff/75/09081792db60470bef19d9c2be89f024d366b1e1973c197bb59e6aabc647/msgpack-1.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8ce0b22b890be5d252de90d0e0d119f363012027cf256185fc3d474c44b1b9e", size = 378034, upload-time = "2024-09-10T04:25:22.097Z" }, + { url = "https://files.pythonhosted.org/packages/32/d3/c152e0c55fead87dd948d4b29879b0f14feeeec92ef1fd2ec21b107c3f49/msgpack-1.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:73322a6cc57fcee3c0c57c4463d828e9428275fb85a27aa2aa1a92fdc42afd7b", size = 363070, upload-time = "2024-09-10T04:24:43.957Z" }, + { url = "https://files.pythonhosted.org/packages/d9/2c/82e73506dd55f9e43ac8aa007c9dd088c6f0de2aa19e8f7330e6a65879fc/msgpack-1.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e1f3c3d21f7cf67bcf2da8e494d30a75e4cf60041d98b3f79875afb5b96f3a3f", size = 359863, upload-time = "2024-09-10T04:24:51.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/a0/3d093b248837094220e1edc9ec4337de3443b1cfeeb6e0896af8ccc4cc7a/msgpack-1.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:64fc9068d701233effd61b19efb1485587560b66fe57b3e50d29c5d78e7fef68", size = 368166, upload-time = "2024-09-10T04:24:19.907Z" }, + { url = "https://files.pythonhosted.org/packages/e4/13/7646f14f06838b406cf5a6ddbb7e8dc78b4996d891ab3b93c33d1ccc8678/msgpack-1.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:42f754515e0f683f9c79210a5d1cad631ec3d06cea5172214d2176a42e67e19b", size = 370105, upload-time = "2024-09-10T04:25:35.141Z" }, + { url = "https://files.pythonhosted.org/packages/67/fa/dbbd2443e4578e165192dabbc6a22c0812cda2649261b1264ff515f19f15/msgpack-1.1.0-cp310-cp310-win32.whl", hash = "sha256:3df7e6b05571b3814361e8464f9304c42d2196808e0119f55d0d3e62cd5ea044", size = 68513, upload-time = "2024-09-10T04:24:36.099Z" }, + { url = "https://files.pythonhosted.org/packages/24/ce/c2c8fbf0ded750cb63cbcbb61bc1f2dfd69e16dca30a8af8ba80ec182dcd/msgpack-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:685ec345eefc757a7c8af44a3032734a739f8c45d1b0ac45efc5d8977aa4720f", size = 74687, upload-time = "2024-09-10T04:24:23.394Z" }, + { url = "https://files.pythonhosted.org/packages/b7/5e/a4c7154ba65d93be91f2f1e55f90e76c5f91ccadc7efc4341e6f04c8647f/msgpack-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3d364a55082fb2a7416f6c63ae383fbd903adb5a6cf78c5b96cc6316dc1cedc7", size = 150803, upload-time = "2024-09-10T04:24:40.911Z" }, + { url = "https://files.pythonhosted.org/packages/60/c2/687684164698f1d51c41778c838d854965dd284a4b9d3a44beba9265c931/msgpack-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:79ec007767b9b56860e0372085f8504db5d06bd6a327a335449508bbee9648fa", size = 84343, upload-time = "2024-09-10T04:24:50.283Z" }, + { url = "https://files.pythonhosted.org/packages/42/ae/d3adea9bb4a1342763556078b5765e666f8fdf242e00f3f6657380920972/msgpack-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6ad622bf7756d5a497d5b6836e7fc3752e2dd6f4c648e24b1803f6048596f701", size = 81408, upload-time = "2024-09-10T04:25:12.774Z" }, + { url = "https://files.pythonhosted.org/packages/dc/17/6313325a6ff40ce9c3207293aee3ba50104aed6c2c1559d20d09e5c1ff54/msgpack-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e59bca908d9ca0de3dc8684f21ebf9a690fe47b6be93236eb40b99af28b6ea6", size = 396096, upload-time = "2024-09-10T04:24:37.245Z" }, + { url = "https://files.pythonhosted.org/packages/a8/a1/ad7b84b91ab5a324e707f4c9761633e357820b011a01e34ce658c1dda7cc/msgpack-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e1da8f11a3dd397f0a32c76165cf0c4eb95b31013a94f6ecc0b280c05c91b59", size = 403671, upload-time = "2024-09-10T04:25:10.201Z" }, + { url = "https://files.pythonhosted.org/packages/bb/0b/fd5b7c0b308bbf1831df0ca04ec76fe2f5bf6319833646b0a4bd5e9dc76d/msgpack-1.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:452aff037287acb1d70a804ffd022b21fa2bb7c46bee884dbc864cc9024128a0", size = 387414, upload-time = "2024-09-10T04:25:27.552Z" }, + { url = "https://files.pythonhosted.org/packages/f0/03/ff8233b7c6e9929a1f5da3c7860eccd847e2523ca2de0d8ef4878d354cfa/msgpack-1.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8da4bf6d54ceed70e8861f833f83ce0814a2b72102e890cbdfe4b34764cdd66e", size = 383759, upload-time = "2024-09-10T04:25:03.366Z" }, + { url = "https://files.pythonhosted.org/packages/1f/1b/eb82e1fed5a16dddd9bc75f0854b6e2fe86c0259c4353666d7fab37d39f4/msgpack-1.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:41c991beebf175faf352fb940bf2af9ad1fb77fd25f38d9142053914947cdbf6", size = 394405, upload-time = "2024-09-10T04:25:07.348Z" }, + { url = "https://files.pythonhosted.org/packages/90/2e/962c6004e373d54ecf33d695fb1402f99b51832631e37c49273cc564ffc5/msgpack-1.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a52a1f3a5af7ba1c9ace055b659189f6c669cf3657095b50f9602af3a3ba0fe5", size = 396041, upload-time = "2024-09-10T04:25:48.311Z" }, + { url = "https://files.pythonhosted.org/packages/f8/20/6e03342f629474414860c48aeffcc2f7f50ddaf351d95f20c3f1c67399a8/msgpack-1.1.0-cp311-cp311-win32.whl", hash = "sha256:58638690ebd0a06427c5fe1a227bb6b8b9fdc2bd07701bec13c2335c82131a88", size = 68538, upload-time = "2024-09-10T04:24:29.953Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c4/5a582fc9a87991a3e6f6800e9bb2f3c82972912235eb9539954f3e9997c7/msgpack-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fd2906780f25c8ed5d7b323379f6138524ba793428db5d0e9d226d3fa6aa1788", size = 74871, upload-time = "2024-09-10T04:25:44.823Z" }, + { url = "https://files.pythonhosted.org/packages/e1/d6/716b7ca1dbde63290d2973d22bbef1b5032ca634c3ff4384a958ec3f093a/msgpack-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:d46cf9e3705ea9485687aa4001a76e44748b609d260af21c4ceea7f2212a501d", size = 152421, upload-time = "2024-09-10T04:25:49.63Z" }, + { url = "https://files.pythonhosted.org/packages/70/da/5312b067f6773429cec2f8f08b021c06af416bba340c912c2ec778539ed6/msgpack-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5dbad74103df937e1325cc4bfeaf57713be0b4f15e1c2da43ccdd836393e2ea2", size = 85277, upload-time = "2024-09-10T04:24:48.562Z" }, + { url = "https://files.pythonhosted.org/packages/28/51/da7f3ae4462e8bb98af0d5bdf2707f1b8c65a0d4f496e46b6afb06cbc286/msgpack-1.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:58dfc47f8b102da61e8949708b3eafc3504509a5728f8b4ddef84bd9e16ad420", size = 82222, upload-time = "2024-09-10T04:25:36.49Z" }, + { url = "https://files.pythonhosted.org/packages/33/af/dc95c4b2a49cff17ce47611ca9ba218198806cad7796c0b01d1e332c86bb/msgpack-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4676e5be1b472909b2ee6356ff425ebedf5142427842aa06b4dfd5117d1ca8a2", size = 392971, upload-time = "2024-09-10T04:24:58.129Z" }, + { url = "https://files.pythonhosted.org/packages/f1/54/65af8de681fa8255402c80eda2a501ba467921d5a7a028c9c22a2c2eedb5/msgpack-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17fb65dd0bec285907f68b15734a993ad3fc94332b5bb21b0435846228de1f39", size = 401403, upload-time = "2024-09-10T04:25:40.428Z" }, + { url = "https://files.pythonhosted.org/packages/97/8c/e333690777bd33919ab7024269dc3c41c76ef5137b211d776fbb404bfead/msgpack-1.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a51abd48c6d8ac89e0cfd4fe177c61481aca2d5e7ba42044fd218cfd8ea9899f", size = 385356, upload-time = "2024-09-10T04:25:31.406Z" }, + { url = "https://files.pythonhosted.org/packages/57/52/406795ba478dc1c890559dd4e89280fa86506608a28ccf3a72fbf45df9f5/msgpack-1.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2137773500afa5494a61b1208619e3871f75f27b03bcfca7b3a7023284140247", size = 383028, upload-time = "2024-09-10T04:25:17.08Z" }, + { url = "https://files.pythonhosted.org/packages/e7/69/053b6549bf90a3acadcd8232eae03e2fefc87f066a5b9fbb37e2e608859f/msgpack-1.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:398b713459fea610861c8a7b62a6fec1882759f308ae0795b5413ff6a160cf3c", size = 391100, upload-time = "2024-09-10T04:25:08.993Z" }, + { url = "https://files.pythonhosted.org/packages/23/f0/d4101d4da054f04274995ddc4086c2715d9b93111eb9ed49686c0f7ccc8a/msgpack-1.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:06f5fd2f6bb2a7914922d935d3b8bb4a7fff3a9a91cfce6d06c13bc42bec975b", size = 394254, upload-time = "2024-09-10T04:25:06.048Z" }, + { url = "https://files.pythonhosted.org/packages/1c/12/cf07458f35d0d775ff3a2dc5559fa2e1fcd06c46f1ef510e594ebefdca01/msgpack-1.1.0-cp312-cp312-win32.whl", hash = "sha256:ad33e8400e4ec17ba782f7b9cf868977d867ed784a1f5f2ab46e7ba53b6e1e1b", size = 69085, upload-time = "2024-09-10T04:25:01.494Z" }, + { url = "https://files.pythonhosted.org/packages/73/80/2708a4641f7d553a63bc934a3eb7214806b5b39d200133ca7f7afb0a53e8/msgpack-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:115a7af8ee9e8cddc10f87636767857e7e3717b7a2e97379dc2054712693e90f", size = 75347, upload-time = "2024-09-10T04:25:33.106Z" }, + { url = "https://files.pythonhosted.org/packages/f7/3b/544a5c5886042b80e1f4847a4757af3430f60d106d8d43bb7be72c9e9650/msgpack-1.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:53258eeb7a80fc46f62fd59c876957a2d0e15e6449a9e71842b6d24419d88ca1", size = 150713, upload-time = "2024-09-10T04:25:23.397Z" }, + { url = "https://files.pythonhosted.org/packages/93/af/d63f25bcccd3d6f06fd518ba4a321f34a4370c67b579ca5c70b4a37721b4/msgpack-1.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7e7b853bbc44fb03fbdba34feb4bd414322180135e2cb5164f20ce1c9795ee48", size = 84277, upload-time = "2024-09-10T04:24:34.656Z" }, + { url = "https://files.pythonhosted.org/packages/92/9b/5c0dfb0009b9f96328664fecb9f8e4e9c8a1ae919e6d53986c1b813cb493/msgpack-1.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3e9b4936df53b970513eac1758f3882c88658a220b58dcc1e39606dccaaf01c", size = 81357, upload-time = "2024-09-10T04:24:56.603Z" }, + { url = "https://files.pythonhosted.org/packages/d1/7c/3a9ee6ec9fc3e47681ad39b4d344ee04ff20a776b594fba92d88d8b68356/msgpack-1.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46c34e99110762a76e3911fc923222472c9d681f1094096ac4102c18319e6468", size = 371256, upload-time = "2024-09-10T04:25:11.473Z" }, + { url = "https://files.pythonhosted.org/packages/f7/0a/8a213cecea7b731c540f25212ba5f9a818f358237ac51a44d448bd753690/msgpack-1.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a706d1e74dd3dea05cb54580d9bd8b2880e9264856ce5068027eed09680aa74", size = 377868, upload-time = "2024-09-10T04:25:24.535Z" }, + { url = "https://files.pythonhosted.org/packages/1b/94/a82b0db0981e9586ed5af77d6cfb343da05d7437dceaae3b35d346498110/msgpack-1.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:534480ee5690ab3cbed89d4c8971a5c631b69a8c0883ecfea96c19118510c846", size = 363370, upload-time = "2024-09-10T04:24:21.812Z" }, + { url = "https://files.pythonhosted.org/packages/93/fc/6c7f0dcc1c913e14861e16eaf494c07fc1dde454ec726ff8cebcf348ae53/msgpack-1.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8cf9e8c3a2153934a23ac160cc4cba0ec035f6867c8013cc6077a79823370346", size = 358970, upload-time = "2024-09-10T04:24:24.741Z" }, + { url = "https://files.pythonhosted.org/packages/1f/c6/e4a04c0089deace870dabcdef5c9f12798f958e2e81d5012501edaff342f/msgpack-1.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3180065ec2abbe13a4ad37688b61b99d7f9e012a535b930e0e683ad6bc30155b", size = 366358, upload-time = "2024-09-10T04:25:45.955Z" }, + { url = "https://files.pythonhosted.org/packages/b6/54/7d8317dac590cf16b3e08e3fb74d2081e5af44eb396f0effa13f17777f30/msgpack-1.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c5a91481a3cc573ac8c0d9aace09345d989dc4a0202b7fcb312c88c26d4e71a8", size = 370336, upload-time = "2024-09-10T04:24:26.918Z" }, + { url = "https://files.pythonhosted.org/packages/dc/6f/a5a1f43b6566831e9630e5bc5d86034a8884386297302be128402555dde1/msgpack-1.1.0-cp39-cp39-win32.whl", hash = "sha256:f80bc7d47f76089633763f952e67f8214cb7b3ee6bfa489b3cb6a84cfac114cd", size = 68683, upload-time = "2024-09-10T04:24:32.984Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e8/2162621e18dbc36e2bc8492fd0e97b3975f5d89fe0472ae6d5f7fbdd8cf7/msgpack-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:4d1b7ff2d6146e16e8bd665ac726a89c74163ef8cd39fa8c1087d4e52d3a2325", size = 74787, upload-time = "2024-09-10T04:25:14.524Z" }, ] [[package]] name = "mypy-extensions" version = "1.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 } +sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433, upload-time = "2023-02-04T12:11:27.157Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 }, + { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695, upload-time = "2023-02-04T12:11:25.002Z" }, ] [[package]] @@ -1025,31 +1056,31 @@ resolution-markers = [ "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", ] dependencies = [ - { name = "llvmlite", version = "0.43.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "numpy", marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3c/93/2849300a9184775ba274aba6f82f303343669b0592b7bb0849ea713dabb0/numba-0.60.0.tar.gz", hash = "sha256:5df6158e5584eece5fc83294b949fd30b9f1125df7708862205217e068aabf16", size = 2702171 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/cf/baa13a7e3556d73d9e38021e6d6aa4aeb30d8b94545aa8b70d0f24a1ccc4/numba-0.60.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d761de835cd38fb400d2c26bb103a2726f548dc30368853121d66201672e651", size = 2647627 }, - { url = "https://files.pythonhosted.org/packages/ac/ba/4b57fa498564457c3cc9fc9e570a6b08e6086c74220f24baaf04e54b995f/numba-0.60.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:159e618ef213fba758837f9837fb402bbe65326e60ba0633dbe6c7f274d42c1b", size = 2650322 }, - { url = "https://files.pythonhosted.org/packages/28/98/7ea97ee75870a54f938a8c70f7e0be4495ba5349c5f9db09d467c4a5d5b7/numba-0.60.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1527dc578b95c7c4ff248792ec33d097ba6bef9eda466c948b68dfc995c25781", size = 3407390 }, - { url = "https://files.pythonhosted.org/packages/79/58/cb4ac5b8f7ec64200460aef1fed88258fb872ceef504ab1f989d2ff0f684/numba-0.60.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe0b28abb8d70f8160798f4de9d486143200f34458d34c4a214114e445d7124e", size = 3699694 }, - { url = "https://files.pythonhosted.org/packages/1c/b0/c61a93ca947d12233ff45de506ddbf52af3f752066a0b8be4d27426e16da/numba-0.60.0-cp310-cp310-win_amd64.whl", hash = "sha256:19407ced081d7e2e4b8d8c36aa57b7452e0283871c296e12d798852bc7d7f198", size = 2687030 }, - { url = "https://files.pythonhosted.org/packages/98/ad/df18d492a8f00d29a30db307904b9b296e37507034eedb523876f3a2e13e/numba-0.60.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a17b70fc9e380ee29c42717e8cc0bfaa5556c416d94f9aa96ba13acb41bdece8", size = 2647254 }, - { url = "https://files.pythonhosted.org/packages/9a/51/a4dc2c01ce7a850b8e56ff6d5381d047a5daea83d12bad08aa071d34b2ee/numba-0.60.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3fb02b344a2a80efa6f677aa5c40cd5dd452e1b35f8d1c2af0dfd9ada9978e4b", size = 2649970 }, - { url = "https://files.pythonhosted.org/packages/f9/4c/8889ac94c0b33dca80bed11564b8c6d9ea14d7f094e674c58e5c5b05859b/numba-0.60.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5f4fde652ea604ea3c86508a3fb31556a6157b2c76c8b51b1d45eb40c8598703", size = 3412492 }, - { url = "https://files.pythonhosted.org/packages/57/03/2b4245b05b71c0cee667e6a0b51606dfa7f4157c9093d71c6b208385a611/numba-0.60.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4142d7ac0210cc86432b818338a2bc368dc773a2f5cf1e32ff7c5b378bd63ee8", size = 3705018 }, - { url = "https://files.pythonhosted.org/packages/79/89/2d924ca60dbf949f18a6fec223a2445f5f428d9a5f97a6b29c2122319015/numba-0.60.0-cp311-cp311-win_amd64.whl", hash = "sha256:cac02c041e9b5bc8cf8f2034ff6f0dbafccd1ae9590dc146b3a02a45e53af4e2", size = 2686920 }, - { url = "https://files.pythonhosted.org/packages/eb/5c/b5ec752c475e78a6c3676b67c514220dbde2725896bbb0b6ec6ea54b2738/numba-0.60.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d7da4098db31182fc5ffe4bc42c6f24cd7d1cb8a14b59fd755bfee32e34b8404", size = 2647866 }, - { url = "https://files.pythonhosted.org/packages/65/42/39559664b2e7c15689a638c2a38b3b74c6e69a04e2b3019b9f7742479188/numba-0.60.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:38d6ea4c1f56417076ecf8fc327c831ae793282e0ff51080c5094cb726507b1c", size = 2650208 }, - { url = "https://files.pythonhosted.org/packages/67/88/c4459ccc05674ef02119abf2888ccd3e2fed12a323f52255f4982fc95876/numba-0.60.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:62908d29fb6a3229c242e981ca27e32a6e606cc253fc9e8faeb0e48760de241e", size = 3466946 }, - { url = "https://files.pythonhosted.org/packages/8b/41/ac11cf33524def12aa5bd698226ae196a1185831c05ed29dc0c56eaa308b/numba-0.60.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0ebaa91538e996f708f1ab30ef4d3ddc344b64b5227b67a57aa74f401bb68b9d", size = 3761463 }, - { url = "https://files.pythonhosted.org/packages/ca/bd/0fe29fcd1b6a8de479a4ed25c6e56470e467e3611c079d55869ceef2b6d1/numba-0.60.0-cp312-cp312-win_amd64.whl", hash = "sha256:f75262e8fe7fa96db1dca93d53a194a38c46da28b112b8a4aca168f0df860347", size = 2707588 }, - { url = "https://files.pythonhosted.org/packages/68/1a/87c53f836cdf557083248c3f47212271f220280ff766538795e77c8c6bbf/numba-0.60.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:01ef4cd7d83abe087d644eaa3d95831b777aa21d441a23703d649e06b8e06b74", size = 2647186 }, - { url = "https://files.pythonhosted.org/packages/28/14/a5baa1f2edea7b49afa4dc1bb1b126645198cf1075186853b5b497be826e/numba-0.60.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:819a3dfd4630d95fd574036f99e47212a1af41cbcb019bf8afac63ff56834449", size = 2650038 }, - { url = "https://files.pythonhosted.org/packages/3b/bd/f1985719ff34e37e07bb18f9d3acd17e5a21da255f550c8eae031e2ddf5f/numba-0.60.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0b983bd6ad82fe868493012487f34eae8bf7dd94654951404114f23c3466d34b", size = 3403010 }, - { url = "https://files.pythonhosted.org/packages/54/9b/cd73d3f6617ddc8398a63ef97d8dc9139a9879b9ca8a7ca4b8789056ea46/numba-0.60.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c151748cd269ddeab66334bd754817ffc0cabd9433acb0f551697e5151917d25", size = 3695086 }, - { url = "https://files.pythonhosted.org/packages/01/01/8b7b670c77c5ea0e47e283d82332969bf672ab6410d0b2610cac5b7a3ded/numba-0.60.0-cp39-cp39-win_amd64.whl", hash = "sha256:3031547a015710140e8c87226b4cfe927cac199835e5bf7d4fe5cb64e814e3ab", size = 2686978 }, + { name = "llvmlite", version = "0.43.0", source = { registry = "https://pypi.org/simple" } }, + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3c/93/2849300a9184775ba274aba6f82f303343669b0592b7bb0849ea713dabb0/numba-0.60.0.tar.gz", hash = "sha256:5df6158e5584eece5fc83294b949fd30b9f1125df7708862205217e068aabf16", size = 2702171, upload-time = "2024-06-13T18:11:19.869Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/cf/baa13a7e3556d73d9e38021e6d6aa4aeb30d8b94545aa8b70d0f24a1ccc4/numba-0.60.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d761de835cd38fb400d2c26bb103a2726f548dc30368853121d66201672e651", size = 2647627, upload-time = "2024-06-13T18:10:29.857Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ba/4b57fa498564457c3cc9fc9e570a6b08e6086c74220f24baaf04e54b995f/numba-0.60.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:159e618ef213fba758837f9837fb402bbe65326e60ba0633dbe6c7f274d42c1b", size = 2650322, upload-time = "2024-06-13T18:10:32.849Z" }, + { url = "https://files.pythonhosted.org/packages/28/98/7ea97ee75870a54f938a8c70f7e0be4495ba5349c5f9db09d467c4a5d5b7/numba-0.60.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1527dc578b95c7c4ff248792ec33d097ba6bef9eda466c948b68dfc995c25781", size = 3407390, upload-time = "2024-06-13T18:10:34.741Z" }, + { url = "https://files.pythonhosted.org/packages/79/58/cb4ac5b8f7ec64200460aef1fed88258fb872ceef504ab1f989d2ff0f684/numba-0.60.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe0b28abb8d70f8160798f4de9d486143200f34458d34c4a214114e445d7124e", size = 3699694, upload-time = "2024-06-13T18:10:37.295Z" }, + { url = "https://files.pythonhosted.org/packages/1c/b0/c61a93ca947d12233ff45de506ddbf52af3f752066a0b8be4d27426e16da/numba-0.60.0-cp310-cp310-win_amd64.whl", hash = "sha256:19407ced081d7e2e4b8d8c36aa57b7452e0283871c296e12d798852bc7d7f198", size = 2687030, upload-time = "2024-06-13T18:10:39.47Z" }, + { url = "https://files.pythonhosted.org/packages/98/ad/df18d492a8f00d29a30db307904b9b296e37507034eedb523876f3a2e13e/numba-0.60.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a17b70fc9e380ee29c42717e8cc0bfaa5556c416d94f9aa96ba13acb41bdece8", size = 2647254, upload-time = "2024-06-13T18:10:41.69Z" }, + { url = "https://files.pythonhosted.org/packages/9a/51/a4dc2c01ce7a850b8e56ff6d5381d047a5daea83d12bad08aa071d34b2ee/numba-0.60.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3fb02b344a2a80efa6f677aa5c40cd5dd452e1b35f8d1c2af0dfd9ada9978e4b", size = 2649970, upload-time = "2024-06-13T18:10:44.682Z" }, + { url = "https://files.pythonhosted.org/packages/f9/4c/8889ac94c0b33dca80bed11564b8c6d9ea14d7f094e674c58e5c5b05859b/numba-0.60.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5f4fde652ea604ea3c86508a3fb31556a6157b2c76c8b51b1d45eb40c8598703", size = 3412492, upload-time = "2024-06-13T18:10:47.1Z" }, + { url = "https://files.pythonhosted.org/packages/57/03/2b4245b05b71c0cee667e6a0b51606dfa7f4157c9093d71c6b208385a611/numba-0.60.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4142d7ac0210cc86432b818338a2bc368dc773a2f5cf1e32ff7c5b378bd63ee8", size = 3705018, upload-time = "2024-06-13T18:10:49.539Z" }, + { url = "https://files.pythonhosted.org/packages/79/89/2d924ca60dbf949f18a6fec223a2445f5f428d9a5f97a6b29c2122319015/numba-0.60.0-cp311-cp311-win_amd64.whl", hash = "sha256:cac02c041e9b5bc8cf8f2034ff6f0dbafccd1ae9590dc146b3a02a45e53af4e2", size = 2686920, upload-time = "2024-06-13T18:10:51.937Z" }, + { url = "https://files.pythonhosted.org/packages/eb/5c/b5ec752c475e78a6c3676b67c514220dbde2725896bbb0b6ec6ea54b2738/numba-0.60.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d7da4098db31182fc5ffe4bc42c6f24cd7d1cb8a14b59fd755bfee32e34b8404", size = 2647866, upload-time = "2024-06-13T18:10:54.453Z" }, + { url = "https://files.pythonhosted.org/packages/65/42/39559664b2e7c15689a638c2a38b3b74c6e69a04e2b3019b9f7742479188/numba-0.60.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:38d6ea4c1f56417076ecf8fc327c831ae793282e0ff51080c5094cb726507b1c", size = 2650208, upload-time = "2024-06-13T18:10:56.779Z" }, + { url = "https://files.pythonhosted.org/packages/67/88/c4459ccc05674ef02119abf2888ccd3e2fed12a323f52255f4982fc95876/numba-0.60.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:62908d29fb6a3229c242e981ca27e32a6e606cc253fc9e8faeb0e48760de241e", size = 3466946, upload-time = "2024-06-13T18:10:58.961Z" }, + { url = "https://files.pythonhosted.org/packages/8b/41/ac11cf33524def12aa5bd698226ae196a1185831c05ed29dc0c56eaa308b/numba-0.60.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0ebaa91538e996f708f1ab30ef4d3ddc344b64b5227b67a57aa74f401bb68b9d", size = 3761463, upload-time = "2024-06-13T18:11:01.657Z" }, + { url = "https://files.pythonhosted.org/packages/ca/bd/0fe29fcd1b6a8de479a4ed25c6e56470e467e3611c079d55869ceef2b6d1/numba-0.60.0-cp312-cp312-win_amd64.whl", hash = "sha256:f75262e8fe7fa96db1dca93d53a194a38c46da28b112b8a4aca168f0df860347", size = 2707588, upload-time = "2024-06-13T18:11:04.261Z" }, + { url = "https://files.pythonhosted.org/packages/68/1a/87c53f836cdf557083248c3f47212271f220280ff766538795e77c8c6bbf/numba-0.60.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:01ef4cd7d83abe087d644eaa3d95831b777aa21d441a23703d649e06b8e06b74", size = 2647186, upload-time = "2024-06-13T18:11:06.753Z" }, + { url = "https://files.pythonhosted.org/packages/28/14/a5baa1f2edea7b49afa4dc1bb1b126645198cf1075186853b5b497be826e/numba-0.60.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:819a3dfd4630d95fd574036f99e47212a1af41cbcb019bf8afac63ff56834449", size = 2650038, upload-time = "2024-06-13T18:11:10.869Z" }, + { url = "https://files.pythonhosted.org/packages/3b/bd/f1985719ff34e37e07bb18f9d3acd17e5a21da255f550c8eae031e2ddf5f/numba-0.60.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0b983bd6ad82fe868493012487f34eae8bf7dd94654951404114f23c3466d34b", size = 3403010, upload-time = "2024-06-13T18:11:13.057Z" }, + { url = "https://files.pythonhosted.org/packages/54/9b/cd73d3f6617ddc8398a63ef97d8dc9139a9879b9ca8a7ca4b8789056ea46/numba-0.60.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c151748cd269ddeab66334bd754817ffc0cabd9433acb0f551697e5151917d25", size = 3695086, upload-time = "2024-06-13T18:11:15.497Z" }, + { url = "https://files.pythonhosted.org/packages/01/01/8b7b670c77c5ea0e47e283d82332969bf672ab6410d0b2610cac5b7a3ded/numba-0.60.0-cp39-cp39-win_amd64.whl", hash = "sha256:3031547a015710140e8c87226b4cfe927cac199835e5bf7d4fe5cb64e814e3ab", size = 2686978, upload-time = "2024-06-13T18:11:17.765Z" }, ] [[package]] @@ -1065,69 +1096,69 @@ resolution-markers = [ "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", ] dependencies = [ - { name = "llvmlite", version = "0.44.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "numpy", marker = "python_full_version >= '3.10'" }, + { name = "llvmlite", version = "0.44.0", source = { registry = "https://pypi.org/simple" } }, + { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1c/a0/e21f57604304aa03ebb8e098429222722ad99176a4f979d34af1d1ee80da/numba-0.61.2.tar.gz", hash = "sha256:8750ee147940a6637b80ecf7f95062185ad8726c8c28a2295b8ec1160a196f7d", size = 2820615 } +sdist = { url = "https://files.pythonhosted.org/packages/1c/a0/e21f57604304aa03ebb8e098429222722ad99176a4f979d34af1d1ee80da/numba-0.61.2.tar.gz", hash = "sha256:8750ee147940a6637b80ecf7f95062185ad8726c8c28a2295b8ec1160a196f7d", size = 2820615, upload-time = "2025-04-09T02:58:07.659Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/ca/f470be59552ccbf9531d2d383b67ae0b9b524d435fb4a0d229fef135116e/numba-0.61.2-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:cf9f9fc00d6eca0c23fc840817ce9f439b9f03c8f03d6246c0e7f0cb15b7162a", size = 2775663 }, - { url = "https://files.pythonhosted.org/packages/f5/13/3bdf52609c80d460a3b4acfb9fdb3817e392875c0d6270cf3fd9546f138b/numba-0.61.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ea0247617edcb5dd61f6106a56255baab031acc4257bddaeddb3a1003b4ca3fd", size = 2778344 }, - { url = "https://files.pythonhosted.org/packages/e2/7d/bfb2805bcfbd479f04f835241ecf28519f6e3609912e3a985aed45e21370/numba-0.61.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ae8c7a522c26215d5f62ebec436e3d341f7f590079245a2f1008dfd498cc1642", size = 3824054 }, - { url = "https://files.pythonhosted.org/packages/e3/27/797b2004745c92955470c73c82f0e300cf033c791f45bdecb4b33b12bdea/numba-0.61.2-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:bd1e74609855aa43661edffca37346e4e8462f6903889917e9f41db40907daa2", size = 3518531 }, - { url = "https://files.pythonhosted.org/packages/b1/c6/c2fb11e50482cb310afae87a997707f6c7d8a48967b9696271347441f650/numba-0.61.2-cp310-cp310-win_amd64.whl", hash = "sha256:ae45830b129c6137294093b269ef0a22998ccc27bf7cf096ab8dcf7bca8946f9", size = 2831612 }, - { url = "https://files.pythonhosted.org/packages/3f/97/c99d1056aed767503c228f7099dc11c402906b42a4757fec2819329abb98/numba-0.61.2-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:efd3db391df53aaa5cfbee189b6c910a5b471488749fd6606c3f33fc984c2ae2", size = 2775825 }, - { url = "https://files.pythonhosted.org/packages/95/9e/63c549f37136e892f006260c3e2613d09d5120672378191f2dc387ba65a2/numba-0.61.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:49c980e4171948ffebf6b9a2520ea81feed113c1f4890747ba7f59e74be84b1b", size = 2778695 }, - { url = "https://files.pythonhosted.org/packages/97/c8/8740616c8436c86c1b9a62e72cb891177d2c34c2d24ddcde4c390371bf4c/numba-0.61.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3945615cd73c2c7eba2a85ccc9c1730c21cd3958bfcf5a44302abae0fb07bb60", size = 3829227 }, - { url = "https://files.pythonhosted.org/packages/fc/06/66e99ae06507c31d15ff3ecd1f108f2f59e18b6e08662cd5f8a5853fbd18/numba-0.61.2-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:bbfdf4eca202cebade0b7d43896978e146f39398909a42941c9303f82f403a18", size = 3523422 }, - { url = "https://files.pythonhosted.org/packages/0f/a4/2b309a6a9f6d4d8cfba583401c7c2f9ff887adb5d54d8e2e130274c0973f/numba-0.61.2-cp311-cp311-win_amd64.whl", hash = "sha256:76bcec9f46259cedf888041b9886e257ae101c6268261b19fda8cfbc52bec9d1", size = 2831505 }, - { url = "https://files.pythonhosted.org/packages/b4/a0/c6b7b9c615cfa3b98c4c63f4316e3f6b3bbe2387740277006551784218cd/numba-0.61.2-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:34fba9406078bac7ab052efbf0d13939426c753ad72946baaa5bf9ae0ebb8dd2", size = 2776626 }, - { url = "https://files.pythonhosted.org/packages/92/4a/fe4e3c2ecad72d88f5f8cd04e7f7cff49e718398a2fac02d2947480a00ca/numba-0.61.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4ddce10009bc097b080fc96876d14c051cc0c7679e99de3e0af59014dab7dfe8", size = 2779287 }, - { url = "https://files.pythonhosted.org/packages/9a/2d/e518df036feab381c23a624dac47f8445ac55686ec7f11083655eb707da3/numba-0.61.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5b1bb509d01f23d70325d3a5a0e237cbc9544dd50e50588bc581ba860c213546", size = 3885928 }, - { url = "https://files.pythonhosted.org/packages/10/0f/23cced68ead67b75d77cfcca3df4991d1855c897ee0ff3fe25a56ed82108/numba-0.61.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:48a53a3de8f8793526cbe330f2a39fe9a6638efcbf11bd63f3d2f9757ae345cd", size = 3577115 }, - { url = "https://files.pythonhosted.org/packages/68/1d/ddb3e704c5a8fb90142bf9dc195c27db02a08a99f037395503bfbc1d14b3/numba-0.61.2-cp312-cp312-win_amd64.whl", hash = "sha256:97cf4f12c728cf77c9c1d7c23707e4d8fb4632b46275f8f3397de33e5877af18", size = 2831929 }, + { url = "https://files.pythonhosted.org/packages/eb/ca/f470be59552ccbf9531d2d383b67ae0b9b524d435fb4a0d229fef135116e/numba-0.61.2-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:cf9f9fc00d6eca0c23fc840817ce9f439b9f03c8f03d6246c0e7f0cb15b7162a", size = 2775663, upload-time = "2025-04-09T02:57:34.143Z" }, + { url = "https://files.pythonhosted.org/packages/f5/13/3bdf52609c80d460a3b4acfb9fdb3817e392875c0d6270cf3fd9546f138b/numba-0.61.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ea0247617edcb5dd61f6106a56255baab031acc4257bddaeddb3a1003b4ca3fd", size = 2778344, upload-time = "2025-04-09T02:57:36.609Z" }, + { url = "https://files.pythonhosted.org/packages/e2/7d/bfb2805bcfbd479f04f835241ecf28519f6e3609912e3a985aed45e21370/numba-0.61.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ae8c7a522c26215d5f62ebec436e3d341f7f590079245a2f1008dfd498cc1642", size = 3824054, upload-time = "2025-04-09T02:57:38.162Z" }, + { url = "https://files.pythonhosted.org/packages/e3/27/797b2004745c92955470c73c82f0e300cf033c791f45bdecb4b33b12bdea/numba-0.61.2-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:bd1e74609855aa43661edffca37346e4e8462f6903889917e9f41db40907daa2", size = 3518531, upload-time = "2025-04-09T02:57:39.709Z" }, + { url = "https://files.pythonhosted.org/packages/b1/c6/c2fb11e50482cb310afae87a997707f6c7d8a48967b9696271347441f650/numba-0.61.2-cp310-cp310-win_amd64.whl", hash = "sha256:ae45830b129c6137294093b269ef0a22998ccc27bf7cf096ab8dcf7bca8946f9", size = 2831612, upload-time = "2025-04-09T02:57:41.559Z" }, + { url = "https://files.pythonhosted.org/packages/3f/97/c99d1056aed767503c228f7099dc11c402906b42a4757fec2819329abb98/numba-0.61.2-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:efd3db391df53aaa5cfbee189b6c910a5b471488749fd6606c3f33fc984c2ae2", size = 2775825, upload-time = "2025-04-09T02:57:43.442Z" }, + { url = "https://files.pythonhosted.org/packages/95/9e/63c549f37136e892f006260c3e2613d09d5120672378191f2dc387ba65a2/numba-0.61.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:49c980e4171948ffebf6b9a2520ea81feed113c1f4890747ba7f59e74be84b1b", size = 2778695, upload-time = "2025-04-09T02:57:44.968Z" }, + { url = "https://files.pythonhosted.org/packages/97/c8/8740616c8436c86c1b9a62e72cb891177d2c34c2d24ddcde4c390371bf4c/numba-0.61.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3945615cd73c2c7eba2a85ccc9c1730c21cd3958bfcf5a44302abae0fb07bb60", size = 3829227, upload-time = "2025-04-09T02:57:46.63Z" }, + { url = "https://files.pythonhosted.org/packages/fc/06/66e99ae06507c31d15ff3ecd1f108f2f59e18b6e08662cd5f8a5853fbd18/numba-0.61.2-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:bbfdf4eca202cebade0b7d43896978e146f39398909a42941c9303f82f403a18", size = 3523422, upload-time = "2025-04-09T02:57:48.222Z" }, + { url = "https://files.pythonhosted.org/packages/0f/a4/2b309a6a9f6d4d8cfba583401c7c2f9ff887adb5d54d8e2e130274c0973f/numba-0.61.2-cp311-cp311-win_amd64.whl", hash = "sha256:76bcec9f46259cedf888041b9886e257ae101c6268261b19fda8cfbc52bec9d1", size = 2831505, upload-time = "2025-04-09T02:57:50.108Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a0/c6b7b9c615cfa3b98c4c63f4316e3f6b3bbe2387740277006551784218cd/numba-0.61.2-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:34fba9406078bac7ab052efbf0d13939426c753ad72946baaa5bf9ae0ebb8dd2", size = 2776626, upload-time = "2025-04-09T02:57:51.857Z" }, + { url = "https://files.pythonhosted.org/packages/92/4a/fe4e3c2ecad72d88f5f8cd04e7f7cff49e718398a2fac02d2947480a00ca/numba-0.61.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4ddce10009bc097b080fc96876d14c051cc0c7679e99de3e0af59014dab7dfe8", size = 2779287, upload-time = "2025-04-09T02:57:53.658Z" }, + { url = "https://files.pythonhosted.org/packages/9a/2d/e518df036feab381c23a624dac47f8445ac55686ec7f11083655eb707da3/numba-0.61.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5b1bb509d01f23d70325d3a5a0e237cbc9544dd50e50588bc581ba860c213546", size = 3885928, upload-time = "2025-04-09T02:57:55.206Z" }, + { url = "https://files.pythonhosted.org/packages/10/0f/23cced68ead67b75d77cfcca3df4991d1855c897ee0ff3fe25a56ed82108/numba-0.61.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:48a53a3de8f8793526cbe330f2a39fe9a6638efcbf11bd63f3d2f9757ae345cd", size = 3577115, upload-time = "2025-04-09T02:57:56.818Z" }, + { url = "https://files.pythonhosted.org/packages/68/1d/ddb3e704c5a8fb90142bf9dc195c27db02a08a99f037395503bfbc1d14b3/numba-0.61.2-cp312-cp312-win_amd64.whl", hash = "sha256:97cf4f12c728cf77c9c1d7c23707e4d8fb4632b46275f8f3397de33e5877af18", size = 2831929, upload-time = "2025-04-09T02:57:58.45Z" }, ] [[package]] name = "numpy" version = "1.26.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/dd/2b/205ddff2314d4eea852e31d53b8e55eb3f32b292efc3dd86bd827ab9019d/numpy-1.26.2.tar.gz", hash = "sha256:f65738447676ab5777f11e6bbbdb8ce11b785e105f690bc45966574816b6d3ea", size = 15664248 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/76/ac/dea2939dfc3c591a2494121669455fd7d049248ef284c9542904ddbe05d5/numpy-1.26.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3703fc9258a4a122d17043e57b35e5ef1c5a5837c3db8be396c82e04c1cf9b0f", size = 20618422 }, - { url = "https://files.pythonhosted.org/packages/2f/ac/be1f2767b7222347d2fefc18d8d58e9febfd9919190cc6fbd8a4d22d6eab/numpy-1.26.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cc392fdcbd21d4be6ae1bb4475a03ce3b025cd49a9be5345d76d7585aea69440", size = 13956966 }, - { url = "https://files.pythonhosted.org/packages/38/39/f726e49ca91cbc336ff297d458dd20b4db2a4204198b075b7f7cc3d3c0ba/numpy-1.26.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36340109af8da8805d8851ef1d74761b3b88e81a9bd80b290bbfed61bd2b4f75", size = 14211889 }, - { url = "https://files.pythonhosted.org/packages/64/41/284783f1014685201e447ea976e85fed0e351f5debbaf3ee6d7645521f1d/numpy-1.26.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcc008217145b3d77abd3e4d5ef586e3bdfba8fe17940769f8aa09b99e856c00", size = 18228265 }, - { url = "https://files.pythonhosted.org/packages/b9/f0/01a7dade8233e6f3c380e2b271aedc98dd1902363661adfb8ab4364c5629/numpy-1.26.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3ced40d4e9e18242f70dd02d739e44698df3dcb010d31f495ff00a31ef6014fe", size = 13867221 }, - { url = "https://files.pythonhosted.org/packages/6c/89/0ef844673002e08444881bff4b6d2a940fbce934d8cd431aa76c8e46e42a/numpy-1.26.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b272d4cecc32c9e19911891446b72e986157e6a1809b7b56518b4f3755267523", size = 18065825 }, - { url = "https://files.pythonhosted.org/packages/15/b7/a7acec96d8c58bf40a24c9fccce988a819840692762e16bf1fc256e1c26a/numpy-1.26.2-cp310-cp310-win32.whl", hash = "sha256:22f8fc02fdbc829e7a8c578dd8d2e15a9074b630d4da29cda483337e300e3ee9", size = 20752812 }, - { url = "https://files.pythonhosted.org/packages/24/b5/fed6f7e582937eb947369dccf6c94602598a25f23e482d1b1f2299159328/numpy-1.26.2-cp310-cp310-win_amd64.whl", hash = "sha256:26c9d33f8e8b846d5a65dd068c14e04018d05533b348d9eaeef6c1bd787f9919", size = 15795633 }, - { url = "https://files.pythonhosted.org/packages/51/3b/2ba379bf754f13041e3d8b994394e78c69cdb9d1e5dd1dba9404b24afbdf/numpy-1.26.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b96e7b9c624ef3ae2ae0e04fa9b460f6b9f17ad8b4bec6d7756510f1f6c0c841", size = 20617415 }, - { url = "https://files.pythonhosted.org/packages/2e/54/218ce51bb571a70975f223671b2a86aa951e83abfd2a416a3d540f35115c/numpy-1.26.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:aa18428111fb9a591d7a9cc1b48150097ba6a7e8299fb56bdf574df650e7d1f1", size = 13987881 }, - { url = "https://files.pythonhosted.org/packages/f1/97/51eb4aa087e95138477e2140b17cd795fb379b1669432413dfad68f535c1/numpy-1.26.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06fa1ed84aa60ea6ef9f91ba57b5ed963c3729534e6e54055fc151fad0423f0a", size = 14216395 }, - { url = "https://files.pythonhosted.org/packages/b6/ab/5b893944b1602a366893559bfb227fdfb3ad7c7629b2a80d039bb5924367/numpy-1.26.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96ca5482c3dbdd051bcd1fce8034603d6ebfc125a7bd59f55b40d8f5d246832b", size = 18238922 }, - { url = "https://files.pythonhosted.org/packages/81/65/abb5808f13e96145b691bfe75cd8c4b7a94a6acfc5db0e8111ea17015675/numpy-1.26.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:854ab91a2906ef29dc3925a064fcd365c7b4da743f84b123002f6139bcb3f8a7", size = 13875653 }, - { url = "https://files.pythonhosted.org/packages/21/17/f9ab7b9f3b46c7d6b024d129259fd5d276aed9047e424537c48ca2e43339/numpy-1.26.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f43740ab089277d403aa07567be138fc2a89d4d9892d113b76153e0e412409f8", size = 18079981 }, - { url = "https://files.pythonhosted.org/packages/ac/6b/ea1405e449059f1e2be85f55d025598c11375c8d64cdf763506b22c244ab/numpy-1.26.2-cp311-cp311-win32.whl", hash = "sha256:a2bbc29fcb1771cd7b7425f98b05307776a6baf43035d3b80c4b0f29e9545186", size = 20755674 }, - { url = "https://files.pythonhosted.org/packages/da/3c/3ff05c2855eee52588f489a4e607e4a61699a0742aa03ccf641c77f9eb0a/numpy-1.26.2-cp311-cp311-win_amd64.whl", hash = "sha256:2b3fca8a5b00184828d12b073af4d0fc5fdd94b1632c2477526f6bd7842d700d", size = 15798609 }, - { url = "https://files.pythonhosted.org/packages/b1/97/6694e0855b11be0fd8598d484c09edd876ec738a8741025dee072f026c33/numpy-1.26.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a4cd6ed4a339c21f1d1b0fdf13426cb3b284555c27ac2f156dfdaaa7e16bfab0", size = 20323012 }, - { url = "https://files.pythonhosted.org/packages/2a/17/1fdc154e75d24d8c20c42b71bae1b5cf752453f0fc3a2504bbb810293dd1/numpy-1.26.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5d5244aabd6ed7f312268b9247be47343a654ebea52a60f002dc70c769048e75", size = 13675818 }, - { url = "https://files.pythonhosted.org/packages/a1/42/a2819c5b77fe6506662ffc13b767e0c216c02f75ae840219013ab822a473/numpy-1.26.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a3cdb4d9c70e6b8c0814239ead47da00934666f668426fc6e94cce869e13fd7", size = 13917117 }, - { url = "https://files.pythonhosted.org/packages/04/89/3b831e2b50c9364069609d1335f46c488a149d5f2be14a08741c92a60009/numpy-1.26.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa317b2325f7aa0a9471663e6093c210cb2ae9c0ad824732b307d2c51983d5b6", size = 17938212 }, - { url = "https://files.pythonhosted.org/packages/02/51/f078f1e7f658022150e7c8d5f99d505b40812840349d54667f98bb915b26/numpy-1.26.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:174a8880739c16c925799c018f3f55b8130c1f7c8e75ab0a6fa9d41cab092fd6", size = 13564269 }, - { url = "https://files.pythonhosted.org/packages/8c/9f/2f5c6b5f63cf006e6190bf750ade791d1fee353bab654bbde2f83a3ab92e/numpy-1.26.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f79b231bf5c16b1f39c7f4875e1ded36abee1591e98742b05d8a0fb55d8a3eec", size = 17774512 }, - { url = "https://files.pythonhosted.org/packages/51/7d/6181c8778cdb15ba0a4959bb72dcc1854c89ca4824481f224c6faf7024e1/numpy-1.26.2-cp312-cp312-win32.whl", hash = "sha256:4a06263321dfd3598cacb252f51e521a8cb4b6df471bb12a7ee5cbab20ea9167", size = 19962368 }, - { url = "https://files.pythonhosted.org/packages/28/75/3b679b41713bb60e2e8f6e2f87be72c971c9e718b1c17b8f8749240ddca8/numpy-1.26.2-cp312-cp312-win_amd64.whl", hash = "sha256:b04f5dc6b3efdaab541f7857351aac359e6ae3c126e2edb376929bd3b7f92d7e", size = 15504951 }, - { url = "https://files.pythonhosted.org/packages/b1/c0/563ef35266a30adfb9801bd1b366bc4f67ff9cfed5e707ae2831b3f6a27c/numpy-1.26.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4eb8df4bf8d3d90d091e0146f6c28492b0be84da3e409ebef54349f71ed271ef", size = 20623324 }, - { url = "https://files.pythonhosted.org/packages/20/be/46eed58d8ca60cfd0c4f3c6db3db79955f6de7d434db0f49fed2f817a6a4/numpy-1.26.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1a13860fdcd95de7cf58bd6f8bc5a5ef81c0b0625eb2c9a783948847abbef2c2", size = 13961919 }, - { url = "https://files.pythonhosted.org/packages/37/65/40ed08f156264e02cc2362ed194bc84daed61d8d2bc6b0ed45cfe024964f/numpy-1.26.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64308ebc366a8ed63fd0bf426b6a9468060962f1a4339ab1074c228fa6ade8e3", size = 14218739 }, - { url = "https://files.pythonhosted.org/packages/2f/75/f007cc0e6a373207818bef17f463d3305e9dd380a70db0e523e7660bf21f/numpy-1.26.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baf8aab04a2c0e859da118f0b38617e5ee65d75b83795055fb66c0d5e9e9b818", size = 18236679 }, - { url = "https://files.pythonhosted.org/packages/ed/09/433eadf9e1ea99e44011139395fcb01d236a674628e9ebacb079bf512622/numpy-1.26.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d73a3abcac238250091b11caef9ad12413dab01669511779bc9b29261dd50210", size = 13872811 }, - { url = "https://files.pythonhosted.org/packages/8f/17/106fbd94a661c3dbbb2a888a8b6624405c9aa44720d90683ba1c559a8de4/numpy-1.26.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b361d369fc7e5e1714cf827b731ca32bff8d411212fccd29ad98ad622449cc36", size = 18070972 }, - { url = "https://files.pythonhosted.org/packages/97/94/002acbb61f9cca0069e6854c04b893d8a7bc44130a4c333a497b23a399da/numpy-1.26.2-cp39-cp39-win32.whl", hash = "sha256:bd3f0091e845164a20bd5a326860c840fe2af79fa12e0469a12768a3ec578d80", size = 20768648 }, - { url = "https://files.pythonhosted.org/packages/07/34/748ec8c81235277f62cc04488052fe28b8b69280e7275bbb8dc143cd7791/numpy-1.26.2-cp39-cp39-win_amd64.whl", hash = "sha256:2beef57fb031dcc0dc8fa4fe297a742027b954949cabb52a2a376c144e5e6060", size = 15801297 }, - { url = "https://files.pythonhosted.org/packages/fb/91/17cea405f20865b0dce6f99dde5446b138e92111e140cde14933d433d69a/numpy-1.26.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:1cc3d5029a30fb5f06704ad6b23b35e11309491c999838c31f124fee32107c79", size = 20443636 }, - { url = "https://files.pythonhosted.org/packages/87/8c/1c5a2bbfb55e4ff35f9c30933e8816b188de3cc3e2eaf9304906991984a9/numpy-1.26.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94cc3c222bb9fb5a12e334d0479b97bb2df446fbe622b470928f5284ffca3f8d", size = 18048339 }, - { url = "https://files.pythonhosted.org/packages/62/b2/b11cde3447d5e6dfbfe0713ecd10760945584632969a15ce2177d37df982/numpy-1.26.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:fe6b44fb8fcdf7eda4ef4461b97b3f63c466b27ab151bec2366db8b197387841", size = 15696105 }, +sdist = { url = "https://files.pythonhosted.org/packages/dd/2b/205ddff2314d4eea852e31d53b8e55eb3f32b292efc3dd86bd827ab9019d/numpy-1.26.2.tar.gz", hash = "sha256:f65738447676ab5777f11e6bbbdb8ce11b785e105f690bc45966574816b6d3ea", size = 15664248, upload-time = "2023-11-12T23:17:31.386Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/ac/dea2939dfc3c591a2494121669455fd7d049248ef284c9542904ddbe05d5/numpy-1.26.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3703fc9258a4a122d17043e57b35e5ef1c5a5837c3db8be396c82e04c1cf9b0f", size = 20618422, upload-time = "2023-11-12T22:51:49.818Z" }, + { url = "https://files.pythonhosted.org/packages/2f/ac/be1f2767b7222347d2fefc18d8d58e9febfd9919190cc6fbd8a4d22d6eab/numpy-1.26.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cc392fdcbd21d4be6ae1bb4475a03ce3b025cd49a9be5345d76d7585aea69440", size = 13956966, upload-time = "2023-11-12T22:52:18.231Z" }, + { url = "https://files.pythonhosted.org/packages/38/39/f726e49ca91cbc336ff297d458dd20b4db2a4204198b075b7f7cc3d3c0ba/numpy-1.26.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36340109af8da8805d8851ef1d74761b3b88e81a9bd80b290bbfed61bd2b4f75", size = 14211889, upload-time = "2023-11-12T22:52:50.334Z" }, + { url = "https://files.pythonhosted.org/packages/64/41/284783f1014685201e447ea976e85fed0e351f5debbaf3ee6d7645521f1d/numpy-1.26.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcc008217145b3d77abd3e4d5ef586e3bdfba8fe17940769f8aa09b99e856c00", size = 18228265, upload-time = "2023-11-12T22:53:30.754Z" }, + { url = "https://files.pythonhosted.org/packages/b9/f0/01a7dade8233e6f3c380e2b271aedc98dd1902363661adfb8ab4364c5629/numpy-1.26.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3ced40d4e9e18242f70dd02d739e44698df3dcb010d31f495ff00a31ef6014fe", size = 13867221, upload-time = "2023-11-12T22:54:06.325Z" }, + { url = "https://files.pythonhosted.org/packages/6c/89/0ef844673002e08444881bff4b6d2a940fbce934d8cd431aa76c8e46e42a/numpy-1.26.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b272d4cecc32c9e19911891446b72e986157e6a1809b7b56518b4f3755267523", size = 18065825, upload-time = "2023-11-12T22:54:45.565Z" }, + { url = "https://files.pythonhosted.org/packages/15/b7/a7acec96d8c58bf40a24c9fccce988a819840692762e16bf1fc256e1c26a/numpy-1.26.2-cp310-cp310-win32.whl", hash = "sha256:22f8fc02fdbc829e7a8c578dd8d2e15a9074b630d4da29cda483337e300e3ee9", size = 20752812, upload-time = "2023-11-12T22:55:26.963Z" }, + { url = "https://files.pythonhosted.org/packages/24/b5/fed6f7e582937eb947369dccf6c94602598a25f23e482d1b1f2299159328/numpy-1.26.2-cp310-cp310-win_amd64.whl", hash = "sha256:26c9d33f8e8b846d5a65dd068c14e04018d05533b348d9eaeef6c1bd787f9919", size = 15795633, upload-time = "2023-11-12T22:56:49.532Z" }, + { url = "https://files.pythonhosted.org/packages/51/3b/2ba379bf754f13041e3d8b994394e78c69cdb9d1e5dd1dba9404b24afbdf/numpy-1.26.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b96e7b9c624ef3ae2ae0e04fa9b460f6b9f17ad8b4bec6d7756510f1f6c0c841", size = 20617415, upload-time = "2023-11-12T22:57:29.251Z" }, + { url = "https://files.pythonhosted.org/packages/2e/54/218ce51bb571a70975f223671b2a86aa951e83abfd2a416a3d540f35115c/numpy-1.26.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:aa18428111fb9a591d7a9cc1b48150097ba6a7e8299fb56bdf574df650e7d1f1", size = 13987881, upload-time = "2023-11-12T22:57:58.522Z" }, + { url = "https://files.pythonhosted.org/packages/f1/97/51eb4aa087e95138477e2140b17cd795fb379b1669432413dfad68f535c1/numpy-1.26.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06fa1ed84aa60ea6ef9f91ba57b5ed963c3729534e6e54055fc151fad0423f0a", size = 14216395, upload-time = "2023-11-12T22:58:26.86Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ab/5b893944b1602a366893559bfb227fdfb3ad7c7629b2a80d039bb5924367/numpy-1.26.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96ca5482c3dbdd051bcd1fce8034603d6ebfc125a7bd59f55b40d8f5d246832b", size = 18238922, upload-time = "2023-11-12T22:59:13.134Z" }, + { url = "https://files.pythonhosted.org/packages/81/65/abb5808f13e96145b691bfe75cd8c4b7a94a6acfc5db0e8111ea17015675/numpy-1.26.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:854ab91a2906ef29dc3925a064fcd365c7b4da743f84b123002f6139bcb3f8a7", size = 13875653, upload-time = "2023-11-12T22:59:43.412Z" }, + { url = "https://files.pythonhosted.org/packages/21/17/f9ab7b9f3b46c7d6b024d129259fd5d276aed9047e424537c48ca2e43339/numpy-1.26.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f43740ab089277d403aa07567be138fc2a89d4d9892d113b76153e0e412409f8", size = 18079981, upload-time = "2023-11-12T23:00:18.926Z" }, + { url = "https://files.pythonhosted.org/packages/ac/6b/ea1405e449059f1e2be85f55d025598c11375c8d64cdf763506b22c244ab/numpy-1.26.2-cp311-cp311-win32.whl", hash = "sha256:a2bbc29fcb1771cd7b7425f98b05307776a6baf43035d3b80c4b0f29e9545186", size = 20755674, upload-time = "2023-11-12T23:01:17.569Z" }, + { url = "https://files.pythonhosted.org/packages/da/3c/3ff05c2855eee52588f489a4e607e4a61699a0742aa03ccf641c77f9eb0a/numpy-1.26.2-cp311-cp311-win_amd64.whl", hash = "sha256:2b3fca8a5b00184828d12b073af4d0fc5fdd94b1632c2477526f6bd7842d700d", size = 15798609, upload-time = "2023-11-12T23:01:58.827Z" }, + { url = "https://files.pythonhosted.org/packages/b1/97/6694e0855b11be0fd8598d484c09edd876ec738a8741025dee072f026c33/numpy-1.26.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a4cd6ed4a339c21f1d1b0fdf13426cb3b284555c27ac2f156dfdaaa7e16bfab0", size = 20323012, upload-time = "2023-11-12T23:02:57.091Z" }, + { url = "https://files.pythonhosted.org/packages/2a/17/1fdc154e75d24d8c20c42b71bae1b5cf752453f0fc3a2504bbb810293dd1/numpy-1.26.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5d5244aabd6ed7f312268b9247be47343a654ebea52a60f002dc70c769048e75", size = 13675818, upload-time = "2023-11-12T23:03:32.823Z" }, + { url = "https://files.pythonhosted.org/packages/a1/42/a2819c5b77fe6506662ffc13b767e0c216c02f75ae840219013ab822a473/numpy-1.26.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a3cdb4d9c70e6b8c0814239ead47da00934666f668426fc6e94cce869e13fd7", size = 13917117, upload-time = "2023-11-12T23:03:59.013Z" }, + { url = "https://files.pythonhosted.org/packages/04/89/3b831e2b50c9364069609d1335f46c488a149d5f2be14a08741c92a60009/numpy-1.26.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa317b2325f7aa0a9471663e6093c210cb2ae9c0ad824732b307d2c51983d5b6", size = 17938212, upload-time = "2023-11-12T23:04:32.896Z" }, + { url = "https://files.pythonhosted.org/packages/02/51/f078f1e7f658022150e7c8d5f99d505b40812840349d54667f98bb915b26/numpy-1.26.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:174a8880739c16c925799c018f3f55b8130c1f7c8e75ab0a6fa9d41cab092fd6", size = 13564269, upload-time = "2023-11-12T23:05:31.101Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9f/2f5c6b5f63cf006e6190bf750ade791d1fee353bab654bbde2f83a3ab92e/numpy-1.26.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f79b231bf5c16b1f39c7f4875e1ded36abee1591e98742b05d8a0fb55d8a3eec", size = 17774512, upload-time = "2023-11-12T23:06:03.941Z" }, + { url = "https://files.pythonhosted.org/packages/51/7d/6181c8778cdb15ba0a4959bb72dcc1854c89ca4824481f224c6faf7024e1/numpy-1.26.2-cp312-cp312-win32.whl", hash = "sha256:4a06263321dfd3598cacb252f51e521a8cb4b6df471bb12a7ee5cbab20ea9167", size = 19962368, upload-time = "2023-11-12T23:06:51.561Z" }, + { url = "https://files.pythonhosted.org/packages/28/75/3b679b41713bb60e2e8f6e2f87be72c971c9e718b1c17b8f8749240ddca8/numpy-1.26.2-cp312-cp312-win_amd64.whl", hash = "sha256:b04f5dc6b3efdaab541f7857351aac359e6ae3c126e2edb376929bd3b7f92d7e", size = 15504951, upload-time = "2023-11-12T23:07:33.828Z" }, + { url = "https://files.pythonhosted.org/packages/b1/c0/563ef35266a30adfb9801bd1b366bc4f67ff9cfed5e707ae2831b3f6a27c/numpy-1.26.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4eb8df4bf8d3d90d091e0146f6c28492b0be84da3e409ebef54349f71ed271ef", size = 20623324, upload-time = "2023-11-12T23:08:23.476Z" }, + { url = "https://files.pythonhosted.org/packages/20/be/46eed58d8ca60cfd0c4f3c6db3db79955f6de7d434db0f49fed2f817a6a4/numpy-1.26.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1a13860fdcd95de7cf58bd6f8bc5a5ef81c0b0625eb2c9a783948847abbef2c2", size = 13961919, upload-time = "2023-11-12T23:09:18.693Z" }, + { url = "https://files.pythonhosted.org/packages/37/65/40ed08f156264e02cc2362ed194bc84daed61d8d2bc6b0ed45cfe024964f/numpy-1.26.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64308ebc366a8ed63fd0bf426b6a9468060962f1a4339ab1074c228fa6ade8e3", size = 14218739, upload-time = "2023-11-12T23:09:51.31Z" }, + { url = "https://files.pythonhosted.org/packages/2f/75/f007cc0e6a373207818bef17f463d3305e9dd380a70db0e523e7660bf21f/numpy-1.26.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baf8aab04a2c0e859da118f0b38617e5ee65d75b83795055fb66c0d5e9e9b818", size = 18236679, upload-time = "2023-11-12T23:11:03.747Z" }, + { url = "https://files.pythonhosted.org/packages/ed/09/433eadf9e1ea99e44011139395fcb01d236a674628e9ebacb079bf512622/numpy-1.26.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d73a3abcac238250091b11caef9ad12413dab01669511779bc9b29261dd50210", size = 13872811, upload-time = "2023-11-12T23:12:01.692Z" }, + { url = "https://files.pythonhosted.org/packages/8f/17/106fbd94a661c3dbbb2a888a8b6624405c9aa44720d90683ba1c559a8de4/numpy-1.26.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b361d369fc7e5e1714cf827b731ca32bff8d411212fccd29ad98ad622449cc36", size = 18070972, upload-time = "2023-11-12T23:12:34.114Z" }, + { url = "https://files.pythonhosted.org/packages/97/94/002acbb61f9cca0069e6854c04b893d8a7bc44130a4c333a497b23a399da/numpy-1.26.2-cp39-cp39-win32.whl", hash = "sha256:bd3f0091e845164a20bd5a326860c840fe2af79fa12e0469a12768a3ec578d80", size = 20768648, upload-time = "2023-11-12T23:13:21.735Z" }, + { url = "https://files.pythonhosted.org/packages/07/34/748ec8c81235277f62cc04488052fe28b8b69280e7275bbb8dc143cd7791/numpy-1.26.2-cp39-cp39-win_amd64.whl", hash = "sha256:2beef57fb031dcc0dc8fa4fe297a742027b954949cabb52a2a376c144e5e6060", size = 15801297, upload-time = "2023-11-12T23:13:54.559Z" }, + { url = "https://files.pythonhosted.org/packages/fb/91/17cea405f20865b0dce6f99dde5446b138e92111e140cde14933d433d69a/numpy-1.26.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:1cc3d5029a30fb5f06704ad6b23b35e11309491c999838c31f124fee32107c79", size = 20443636, upload-time = "2023-11-12T23:14:46.626Z" }, + { url = "https://files.pythonhosted.org/packages/87/8c/1c5a2bbfb55e4ff35f9c30933e8816b188de3cc3e2eaf9304906991984a9/numpy-1.26.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94cc3c222bb9fb5a12e334d0479b97bb2df446fbe622b470928f5284ffca3f8d", size = 18048339, upload-time = "2023-11-12T23:15:26.573Z" }, + { url = "https://files.pythonhosted.org/packages/62/b2/b11cde3447d5e6dfbfe0713ecd10760945584632969a15ce2177d37df982/numpy-1.26.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:fe6b44fb8fcdf7eda4ef4461b97b3f63c466b27ab151bec2366db8b197387841", size = 15696105, upload-time = "2023-11-12T23:16:05.298Z" }, ] [[package]] @@ -1137,32 +1168,32 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1f/b3/9e705dbe26fbb055e56ffa8a0e398abb088a72ef503a1bf9613e8a807961/opencv-contrib-python-4.8.1.78.tar.gz", hash = "sha256:81804332299d656905d4f404fcec5f400d692c652d7a47926b7a441272ce795b", size = 151238067 } +sdist = { url = "https://files.pythonhosted.org/packages/1f/b3/9e705dbe26fbb055e56ffa8a0e398abb088a72ef503a1bf9613e8a807961/opencv-contrib-python-4.8.1.78.tar.gz", hash = "sha256:81804332299d656905d4f404fcec5f400d692c652d7a47926b7a441272ce795b", size = 151238067, upload-time = "2023-09-28T11:11:40.367Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/c1/8807b1145c89f64734517ab18f0acc11e021059cd8fdf5c765f4633ae0bc/opencv_contrib_python-4.8.1.78-cp37-abi3-macosx_10_16_x86_64.whl", hash = "sha256:8d97192471c7d42532103ecebf8ad9d9534b7cd655ffadbccacb9ff3d4d49b40", size = 64403609 }, - { url = "https://files.pythonhosted.org/packages/63/cc/c7c858456e5ad20e91fb0b2bfcc93f0f5a3805e4f98050fc082020c006ae/opencv_contrib_python-4.8.1.78-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:8d6feb39d4af2cd1e8919110229bfedd13d4798a089bbe88fbd1a001b664d552", size = 41438544 }, - { url = "https://files.pythonhosted.org/packages/fb/2c/e3be24016ad990eefb2b24bfb1cee64ca61f67117a1e6fb369a53fb0608e/opencv_contrib_python-4.8.1.78-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6cad1720ac701cb3742f48f95bef5cfa288b916b6ac5700f63d5809e3ad5999e", size = 46642477 }, - { url = "https://files.pythonhosted.org/packages/32/9e/4dcc0bb70e3b365dc85b8f96c63e6a306653f7cc6ed061aa6cc7b2bddee7/opencv_contrib_python-4.8.1.78-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3fb62bc5967a79bce7c576ef94dea0b9172e52bab91630103e042cb5f29a148a", size = 67807199 }, - { url = "https://files.pythonhosted.org/packages/cd/bf/5880ce389a9d47fc96d52280f8ce3f69fbd0388662819fbcdf6fd829719f/opencv_contrib_python-4.8.1.78-cp37-abi3-win32.whl", hash = "sha256:f8737cf3055a6156c66c75432ed28ee3c1d52532b17d91ed73d508ae351b3e66", size = 34070000 }, - { url = "https://files.pythonhosted.org/packages/81/3c/bbb3ceee9fbefc505f98c24dafda68c7b3c4f83b6951c0712b4623fe4cce/opencv_contrib_python-4.8.1.78-cp37-abi3-win_amd64.whl", hash = "sha256:377936b02dcf82dc70261101381a8ad82b03d1298f185886be298d74fe35c328", size = 44790515 }, + { url = "https://files.pythonhosted.org/packages/d7/c1/8807b1145c89f64734517ab18f0acc11e021059cd8fdf5c765f4633ae0bc/opencv_contrib_python-4.8.1.78-cp37-abi3-macosx_10_16_x86_64.whl", hash = "sha256:8d97192471c7d42532103ecebf8ad9d9534b7cd655ffadbccacb9ff3d4d49b40", size = 64403609, upload-time = "2023-09-28T11:01:42.815Z" }, + { url = "https://files.pythonhosted.org/packages/63/cc/c7c858456e5ad20e91fb0b2bfcc93f0f5a3805e4f98050fc082020c006ae/opencv_contrib_python-4.8.1.78-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:8d6feb39d4af2cd1e8919110229bfedd13d4798a089bbe88fbd1a001b664d552", size = 41438544, upload-time = "2023-09-28T11:01:52.331Z" }, + { url = "https://files.pythonhosted.org/packages/fb/2c/e3be24016ad990eefb2b24bfb1cee64ca61f67117a1e6fb369a53fb0608e/opencv_contrib_python-4.8.1.78-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6cad1720ac701cb3742f48f95bef5cfa288b916b6ac5700f63d5809e3ad5999e", size = 46642477, upload-time = "2023-09-28T11:02:00.442Z" }, + { url = "https://files.pythonhosted.org/packages/32/9e/4dcc0bb70e3b365dc85b8f96c63e6a306653f7cc6ed061aa6cc7b2bddee7/opencv_contrib_python-4.8.1.78-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3fb62bc5967a79bce7c576ef94dea0b9172e52bab91630103e042cb5f29a148a", size = 67807199, upload-time = "2023-09-28T11:02:11.339Z" }, + { url = "https://files.pythonhosted.org/packages/cd/bf/5880ce389a9d47fc96d52280f8ce3f69fbd0388662819fbcdf6fd829719f/opencv_contrib_python-4.8.1.78-cp37-abi3-win32.whl", hash = "sha256:f8737cf3055a6156c66c75432ed28ee3c1d52532b17d91ed73d508ae351b3e66", size = 34070000, upload-time = "2023-09-28T11:02:19.682Z" }, + { url = "https://files.pythonhosted.org/packages/81/3c/bbb3ceee9fbefc505f98c24dafda68c7b3c4f83b6951c0712b4623fe4cce/opencv_contrib_python-4.8.1.78-cp37-abi3-win_amd64.whl", hash = "sha256:377936b02dcf82dc70261101381a8ad82b03d1298f185886be298d74fe35c328", size = 44790515, upload-time = "2023-09-28T11:02:28.069Z" }, ] [[package]] name = "packaging" version = "24.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } +sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950, upload-time = "2024-11-08T09:47:47.202Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, + { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451, upload-time = "2024-11-08T09:47:44.722Z" }, ] [[package]] name = "pathspec" version = "0.12.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043 } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 }, + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, ] [[package]] @@ -1172,84 +1203,84 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "setuptools" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/01/d2/510cc0d218e753ba62a1bc1434651db3cd797a9716a0a66cc714cb4f0935/pbr-6.1.1.tar.gz", hash = "sha256:93ea72ce6989eb2eed99d0f75721474f69ad88128afdef5ac377eb797c4bf76b", size = 125702 } +sdist = { url = "https://files.pythonhosted.org/packages/01/d2/510cc0d218e753ba62a1bc1434651db3cd797a9716a0a66cc714cb4f0935/pbr-6.1.1.tar.gz", hash = "sha256:93ea72ce6989eb2eed99d0f75721474f69ad88128afdef5ac377eb797c4bf76b", size = 125702, upload-time = "2025-02-04T14:28:06.514Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/47/ac/684d71315abc7b1214d59304e23a982472967f6bf4bde5a98f1503f648dc/pbr-6.1.1-py2.py3-none-any.whl", hash = "sha256:38d4daea5d9fa63b3f626131b9d34947fd0c8be9b05a29276870580050a25a76", size = 108997 }, + { url = "https://files.pythonhosted.org/packages/47/ac/684d71315abc7b1214d59304e23a982472967f6bf4bde5a98f1503f648dc/pbr-6.1.1-py2.py3-none-any.whl", hash = "sha256:38d4daea5d9fa63b3f626131b9d34947fd0c8be9b05a29276870580050a25a76", size = 108997, upload-time = "2025-02-04T14:28:03.168Z" }, ] [[package]] name = "pillow" version = "11.2.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/af/cb/bb5c01fcd2a69335b86c22142b2bccfc3464087efb7fd382eee5ffc7fdf7/pillow-11.2.1.tar.gz", hash = "sha256:a64dd61998416367b7ef979b73d3a85853ba9bec4c2925f74e588879a58716b6", size = 47026707 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/8b/b158ad57ed44d3cc54db8d68ad7c0a58b8fc0e4c7a3f995f9d62d5b464a1/pillow-11.2.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:d57a75d53922fc20c165016a20d9c44f73305e67c351bbc60d1adaf662e74047", size = 3198442 }, - { url = "https://files.pythonhosted.org/packages/b1/f8/bb5d956142f86c2d6cc36704943fa761f2d2e4c48b7436fd0a85c20f1713/pillow-11.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:127bf6ac4a5b58b3d32fc8289656f77f80567d65660bc46f72c0d77e6600cc95", size = 3030553 }, - { url = "https://files.pythonhosted.org/packages/22/7f/0e413bb3e2aa797b9ca2c5c38cb2e2e45d88654e5b12da91ad446964cfae/pillow-11.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4ba4be812c7a40280629e55ae0b14a0aafa150dd6451297562e1764808bbe61", size = 4405503 }, - { url = "https://files.pythonhosted.org/packages/f3/b4/cc647f4d13f3eb837d3065824aa58b9bcf10821f029dc79955ee43f793bd/pillow-11.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8bd62331e5032bc396a93609982a9ab6b411c05078a52f5fe3cc59234a3abd1", size = 4490648 }, - { url = "https://files.pythonhosted.org/packages/c2/6f/240b772a3b35cdd7384166461567aa6713799b4e78d180c555bd284844ea/pillow-11.2.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:562d11134c97a62fe3af29581f083033179f7ff435f78392565a1ad2d1c2c45c", size = 4508937 }, - { url = "https://files.pythonhosted.org/packages/f3/5e/7ca9c815ade5fdca18853db86d812f2f188212792780208bdb37a0a6aef4/pillow-11.2.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:c97209e85b5be259994eb5b69ff50c5d20cca0f458ef9abd835e262d9d88b39d", size = 4599802 }, - { url = "https://files.pythonhosted.org/packages/02/81/c3d9d38ce0c4878a77245d4cf2c46d45a4ad0f93000227910a46caff52f3/pillow-11.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0c3e6d0f59171dfa2e25d7116217543310908dfa2770aa64b8f87605f8cacc97", size = 4576717 }, - { url = "https://files.pythonhosted.org/packages/42/49/52b719b89ac7da3185b8d29c94d0e6aec8140059e3d8adcaa46da3751180/pillow-11.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc1c3bc53befb6096b84165956e886b1729634a799e9d6329a0c512ab651e579", size = 4654874 }, - { url = "https://files.pythonhosted.org/packages/5b/0b/ede75063ba6023798267023dc0d0401f13695d228194d2242d5a7ba2f964/pillow-11.2.1-cp310-cp310-win32.whl", hash = "sha256:312c77b7f07ab2139924d2639860e084ec2a13e72af54d4f08ac843a5fc9c79d", size = 2331717 }, - { url = "https://files.pythonhosted.org/packages/ed/3c/9831da3edea527c2ed9a09f31a2c04e77cd705847f13b69ca60269eec370/pillow-11.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:9bc7ae48b8057a611e5fe9f853baa88093b9a76303937449397899385da06fad", size = 2676204 }, - { url = "https://files.pythonhosted.org/packages/01/97/1f66ff8a1503d8cbfc5bae4dc99d54c6ec1e22ad2b946241365320caabc2/pillow-11.2.1-cp310-cp310-win_arm64.whl", hash = "sha256:2728567e249cdd939f6cc3d1f049595c66e4187f3c34078cbc0a7d21c47482d2", size = 2414767 }, - { url = "https://files.pythonhosted.org/packages/68/08/3fbf4b98924c73037a8e8b4c2c774784805e0fb4ebca6c5bb60795c40125/pillow-11.2.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:35ca289f712ccfc699508c4658a1d14652e8033e9b69839edf83cbdd0ba39e70", size = 3198450 }, - { url = "https://files.pythonhosted.org/packages/84/92/6505b1af3d2849d5e714fc75ba9e69b7255c05ee42383a35a4d58f576b16/pillow-11.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0409af9f829f87a2dfb7e259f78f317a5351f2045158be321fd135973fff7bf", size = 3030550 }, - { url = "https://files.pythonhosted.org/packages/3c/8c/ac2f99d2a70ff966bc7eb13dacacfaab57c0549b2ffb351b6537c7840b12/pillow-11.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4e5c5edee874dce4f653dbe59db7c73a600119fbea8d31f53423586ee2aafd7", size = 4415018 }, - { url = "https://files.pythonhosted.org/packages/1f/e3/0a58b5d838687f40891fff9cbaf8669f90c96b64dc8f91f87894413856c6/pillow-11.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b93a07e76d13bff9444f1a029e0af2964e654bfc2e2c2d46bfd080df5ad5f3d8", size = 4498006 }, - { url = "https://files.pythonhosted.org/packages/21/f5/6ba14718135f08fbfa33308efe027dd02b781d3f1d5c471444a395933aac/pillow-11.2.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:e6def7eed9e7fa90fde255afaf08060dc4b343bbe524a8f69bdd2a2f0018f600", size = 4517773 }, - { url = "https://files.pythonhosted.org/packages/20/f2/805ad600fc59ebe4f1ba6129cd3a75fb0da126975c8579b8f57abeb61e80/pillow-11.2.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:8f4f3724c068be008c08257207210c138d5f3731af6c155a81c2b09a9eb3a788", size = 4607069 }, - { url = "https://files.pythonhosted.org/packages/71/6b/4ef8a288b4bb2e0180cba13ca0a519fa27aa982875882392b65131401099/pillow-11.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a0a6709b47019dff32e678bc12c63008311b82b9327613f534e496dacaefb71e", size = 4583460 }, - { url = "https://files.pythonhosted.org/packages/62/ae/f29c705a09cbc9e2a456590816e5c234382ae5d32584f451c3eb41a62062/pillow-11.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f6b0c664ccb879109ee3ca702a9272d877f4fcd21e5eb63c26422fd6e415365e", size = 4661304 }, - { url = "https://files.pythonhosted.org/packages/6e/1a/c8217b6f2f73794a5e219fbad087701f412337ae6dbb956db37d69a9bc43/pillow-11.2.1-cp311-cp311-win32.whl", hash = "sha256:cc5d875d56e49f112b6def6813c4e3d3036d269c008bf8aef72cd08d20ca6df6", size = 2331809 }, - { url = "https://files.pythonhosted.org/packages/e2/72/25a8f40170dc262e86e90f37cb72cb3de5e307f75bf4b02535a61afcd519/pillow-11.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:0f5c7eda47bf8e3c8a283762cab94e496ba977a420868cb819159980b6709193", size = 2676338 }, - { url = "https://files.pythonhosted.org/packages/06/9e/76825e39efee61efea258b479391ca77d64dbd9e5804e4ad0fa453b4ba55/pillow-11.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:4d375eb838755f2528ac8cbc926c3e31cc49ca4ad0cf79cff48b20e30634a4a7", size = 2414918 }, - { url = "https://files.pythonhosted.org/packages/c7/40/052610b15a1b8961f52537cc8326ca6a881408bc2bdad0d852edeb6ed33b/pillow-11.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:78afba22027b4accef10dbd5eed84425930ba41b3ea0a86fa8d20baaf19d807f", size = 3190185 }, - { url = "https://files.pythonhosted.org/packages/e5/7e/b86dbd35a5f938632093dc40d1682874c33dcfe832558fc80ca56bfcb774/pillow-11.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78092232a4ab376a35d68c4e6d5e00dfd73454bd12b230420025fbe178ee3b0b", size = 3030306 }, - { url = "https://files.pythonhosted.org/packages/a4/5c/467a161f9ed53e5eab51a42923c33051bf8d1a2af4626ac04f5166e58e0c/pillow-11.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25a5f306095c6780c52e6bbb6109624b95c5b18e40aab1c3041da3e9e0cd3e2d", size = 4416121 }, - { url = "https://files.pythonhosted.org/packages/62/73/972b7742e38ae0e2ac76ab137ca6005dcf877480da0d9d61d93b613065b4/pillow-11.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c7b29dbd4281923a2bfe562acb734cee96bbb129e96e6972d315ed9f232bef4", size = 4501707 }, - { url = "https://files.pythonhosted.org/packages/e4/3a/427e4cb0b9e177efbc1a84798ed20498c4f233abde003c06d2650a6d60cb/pillow-11.2.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3e645b020f3209a0181a418bffe7b4a93171eef6c4ef6cc20980b30bebf17b7d", size = 4522921 }, - { url = "https://files.pythonhosted.org/packages/fe/7c/d8b1330458e4d2f3f45d9508796d7caf0c0d3764c00c823d10f6f1a3b76d/pillow-11.2.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b2dbea1012ccb784a65349f57bbc93730b96e85b42e9bf7b01ef40443db720b4", size = 4612523 }, - { url = "https://files.pythonhosted.org/packages/b3/2f/65738384e0b1acf451de5a573d8153fe84103772d139e1e0bdf1596be2ea/pillow-11.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:da3104c57bbd72948d75f6a9389e6727d2ab6333c3617f0a89d72d4940aa0443", size = 4587836 }, - { url = "https://files.pythonhosted.org/packages/6a/c5/e795c9f2ddf3debb2dedd0df889f2fe4b053308bb59a3cc02a0cd144d641/pillow-11.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:598174aef4589af795f66f9caab87ba4ff860ce08cd5bb447c6fc553ffee603c", size = 4669390 }, - { url = "https://files.pythonhosted.org/packages/96/ae/ca0099a3995976a9fce2f423166f7bff9b12244afdc7520f6ed38911539a/pillow-11.2.1-cp312-cp312-win32.whl", hash = "sha256:1d535df14716e7f8776b9e7fee118576d65572b4aad3ed639be9e4fa88a1cad3", size = 2332309 }, - { url = "https://files.pythonhosted.org/packages/7c/18/24bff2ad716257fc03da964c5e8f05d9790a779a8895d6566e493ccf0189/pillow-11.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:14e33b28bf17c7a38eede290f77db7c664e4eb01f7869e37fa98a5aa95978941", size = 2676768 }, - { url = "https://files.pythonhosted.org/packages/da/bb/e8d656c9543276517ee40184aaa39dcb41e683bca121022f9323ae11b39d/pillow-11.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:21e1470ac9e5739ff880c211fc3af01e3ae505859392bf65458c224d0bf283eb", size = 2415087 }, - { url = "https://files.pythonhosted.org/packages/21/3a/c1835d1c7cf83559e95b4f4ed07ab0bb7acc689712adfce406b3f456e9fd/pillow-11.2.1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:7491cf8a79b8eb867d419648fff2f83cb0b3891c8b36da92cc7f1931d46108c8", size = 3198391 }, - { url = "https://files.pythonhosted.org/packages/b6/4d/dcb7a9af3fc1e8653267c38ed622605d9d1793349274b3ef7af06457e257/pillow-11.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8b02d8f9cb83c52578a0b4beadba92e37d83a4ef11570a8688bbf43f4ca50909", size = 3030573 }, - { url = "https://files.pythonhosted.org/packages/9d/29/530ca098c1a1eb31d4e163d317d0e24e6d2ead907991c69ca5b663de1bc5/pillow-11.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:014ca0050c85003620526b0ac1ac53f56fc93af128f7546623cc8e31875ab928", size = 4398677 }, - { url = "https://files.pythonhosted.org/packages/8b/ee/0e5e51db34de1690264e5f30dcd25328c540aa11d50a3bc0b540e2a445b6/pillow-11.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3692b68c87096ac6308296d96354eddd25f98740c9d2ab54e1549d6c8aea9d79", size = 4484986 }, - { url = "https://files.pythonhosted.org/packages/93/7d/bc723b41ce3d2c28532c47678ec988974f731b5c6fadd5b3a4fba9015e4f/pillow-11.2.1-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:f781dcb0bc9929adc77bad571b8621ecb1e4cdef86e940fe2e5b5ee24fd33b35", size = 4501897 }, - { url = "https://files.pythonhosted.org/packages/be/0b/532e31abc7389617ddff12551af625a9b03cd61d2989fa595e43c470ec67/pillow-11.2.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:2b490402c96f907a166615e9a5afacf2519e28295f157ec3a2bb9bd57de638cb", size = 4592618 }, - { url = "https://files.pythonhosted.org/packages/4c/f0/21ed6499a6216fef753e2e2254a19d08bff3747108ba042422383f3e9faa/pillow-11.2.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dd6b20b93b3ccc9c1b597999209e4bc5cf2853f9ee66e3fc9a400a78733ffc9a", size = 4570493 }, - { url = "https://files.pythonhosted.org/packages/68/de/17004ddb8ab855573fe1127ab0168d11378cdfe4a7ee2a792a70ff2e9ba7/pillow-11.2.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:4b835d89c08a6c2ee7781b8dd0a30209a8012b5f09c0a665b65b0eb3560b6f36", size = 4647748 }, - { url = "https://files.pythonhosted.org/packages/c7/23/82ecb486384bb3578115c509d4a00bb52f463ee700a5ca1be53da3c88c19/pillow-11.2.1-cp39-cp39-win32.whl", hash = "sha256:b10428b3416d4f9c61f94b494681280be7686bda15898a3a9e08eb66a6d92d67", size = 2331731 }, - { url = "https://files.pythonhosted.org/packages/58/bb/87efd58b3689537a623d44dbb2550ef0bb5ff6a62769707a0fe8b1a7bdeb/pillow-11.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:6ebce70c3f486acf7591a3d73431fa504a4e18a9b97ff27f5f47b7368e4b9dd1", size = 2676346 }, - { url = "https://files.pythonhosted.org/packages/80/08/dc268475b22887b816e5dcfae31bce897f524b4646bab130c2142c9b2400/pillow-11.2.1-cp39-cp39-win_arm64.whl", hash = "sha256:c27476257b2fdcd7872d54cfd119b3a9ce4610fb85c8e32b70b42e3680a29a1e", size = 2414623 }, - { url = "https://files.pythonhosted.org/packages/33/49/c8c21e4255b4f4a2c0c68ac18125d7f5460b109acc6dfdef1a24f9b960ef/pillow-11.2.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:9b7b0d4fd2635f54ad82785d56bc0d94f147096493a79985d0ab57aedd563156", size = 3181727 }, - { url = "https://files.pythonhosted.org/packages/6d/f1/f7255c0838f8c1ef6d55b625cfb286835c17e8136ce4351c5577d02c443b/pillow-11.2.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:aa442755e31c64037aa7c1cb186e0b369f8416c567381852c63444dd666fb772", size = 2999833 }, - { url = "https://files.pythonhosted.org/packages/e2/57/9968114457bd131063da98d87790d080366218f64fa2943b65ac6739abb3/pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0d3348c95b766f54b76116d53d4cb171b52992a1027e7ca50c81b43b9d9e363", size = 3437472 }, - { url = "https://files.pythonhosted.org/packages/b2/1b/e35d8a158e21372ecc48aac9c453518cfe23907bb82f950d6e1c72811eb0/pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85d27ea4c889342f7e35f6d56e7e1cb345632ad592e8c51b693d7b7556043ce0", size = 3459976 }, - { url = "https://files.pythonhosted.org/packages/26/da/2c11d03b765efff0ccc473f1c4186dc2770110464f2177efaed9cf6fae01/pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bf2c33d6791c598142f00c9c4c7d47f6476731c31081331664eb26d6ab583e01", size = 3527133 }, - { url = "https://files.pythonhosted.org/packages/79/1a/4e85bd7cadf78412c2a3069249a09c32ef3323650fd3005c97cca7aa21df/pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e616e7154c37669fc1dfc14584f11e284e05d1c650e1c0f972f281c4ccc53193", size = 3571555 }, - { url = "https://files.pythonhosted.org/packages/69/03/239939915216de1e95e0ce2334bf17a7870ae185eb390fab6d706aadbfc0/pillow-11.2.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:39ad2e0f424394e3aebc40168845fee52df1394a4673a6ee512d840d14ab3013", size = 2674713 }, - { url = "https://files.pythonhosted.org/packages/a4/ad/2613c04633c7257d9481ab21d6b5364b59fc5d75faafd7cb8693523945a3/pillow-11.2.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:80f1df8dbe9572b4b7abdfa17eb5d78dd620b1d55d9e25f834efdbee872d3aed", size = 3181734 }, - { url = "https://files.pythonhosted.org/packages/a4/fd/dcdda4471ed667de57bb5405bb42d751e6cfdd4011a12c248b455c778e03/pillow-11.2.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ea926cfbc3957090becbcbbb65ad177161a2ff2ad578b5a6ec9bb1e1cd78753c", size = 2999841 }, - { url = "https://files.pythonhosted.org/packages/ac/89/8a2536e95e77432833f0db6fd72a8d310c8e4272a04461fb833eb021bf94/pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:738db0e0941ca0376804d4de6a782c005245264edaa253ffce24e5a15cbdc7bd", size = 3437470 }, - { url = "https://files.pythonhosted.org/packages/9d/8f/abd47b73c60712f88e9eda32baced7bfc3e9bd6a7619bb64b93acff28c3e/pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db98ab6565c69082ec9b0d4e40dd9f6181dab0dd236d26f7a50b8b9bfbd5076", size = 3460013 }, - { url = "https://files.pythonhosted.org/packages/f6/20/5c0a0aa83b213b7a07ec01e71a3d6ea2cf4ad1d2c686cc0168173b6089e7/pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:036e53f4170e270ddb8797d4c590e6dd14d28e15c7da375c18978045f7e6c37b", size = 3527165 }, - { url = "https://files.pythonhosted.org/packages/58/0e/2abab98a72202d91146abc839e10c14f7cf36166f12838ea0c4db3ca6ecb/pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:14f73f7c291279bd65fda51ee87affd7c1e097709f7fdd0188957a16c264601f", size = 3571586 }, - { url = "https://files.pythonhosted.org/packages/21/2c/5e05f58658cf49b6667762cca03d6e7d85cededde2caf2ab37b81f80e574/pillow-11.2.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:208653868d5c9ecc2b327f9b9ef34e0e42a4cdd172c2988fd81d62d2bc9bc044", size = 2674751 }, +sdist = { url = "https://files.pythonhosted.org/packages/af/cb/bb5c01fcd2a69335b86c22142b2bccfc3464087efb7fd382eee5ffc7fdf7/pillow-11.2.1.tar.gz", hash = "sha256:a64dd61998416367b7ef979b73d3a85853ba9bec4c2925f74e588879a58716b6", size = 47026707, upload-time = "2025-04-12T17:50:03.289Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/8b/b158ad57ed44d3cc54db8d68ad7c0a58b8fc0e4c7a3f995f9d62d5b464a1/pillow-11.2.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:d57a75d53922fc20c165016a20d9c44f73305e67c351bbc60d1adaf662e74047", size = 3198442, upload-time = "2025-04-12T17:47:10.666Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f8/bb5d956142f86c2d6cc36704943fa761f2d2e4c48b7436fd0a85c20f1713/pillow-11.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:127bf6ac4a5b58b3d32fc8289656f77f80567d65660bc46f72c0d77e6600cc95", size = 3030553, upload-time = "2025-04-12T17:47:13.153Z" }, + { url = "https://files.pythonhosted.org/packages/22/7f/0e413bb3e2aa797b9ca2c5c38cb2e2e45d88654e5b12da91ad446964cfae/pillow-11.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4ba4be812c7a40280629e55ae0b14a0aafa150dd6451297562e1764808bbe61", size = 4405503, upload-time = "2025-04-12T17:47:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/f3/b4/cc647f4d13f3eb837d3065824aa58b9bcf10821f029dc79955ee43f793bd/pillow-11.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8bd62331e5032bc396a93609982a9ab6b411c05078a52f5fe3cc59234a3abd1", size = 4490648, upload-time = "2025-04-12T17:47:17.37Z" }, + { url = "https://files.pythonhosted.org/packages/c2/6f/240b772a3b35cdd7384166461567aa6713799b4e78d180c555bd284844ea/pillow-11.2.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:562d11134c97a62fe3af29581f083033179f7ff435f78392565a1ad2d1c2c45c", size = 4508937, upload-time = "2025-04-12T17:47:19.066Z" }, + { url = "https://files.pythonhosted.org/packages/f3/5e/7ca9c815ade5fdca18853db86d812f2f188212792780208bdb37a0a6aef4/pillow-11.2.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:c97209e85b5be259994eb5b69ff50c5d20cca0f458ef9abd835e262d9d88b39d", size = 4599802, upload-time = "2025-04-12T17:47:21.404Z" }, + { url = "https://files.pythonhosted.org/packages/02/81/c3d9d38ce0c4878a77245d4cf2c46d45a4ad0f93000227910a46caff52f3/pillow-11.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0c3e6d0f59171dfa2e25d7116217543310908dfa2770aa64b8f87605f8cacc97", size = 4576717, upload-time = "2025-04-12T17:47:23.571Z" }, + { url = "https://files.pythonhosted.org/packages/42/49/52b719b89ac7da3185b8d29c94d0e6aec8140059e3d8adcaa46da3751180/pillow-11.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc1c3bc53befb6096b84165956e886b1729634a799e9d6329a0c512ab651e579", size = 4654874, upload-time = "2025-04-12T17:47:25.783Z" }, + { url = "https://files.pythonhosted.org/packages/5b/0b/ede75063ba6023798267023dc0d0401f13695d228194d2242d5a7ba2f964/pillow-11.2.1-cp310-cp310-win32.whl", hash = "sha256:312c77b7f07ab2139924d2639860e084ec2a13e72af54d4f08ac843a5fc9c79d", size = 2331717, upload-time = "2025-04-12T17:47:28.922Z" }, + { url = "https://files.pythonhosted.org/packages/ed/3c/9831da3edea527c2ed9a09f31a2c04e77cd705847f13b69ca60269eec370/pillow-11.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:9bc7ae48b8057a611e5fe9f853baa88093b9a76303937449397899385da06fad", size = 2676204, upload-time = "2025-04-12T17:47:31.283Z" }, + { url = "https://files.pythonhosted.org/packages/01/97/1f66ff8a1503d8cbfc5bae4dc99d54c6ec1e22ad2b946241365320caabc2/pillow-11.2.1-cp310-cp310-win_arm64.whl", hash = "sha256:2728567e249cdd939f6cc3d1f049595c66e4187f3c34078cbc0a7d21c47482d2", size = 2414767, upload-time = "2025-04-12T17:47:34.655Z" }, + { url = "https://files.pythonhosted.org/packages/68/08/3fbf4b98924c73037a8e8b4c2c774784805e0fb4ebca6c5bb60795c40125/pillow-11.2.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:35ca289f712ccfc699508c4658a1d14652e8033e9b69839edf83cbdd0ba39e70", size = 3198450, upload-time = "2025-04-12T17:47:37.135Z" }, + { url = "https://files.pythonhosted.org/packages/84/92/6505b1af3d2849d5e714fc75ba9e69b7255c05ee42383a35a4d58f576b16/pillow-11.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0409af9f829f87a2dfb7e259f78f317a5351f2045158be321fd135973fff7bf", size = 3030550, upload-time = "2025-04-12T17:47:39.345Z" }, + { url = "https://files.pythonhosted.org/packages/3c/8c/ac2f99d2a70ff966bc7eb13dacacfaab57c0549b2ffb351b6537c7840b12/pillow-11.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4e5c5edee874dce4f653dbe59db7c73a600119fbea8d31f53423586ee2aafd7", size = 4415018, upload-time = "2025-04-12T17:47:41.128Z" }, + { url = "https://files.pythonhosted.org/packages/1f/e3/0a58b5d838687f40891fff9cbaf8669f90c96b64dc8f91f87894413856c6/pillow-11.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b93a07e76d13bff9444f1a029e0af2964e654bfc2e2c2d46bfd080df5ad5f3d8", size = 4498006, upload-time = "2025-04-12T17:47:42.912Z" }, + { url = "https://files.pythonhosted.org/packages/21/f5/6ba14718135f08fbfa33308efe027dd02b781d3f1d5c471444a395933aac/pillow-11.2.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:e6def7eed9e7fa90fde255afaf08060dc4b343bbe524a8f69bdd2a2f0018f600", size = 4517773, upload-time = "2025-04-12T17:47:44.611Z" }, + { url = "https://files.pythonhosted.org/packages/20/f2/805ad600fc59ebe4f1ba6129cd3a75fb0da126975c8579b8f57abeb61e80/pillow-11.2.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:8f4f3724c068be008c08257207210c138d5f3731af6c155a81c2b09a9eb3a788", size = 4607069, upload-time = "2025-04-12T17:47:46.46Z" }, + { url = "https://files.pythonhosted.org/packages/71/6b/4ef8a288b4bb2e0180cba13ca0a519fa27aa982875882392b65131401099/pillow-11.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a0a6709b47019dff32e678bc12c63008311b82b9327613f534e496dacaefb71e", size = 4583460, upload-time = "2025-04-12T17:47:49.255Z" }, + { url = "https://files.pythonhosted.org/packages/62/ae/f29c705a09cbc9e2a456590816e5c234382ae5d32584f451c3eb41a62062/pillow-11.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f6b0c664ccb879109ee3ca702a9272d877f4fcd21e5eb63c26422fd6e415365e", size = 4661304, upload-time = "2025-04-12T17:47:51.067Z" }, + { url = "https://files.pythonhosted.org/packages/6e/1a/c8217b6f2f73794a5e219fbad087701f412337ae6dbb956db37d69a9bc43/pillow-11.2.1-cp311-cp311-win32.whl", hash = "sha256:cc5d875d56e49f112b6def6813c4e3d3036d269c008bf8aef72cd08d20ca6df6", size = 2331809, upload-time = "2025-04-12T17:47:54.425Z" }, + { url = "https://files.pythonhosted.org/packages/e2/72/25a8f40170dc262e86e90f37cb72cb3de5e307f75bf4b02535a61afcd519/pillow-11.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:0f5c7eda47bf8e3c8a283762cab94e496ba977a420868cb819159980b6709193", size = 2676338, upload-time = "2025-04-12T17:47:56.535Z" }, + { url = "https://files.pythonhosted.org/packages/06/9e/76825e39efee61efea258b479391ca77d64dbd9e5804e4ad0fa453b4ba55/pillow-11.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:4d375eb838755f2528ac8cbc926c3e31cc49ca4ad0cf79cff48b20e30634a4a7", size = 2414918, upload-time = "2025-04-12T17:47:58.217Z" }, + { url = "https://files.pythonhosted.org/packages/c7/40/052610b15a1b8961f52537cc8326ca6a881408bc2bdad0d852edeb6ed33b/pillow-11.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:78afba22027b4accef10dbd5eed84425930ba41b3ea0a86fa8d20baaf19d807f", size = 3190185, upload-time = "2025-04-12T17:48:00.417Z" }, + { url = "https://files.pythonhosted.org/packages/e5/7e/b86dbd35a5f938632093dc40d1682874c33dcfe832558fc80ca56bfcb774/pillow-11.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78092232a4ab376a35d68c4e6d5e00dfd73454bd12b230420025fbe178ee3b0b", size = 3030306, upload-time = "2025-04-12T17:48:02.391Z" }, + { url = "https://files.pythonhosted.org/packages/a4/5c/467a161f9ed53e5eab51a42923c33051bf8d1a2af4626ac04f5166e58e0c/pillow-11.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25a5f306095c6780c52e6bbb6109624b95c5b18e40aab1c3041da3e9e0cd3e2d", size = 4416121, upload-time = "2025-04-12T17:48:04.554Z" }, + { url = "https://files.pythonhosted.org/packages/62/73/972b7742e38ae0e2ac76ab137ca6005dcf877480da0d9d61d93b613065b4/pillow-11.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c7b29dbd4281923a2bfe562acb734cee96bbb129e96e6972d315ed9f232bef4", size = 4501707, upload-time = "2025-04-12T17:48:06.831Z" }, + { url = "https://files.pythonhosted.org/packages/e4/3a/427e4cb0b9e177efbc1a84798ed20498c4f233abde003c06d2650a6d60cb/pillow-11.2.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3e645b020f3209a0181a418bffe7b4a93171eef6c4ef6cc20980b30bebf17b7d", size = 4522921, upload-time = "2025-04-12T17:48:09.229Z" }, + { url = "https://files.pythonhosted.org/packages/fe/7c/d8b1330458e4d2f3f45d9508796d7caf0c0d3764c00c823d10f6f1a3b76d/pillow-11.2.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b2dbea1012ccb784a65349f57bbc93730b96e85b42e9bf7b01ef40443db720b4", size = 4612523, upload-time = "2025-04-12T17:48:11.631Z" }, + { url = "https://files.pythonhosted.org/packages/b3/2f/65738384e0b1acf451de5a573d8153fe84103772d139e1e0bdf1596be2ea/pillow-11.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:da3104c57bbd72948d75f6a9389e6727d2ab6333c3617f0a89d72d4940aa0443", size = 4587836, upload-time = "2025-04-12T17:48:13.592Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c5/e795c9f2ddf3debb2dedd0df889f2fe4b053308bb59a3cc02a0cd144d641/pillow-11.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:598174aef4589af795f66f9caab87ba4ff860ce08cd5bb447c6fc553ffee603c", size = 4669390, upload-time = "2025-04-12T17:48:15.938Z" }, + { url = "https://files.pythonhosted.org/packages/96/ae/ca0099a3995976a9fce2f423166f7bff9b12244afdc7520f6ed38911539a/pillow-11.2.1-cp312-cp312-win32.whl", hash = "sha256:1d535df14716e7f8776b9e7fee118576d65572b4aad3ed639be9e4fa88a1cad3", size = 2332309, upload-time = "2025-04-12T17:48:17.885Z" }, + { url = "https://files.pythonhosted.org/packages/7c/18/24bff2ad716257fc03da964c5e8f05d9790a779a8895d6566e493ccf0189/pillow-11.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:14e33b28bf17c7a38eede290f77db7c664e4eb01f7869e37fa98a5aa95978941", size = 2676768, upload-time = "2025-04-12T17:48:19.655Z" }, + { url = "https://files.pythonhosted.org/packages/da/bb/e8d656c9543276517ee40184aaa39dcb41e683bca121022f9323ae11b39d/pillow-11.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:21e1470ac9e5739ff880c211fc3af01e3ae505859392bf65458c224d0bf283eb", size = 2415087, upload-time = "2025-04-12T17:48:21.991Z" }, + { url = "https://files.pythonhosted.org/packages/21/3a/c1835d1c7cf83559e95b4f4ed07ab0bb7acc689712adfce406b3f456e9fd/pillow-11.2.1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:7491cf8a79b8eb867d419648fff2f83cb0b3891c8b36da92cc7f1931d46108c8", size = 3198391, upload-time = "2025-04-12T17:49:10.122Z" }, + { url = "https://files.pythonhosted.org/packages/b6/4d/dcb7a9af3fc1e8653267c38ed622605d9d1793349274b3ef7af06457e257/pillow-11.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8b02d8f9cb83c52578a0b4beadba92e37d83a4ef11570a8688bbf43f4ca50909", size = 3030573, upload-time = "2025-04-12T17:49:11.938Z" }, + { url = "https://files.pythonhosted.org/packages/9d/29/530ca098c1a1eb31d4e163d317d0e24e6d2ead907991c69ca5b663de1bc5/pillow-11.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:014ca0050c85003620526b0ac1ac53f56fc93af128f7546623cc8e31875ab928", size = 4398677, upload-time = "2025-04-12T17:49:13.861Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ee/0e5e51db34de1690264e5f30dcd25328c540aa11d50a3bc0b540e2a445b6/pillow-11.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3692b68c87096ac6308296d96354eddd25f98740c9d2ab54e1549d6c8aea9d79", size = 4484986, upload-time = "2025-04-12T17:49:15.948Z" }, + { url = "https://files.pythonhosted.org/packages/93/7d/bc723b41ce3d2c28532c47678ec988974f731b5c6fadd5b3a4fba9015e4f/pillow-11.2.1-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:f781dcb0bc9929adc77bad571b8621ecb1e4cdef86e940fe2e5b5ee24fd33b35", size = 4501897, upload-time = "2025-04-12T17:49:17.839Z" }, + { url = "https://files.pythonhosted.org/packages/be/0b/532e31abc7389617ddff12551af625a9b03cd61d2989fa595e43c470ec67/pillow-11.2.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:2b490402c96f907a166615e9a5afacf2519e28295f157ec3a2bb9bd57de638cb", size = 4592618, upload-time = "2025-04-12T17:49:19.7Z" }, + { url = "https://files.pythonhosted.org/packages/4c/f0/21ed6499a6216fef753e2e2254a19d08bff3747108ba042422383f3e9faa/pillow-11.2.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dd6b20b93b3ccc9c1b597999209e4bc5cf2853f9ee66e3fc9a400a78733ffc9a", size = 4570493, upload-time = "2025-04-12T17:49:21.703Z" }, + { url = "https://files.pythonhosted.org/packages/68/de/17004ddb8ab855573fe1127ab0168d11378cdfe4a7ee2a792a70ff2e9ba7/pillow-11.2.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:4b835d89c08a6c2ee7781b8dd0a30209a8012b5f09c0a665b65b0eb3560b6f36", size = 4647748, upload-time = "2025-04-12T17:49:23.579Z" }, + { url = "https://files.pythonhosted.org/packages/c7/23/82ecb486384bb3578115c509d4a00bb52f463ee700a5ca1be53da3c88c19/pillow-11.2.1-cp39-cp39-win32.whl", hash = "sha256:b10428b3416d4f9c61f94b494681280be7686bda15898a3a9e08eb66a6d92d67", size = 2331731, upload-time = "2025-04-12T17:49:25.58Z" }, + { url = "https://files.pythonhosted.org/packages/58/bb/87efd58b3689537a623d44dbb2550ef0bb5ff6a62769707a0fe8b1a7bdeb/pillow-11.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:6ebce70c3f486acf7591a3d73431fa504a4e18a9b97ff27f5f47b7368e4b9dd1", size = 2676346, upload-time = "2025-04-12T17:49:27.342Z" }, + { url = "https://files.pythonhosted.org/packages/80/08/dc268475b22887b816e5dcfae31bce897f524b4646bab130c2142c9b2400/pillow-11.2.1-cp39-cp39-win_arm64.whl", hash = "sha256:c27476257b2fdcd7872d54cfd119b3a9ce4610fb85c8e32b70b42e3680a29a1e", size = 2414623, upload-time = "2025-04-12T17:49:29.139Z" }, + { url = "https://files.pythonhosted.org/packages/33/49/c8c21e4255b4f4a2c0c68ac18125d7f5460b109acc6dfdef1a24f9b960ef/pillow-11.2.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:9b7b0d4fd2635f54ad82785d56bc0d94f147096493a79985d0ab57aedd563156", size = 3181727, upload-time = "2025-04-12T17:49:31.898Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f1/f7255c0838f8c1ef6d55b625cfb286835c17e8136ce4351c5577d02c443b/pillow-11.2.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:aa442755e31c64037aa7c1cb186e0b369f8416c567381852c63444dd666fb772", size = 2999833, upload-time = "2025-04-12T17:49:34.2Z" }, + { url = "https://files.pythonhosted.org/packages/e2/57/9968114457bd131063da98d87790d080366218f64fa2943b65ac6739abb3/pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0d3348c95b766f54b76116d53d4cb171b52992a1027e7ca50c81b43b9d9e363", size = 3437472, upload-time = "2025-04-12T17:49:36.294Z" }, + { url = "https://files.pythonhosted.org/packages/b2/1b/e35d8a158e21372ecc48aac9c453518cfe23907bb82f950d6e1c72811eb0/pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85d27ea4c889342f7e35f6d56e7e1cb345632ad592e8c51b693d7b7556043ce0", size = 3459976, upload-time = "2025-04-12T17:49:38.988Z" }, + { url = "https://files.pythonhosted.org/packages/26/da/2c11d03b765efff0ccc473f1c4186dc2770110464f2177efaed9cf6fae01/pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bf2c33d6791c598142f00c9c4c7d47f6476731c31081331664eb26d6ab583e01", size = 3527133, upload-time = "2025-04-12T17:49:40.985Z" }, + { url = "https://files.pythonhosted.org/packages/79/1a/4e85bd7cadf78412c2a3069249a09c32ef3323650fd3005c97cca7aa21df/pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e616e7154c37669fc1dfc14584f11e284e05d1c650e1c0f972f281c4ccc53193", size = 3571555, upload-time = "2025-04-12T17:49:42.964Z" }, + { url = "https://files.pythonhosted.org/packages/69/03/239939915216de1e95e0ce2334bf17a7870ae185eb390fab6d706aadbfc0/pillow-11.2.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:39ad2e0f424394e3aebc40168845fee52df1394a4673a6ee512d840d14ab3013", size = 2674713, upload-time = "2025-04-12T17:49:44.944Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ad/2613c04633c7257d9481ab21d6b5364b59fc5d75faafd7cb8693523945a3/pillow-11.2.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:80f1df8dbe9572b4b7abdfa17eb5d78dd620b1d55d9e25f834efdbee872d3aed", size = 3181734, upload-time = "2025-04-12T17:49:46.789Z" }, + { url = "https://files.pythonhosted.org/packages/a4/fd/dcdda4471ed667de57bb5405bb42d751e6cfdd4011a12c248b455c778e03/pillow-11.2.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ea926cfbc3957090becbcbbb65ad177161a2ff2ad578b5a6ec9bb1e1cd78753c", size = 2999841, upload-time = "2025-04-12T17:49:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/ac/89/8a2536e95e77432833f0db6fd72a8d310c8e4272a04461fb833eb021bf94/pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:738db0e0941ca0376804d4de6a782c005245264edaa253ffce24e5a15cbdc7bd", size = 3437470, upload-time = "2025-04-12T17:49:50.831Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8f/abd47b73c60712f88e9eda32baced7bfc3e9bd6a7619bb64b93acff28c3e/pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db98ab6565c69082ec9b0d4e40dd9f6181dab0dd236d26f7a50b8b9bfbd5076", size = 3460013, upload-time = "2025-04-12T17:49:53.278Z" }, + { url = "https://files.pythonhosted.org/packages/f6/20/5c0a0aa83b213b7a07ec01e71a3d6ea2cf4ad1d2c686cc0168173b6089e7/pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:036e53f4170e270ddb8797d4c590e6dd14d28e15c7da375c18978045f7e6c37b", size = 3527165, upload-time = "2025-04-12T17:49:55.164Z" }, + { url = "https://files.pythonhosted.org/packages/58/0e/2abab98a72202d91146abc839e10c14f7cf36166f12838ea0c4db3ca6ecb/pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:14f73f7c291279bd65fda51ee87affd7c1e097709f7fdd0188957a16c264601f", size = 3571586, upload-time = "2025-04-12T17:49:57.171Z" }, + { url = "https://files.pythonhosted.org/packages/21/2c/5e05f58658cf49b6667762cca03d6e7d85cededde2caf2ab37b81f80e574/pillow-11.2.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:208653868d5c9ecc2b327f9b9ef34e0e42a4cdd172c2988fd81d62d2bc9bc044", size = 2674751, upload-time = "2025-04-12T17:49:59.628Z" }, ] [[package]] name = "pip" version = "25.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/70/53/b309b4a497b09655cb7e07088966881a57d082f48ac3cb54ea729fd2c6cf/pip-25.0.1.tar.gz", hash = "sha256:88f96547ea48b940a3a385494e181e29fb8637898f88d88737c5049780f196ea", size = 1950850 } +sdist = { url = "https://files.pythonhosted.org/packages/70/53/b309b4a497b09655cb7e07088966881a57d082f48ac3cb54ea729fd2c6cf/pip-25.0.1.tar.gz", hash = "sha256:88f96547ea48b940a3a385494e181e29fb8637898f88d88737c5049780f196ea", size = 1950850, upload-time = "2025-02-09T17:14:04.423Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c9/bc/b7db44f5f39f9d0494071bddae6880eb645970366d0a200022a1a93d57f5/pip-25.0.1-py3-none-any.whl", hash = "sha256:c46efd13b6aa8279f33f2864459c8ce587ea6a1a59ee20de055868d8f7688f7f", size = 1841526 }, + { url = "https://files.pythonhosted.org/packages/c9/bc/b7db44f5f39f9d0494071bddae6880eb645970366d0a200022a1a93d57f5/pip-25.0.1-py3-none-any.whl", hash = "sha256:c46efd13b6aa8279f33f2864459c8ce587ea6a1a59ee20de055868d8f7688f7f", size = 1841526, upload-time = "2025-02-09T17:14:01.463Z" }, ] [[package]] @@ -1265,27 +1296,27 @@ dependencies = [ { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "wheel" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1a/87/1ef453f10fb0772f43549686f924460cc0a2404b828b348f72c52cb2f5bf/pip-tools-7.4.1.tar.gz", hash = "sha256:864826f5073864450e24dbeeb85ce3920cdfb09848a3d69ebf537b521f14bcc9", size = 145417 } +sdist = { url = "https://files.pythonhosted.org/packages/1a/87/1ef453f10fb0772f43549686f924460cc0a2404b828b348f72c52cb2f5bf/pip-tools-7.4.1.tar.gz", hash = "sha256:864826f5073864450e24dbeeb85ce3920cdfb09848a3d69ebf537b521f14bcc9", size = 145417, upload-time = "2024-03-06T12:13:23.533Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/dc/38f4ce065e92c66f058ea7a368a9c5de4e702272b479c0992059f7693941/pip_tools-7.4.1-py3-none-any.whl", hash = "sha256:4c690e5fbae2f21e87843e89c26191f0d9454f362d8acdbd695716493ec8b3a9", size = 61235 }, + { url = "https://files.pythonhosted.org/packages/0d/dc/38f4ce065e92c66f058ea7a368a9c5de4e702272b479c0992059f7693941/pip_tools-7.4.1-py3-none-any.whl", hash = "sha256:4c690e5fbae2f21e87843e89c26191f0d9454f362d8acdbd695716493ec8b3a9", size = 61235, upload-time = "2024-03-06T12:13:40.124Z" }, ] [[package]] name = "platformdirs" version = "4.3.7" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b6/2d/7d512a3913d60623e7eb945c6d1b4f0bddf1d0b7ada5225274c87e5b53d1/platformdirs-4.3.7.tar.gz", hash = "sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351", size = 21291 } +sdist = { url = "https://files.pythonhosted.org/packages/b6/2d/7d512a3913d60623e7eb945c6d1b4f0bddf1d0b7ada5225274c87e5b53d1/platformdirs-4.3.7.tar.gz", hash = "sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351", size = 21291, upload-time = "2025-03-19T20:36:10.989Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6d/45/59578566b3275b8fd9157885918fcd0c4d74162928a5310926887b856a51/platformdirs-4.3.7-py3-none-any.whl", hash = "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94", size = 18499 }, + { url = "https://files.pythonhosted.org/packages/6d/45/59578566b3275b8fd9157885918fcd0c4d74162928a5310926887b856a51/platformdirs-4.3.7-py3-none-any.whl", hash = "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94", size = 18499, upload-time = "2025-03-19T20:36:09.038Z" }, ] [[package]] name = "pluggy" version = "1.5.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955, upload-time = "2024-04-20T21:34:42.531Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556, upload-time = "2024-04-20T21:34:40.434Z" }, ] [[package]] @@ -1297,108 +1328,164 @@ dependencies = [ { name = "platformdirs" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c6/77/b3d3e00c696c16cf99af81ef7b1f5fe73bd2a307abca41bd7605429fe6e5/pooch-1.8.2.tar.gz", hash = "sha256:76561f0de68a01da4df6af38e9955c4c9d1a5c90da73f7e40276a5728ec83d10", size = 59353 } +sdist = { url = "https://files.pythonhosted.org/packages/c6/77/b3d3e00c696c16cf99af81ef7b1f5fe73bd2a307abca41bd7605429fe6e5/pooch-1.8.2.tar.gz", hash = "sha256:76561f0de68a01da4df6af38e9955c4c9d1a5c90da73f7e40276a5728ec83d10", size = 59353, upload-time = "2024-06-06T16:53:46.224Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/87/77cc11c7a9ea9fd05503def69e3d18605852cd0d4b0d3b8f15bbeb3ef1d1/pooch-1.8.2-py3-none-any.whl", hash = "sha256:3529a57096f7198778a5ceefd5ac3ef0e4d06a6ddaf9fc2d609b806f25302c47", size = 64574 }, + { url = "https://files.pythonhosted.org/packages/a8/87/77cc11c7a9ea9fd05503def69e3d18605852cd0d4b0d3b8f15bbeb3ef1d1/pooch-1.8.2-py3-none-any.whl", hash = "sha256:3529a57096f7198778a5ceefd5ac3ef0e4d06a6ddaf9fc2d609b806f25302c47", size = 64574, upload-time = "2024-06-06T16:53:44.343Z" }, ] [[package]] name = "pycodestyle" version = "2.13.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/04/6e/1f4a62078e4d95d82367f24e685aef3a672abfd27d1a868068fed4ed2254/pycodestyle-2.13.0.tar.gz", hash = "sha256:c8415bf09abe81d9c7f872502a6eee881fbe85d8763dd5b9924bb0a01d67efae", size = 39312 } +sdist = { url = "https://files.pythonhosted.org/packages/04/6e/1f4a62078e4d95d82367f24e685aef3a672abfd27d1a868068fed4ed2254/pycodestyle-2.13.0.tar.gz", hash = "sha256:c8415bf09abe81d9c7f872502a6eee881fbe85d8763dd5b9924bb0a01d67efae", size = 39312, upload-time = "2025-03-29T17:33:30.669Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/07/be/b00116df1bfb3e0bb5b45e29d604799f7b91dd861637e4d448b4e09e6a3e/pycodestyle-2.13.0-py2.py3-none-any.whl", hash = "sha256:35863c5974a271c7a726ed228a14a4f6daf49df369d8c50cd9a6f58a5e143ba9", size = 31424 }, + { url = "https://files.pythonhosted.org/packages/07/be/b00116df1bfb3e0bb5b45e29d604799f7b91dd861637e4d448b4e09e6a3e/pycodestyle-2.13.0-py2.py3-none-any.whl", hash = "sha256:35863c5974a271c7a726ed228a14a4f6daf49df369d8c50cd9a6f58a5e143ba9", size = 31424, upload-time = "2025-03-29T17:33:29.405Z" }, ] [[package]] name = "pycparser" version = "2.22" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 } +sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736, upload-time = "2024-03-30T13:22:22.564Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 }, + { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" }, ] [[package]] -name = "pyflakes" -version = "3.3.2" +name = "pydantic" +version = "2.13.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/af/cc/1df338bd7ed1fa7c317081dcf29bf2f01266603b301e6858856d346a12b3/pyflakes-3.3.2.tar.gz", hash = "sha256:6dfd61d87b97fba5dcfaaf781171ac16be16453be6d816147989e7f6e6a9576b", size = 64175 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/15/40/b293a4fa769f3b02ab9e387c707c4cbdc34f073f945de0386107d4e669e6/pyflakes-3.3.2-py2.py3-none-any.whl", hash = "sha256:5039c8339cbb1944045f4ee5466908906180f13cc99cc9949348d10f82a5c32a", size = 63164 }, +dependencies = [ + { name = "annotated-types", version = "0.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "annotated-types", version = "0.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, ] - -[[package]] -name = "pygments" -version = "2.19.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 } +sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, + { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, ] [[package]] -name = "pyparsing" -version = "3.2.3" +name = "pydantic-core" +version = "2.46.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bb/22/f1129e69d94ffff626bdb5c835506b3a5b4f3d070f17ea295e12c2c6f60f/pyparsing-3.2.3.tar.gz", hash = "sha256:b9c13f1ab8b3b542f72e28f634bad4de758ab3ce4546e4301970ad6fa77c38be", size = 1088608 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/05/e7/df2285f3d08fee213f2d041540fa4fc9ca6c2d44cf36d3a035bf2a8d2bcc/pyparsing-3.2.3-py3-none-any.whl", hash = "sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf", size = 111120 }, +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/08/f1ba952f1c8ae5581c70fa9c6da89f247b83e3dd8c09c035d5d7931fc23d/pydantic_core-2.46.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4", size = 2113146, upload-time = "2026-05-06T13:37:36.537Z" }, + { url = "https://files.pythonhosted.org/packages/56/c6/65f646c7ff09bd257f660434adb45c4dfcbbcebcc030562fecf6f5bf887d/pydantic_core-2.46.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5", size = 1949769, upload-time = "2026-05-06T13:37:46.365Z" }, + { url = "https://files.pythonhosted.org/packages/64/ba/bfb1d928fd5b49e1258935ff104ae356e9fd89384a55bf9f847e9193ad40/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba", size = 1974958, upload-time = "2026-05-06T13:37:28.611Z" }, + { url = "https://files.pythonhosted.org/packages/4e/74/76223bfb117b64af743c9b6670d1364516f5c0604f96b48f3272f6af6cc6/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b", size = 2042118, upload-time = "2026-05-06T13:36:55.216Z" }, + { url = "https://files.pythonhosted.org/packages/cb/7b/848732968bc8f48f3187542f08358b9d842db564147b256669426ebb1652/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c", size = 2222876, upload-time = "2026-05-06T13:38:25.455Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2f/e90b63ee2e14bd8d3db8f705a6d75d64e6ee1b7c2c8833747ce706e1e0ce/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50", size = 2286703, upload-time = "2026-05-06T13:37:53.304Z" }, + { url = "https://files.pythonhosted.org/packages/ba/1e/acc4d70f88a0a277e4a1fa77ebb985ceabaf900430f875bf9338e11c9420/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd", size = 2092042, upload-time = "2026-05-06T13:38:46.981Z" }, + { url = "https://files.pythonhosted.org/packages/a9/da/0a422b57bf8504102bf3c4ccea9c41bab5a5cee6a54650acf8faf67f5a24/pydantic_core-2.46.4-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01", size = 2117231, upload-time = "2026-05-06T13:39:23.146Z" }, + { url = "https://files.pythonhosted.org/packages/bd/2a/2ac13c3af305843e23c5078c53d135656b3f05a2fd78cb7bbbb12e97b473/pydantic_core-2.46.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d", size = 2168388, upload-time = "2026-05-06T13:40:08.06Z" }, + { url = "https://files.pythonhosted.org/packages/72/04/2beacf7e1607e93eefe4aed1b4709f079b905fb77530179d4f7c71745f22/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4", size = 2184769, upload-time = "2026-05-06T13:38:13.901Z" }, + { url = "https://files.pythonhosted.org/packages/9e/29/d2b9fd9f539133548eaf622c06a4ce176cb46ac59f32d0359c4abc0de047/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f", size = 2319312, upload-time = "2026-05-06T13:39:08.24Z" }, + { url = "https://files.pythonhosted.org/packages/7c/af/0f7a5b85fec6075bea96e3ef9187de38fccced0de92c1e7feda8d5cc7bb9/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39", size = 2361817, upload-time = "2026-05-06T13:38:43.2Z" }, + { url = "https://files.pythonhosted.org/packages/25/a4/73363fec545fd3ec025490bdda2743c56d0dd5b6266b1a53bbe9e4265375/pydantic_core-2.46.4-cp310-cp310-win32.whl", hash = "sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d", size = 1987085, upload-time = "2026-05-06T13:39:25.497Z" }, + { url = "https://files.pythonhosted.org/packages/01/aa/62f082da2c91fac1c234bc9ee0066257ce83f0604abd72e4c9d5991f2d84/pydantic_core-2.46.4-cp310-cp310-win_amd64.whl", hash = "sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf", size = 2074311, upload-time = "2026-05-06T13:39:59.922Z" }, + { url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872, upload-time = "2026-05-06T13:40:27.596Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" }, + { url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" }, + { url = "https://files.pythonhosted.org/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size = 2041051, upload-time = "2026-05-06T13:38:10.447Z" }, + { url = "https://files.pythonhosted.org/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size = 2221314, upload-time = "2026-05-06T13:40:13.089Z" }, + { url = "https://files.pythonhosted.org/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size = 2285146, upload-time = "2026-05-06T13:38:59.224Z" }, + { url = "https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size = 2089685, upload-time = "2026-05-06T13:38:17.762Z" }, + { url = "https://files.pythonhosted.org/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size = 2115420, upload-time = "2026-05-06T13:37:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3", size = 2165122, upload-time = "2026-05-06T13:37:01.167Z" }, + { url = "https://files.pythonhosted.org/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size = 2182573, upload-time = "2026-05-06T13:38:45.04Z" }, + { url = "https://files.pythonhosted.org/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size = 2317139, upload-time = "2026-05-06T13:37:15.539Z" }, + { url = "https://files.pythonhosted.org/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size = 2360433, upload-time = "2026-05-06T13:37:30.099Z" }, + { url = "https://files.pythonhosted.org/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda", size = 1985513, upload-time = "2026-05-06T13:38:15.669Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33", size = 2071114, upload-time = "2026-05-06T13:40:35.416Z" }, + { url = "https://files.pythonhosted.org/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d", size = 2044298, upload-time = "2026-05-06T13:38:29.754Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, + { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, + { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, + { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, + { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, + { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, + { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, + { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, + { url = "https://files.pythonhosted.org/packages/5d/00/13a0c039569d1e583779ee1b8d7df6bfe275a0db83fcae14f01d6856c16e/pydantic_core-2.46.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:fd8b3d9fd264be37976686c7f65cd52a83f5e84f4bfd2adf9c1d469676bbb6ae", size = 2115337, upload-time = "2026-05-06T13:38:37.741Z" }, + { url = "https://files.pythonhosted.org/packages/41/60/e70fa1ee03e243bdfd4b1fddf1e1f2a8fba681df3034b51b9376c0fb5bf5/pydantic_core-2.46.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9f444c499b3eefd3a92e348059471ea0c3a6e303d9c1cec09fa748fd9f895201", size = 1957976, upload-time = "2026-05-06T13:37:33.478Z" }, + { url = "https://files.pythonhosted.org/packages/11/9a/78fb5f2ea849f767ea802de8b4e8f5a0c4a48ddbe4bc66bd19ac2f55a01c/pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3447661d99f75a3683a4cf5c87da72f2161964611864dbbeac7fbb118bb4bfc0", size = 1979390, upload-time = "2026-05-06T13:36:52.419Z" }, + { url = "https://files.pythonhosted.org/packages/f5/7d/3acfdcd000bad9735de0430a88355948469781f62cb841fd63e8a307e80e/pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8b9bab013d1c7a79d3501ff86d0bc9c31bf587db4551677b96bec07df78c6b15", size = 2043263, upload-time = "2026-05-06T13:39:54.798Z" }, + { url = "https://files.pythonhosted.org/packages/35/60/1325e5a8d7f9697416481c7f7c1c304738d6b961a7fd1ea0f054ce0f14fb/pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d995260fdf4e1db774581b4900e0f832abe3c7c84996726bbc161b19c8f29e76", size = 2225708, upload-time = "2026-05-06T13:40:24.887Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b0/9ec8c38f33b26db0b612cb7fd165bb0a370773710432a2a74fa31287b430/pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f13a646d65d09fbf1bc6b3a9635d30095c8e7e5cc419ff35ecc563c5fd04cd49", size = 2288494, upload-time = "2026-05-06T13:38:00.091Z" }, + { url = "https://files.pythonhosted.org/packages/65/05/497446a9586d1b2d24ee25ebe208beb15388f1875d783e1e014055d150ac/pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432c179df7874eeb73307aad2df0755e1ae0efa61ff0ea89b93e194411ae3928", size = 2095629, upload-time = "2026-05-06T13:38:23.632Z" }, + { url = "https://files.pythonhosted.org/packages/93/d9/cd5fa98f9d94f9294c15459396c8a2383c164469e679ac178d6d42cfee6b/pydantic_core-2.46.4-cp39-cp39-manylinux_2_31_riscv64.whl", hash = "sha256:e68b7a074f65a2fd746c52a7ce6142ab7006074ac269ace0c25cd8ba171f8066", size = 2119309, upload-time = "2026-05-06T13:39:50.144Z" }, + { url = "https://files.pythonhosted.org/packages/20/1b/64cec655451ddbf3976df5dc9706b240df4fdaebdeebeadd4f59a8dab926/pydantic_core-2.46.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4a05d69cba51d852c5c3e92758653245a50c0b646ced0cf05bd793ed592839d6", size = 2170216, upload-time = "2026-05-06T13:39:14.561Z" }, + { url = "https://files.pythonhosted.org/packages/2a/21/fe9f039138c9ea3be10ccdb6ec490acb54dcbef5a5e96dbdf1411f82b929/pydantic_core-2.46.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:228ee9bae8bef5b1e97ec58302f80357c37199e0d0a99174e138d28e6957b9d9", size = 2186726, upload-time = "2026-05-06T13:37:51.597Z" }, + { url = "https://files.pythonhosted.org/packages/44/cb/19ca0da64821d1aefcef65f253aa9ecbdd0dde360f607d0f9b3d95db2b4e/pydantic_core-2.46.4-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:10e17cbb10a330363733efc4d7c4d0dd827ac0909b8f6a6542298fed1ea62f29", size = 2320400, upload-time = "2026-05-06T13:39:36.29Z" }, + { url = "https://files.pythonhosted.org/packages/cd/14/fe3fbf6e845bf2080dc2f282d75085ddf79d037b35634ecde68f33c217b4/pydantic_core-2.46.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:91a06d2e259ecfbd8c901d70c3c507900458498142b3026a296b7de4d1322cc9", size = 2363318, upload-time = "2026-05-06T13:38:53.039Z" }, + { url = "https://files.pythonhosted.org/packages/62/88/60b110889507a426eecf626f7536566cb290ada71147eff49b6e2724ca62/pydantic_core-2.46.4-cp39-cp39-win32.whl", hash = "sha256:d80ee3d731373b24cebbc10d689ca4ee1875caf0d5703a245db18efd4dd37fc1", size = 1988880, upload-time = "2026-05-06T13:39:16.572Z" }, + { url = "https://files.pythonhosted.org/packages/0b/d6/8ede2f98f17e1e4e127d37be0eced4eee931a511c62cd68af50e1b25bfa9/pydantic_core-2.46.4-cp39-cp39-win_amd64.whl", hash = "sha256:3be77f45df024d789a672ae34f8b06fb346c4f9f46ea714956660ea4862e89ac", size = 2079257, upload-time = "2026-05-06T13:39:38.498Z" }, + { url = "https://files.pythonhosted.org/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c", size = 2111589, upload-time = "2026-05-06T13:37:10.817Z" }, + { url = "https://files.pythonhosted.org/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size = 1944552, upload-time = "2026-05-06T13:36:56.717Z" }, + { url = "https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984, upload-time = "2026-05-06T13:39:06.207Z" }, + { url = "https://files.pythonhosted.org/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea", size = 2138417, upload-time = "2026-05-06T13:39:45.476Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, + { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, + { url = "https://files.pythonhosted.org/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0", size = 2112782, upload-time = "2026-05-06T13:37:04.016Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7", size = 1952146, upload-time = "2026-05-06T13:39:43.092Z" }, + { url = "https://files.pythonhosted.org/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size = 2134492, upload-time = "2026-05-06T13:36:58.124Z" }, + { url = "https://files.pythonhosted.org/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9", size = 2156604, upload-time = "2026-05-06T13:37:49.88Z" }, + { url = "https://files.pythonhosted.org/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size = 2183828, upload-time = "2026-05-06T13:37:43.053Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size = 2310000, upload-time = "2026-05-06T13:37:56.694Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size = 2361286, upload-time = "2026-05-06T13:40:05.667Z" }, + { url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" }, ] [[package]] -name = "pyproject-hooks" -version = "1.2.0" +name = "pyflakes" +version = "3.3.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/82/28175b2414effca1cdac8dc99f76d660e7a4fb0ceefa4b4ab8f5f6742925/pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8", size = 19228 } +sdist = { url = "https://files.pythonhosted.org/packages/af/cc/1df338bd7ed1fa7c317081dcf29bf2f01266603b301e6858856d346a12b3/pyflakes-3.3.2.tar.gz", hash = "sha256:6dfd61d87b97fba5dcfaaf781171ac16be16453be6d816147989e7f6e6a9576b", size = 64175, upload-time = "2025-03-31T13:21:20.34Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913", size = 10216 }, + { url = "https://files.pythonhosted.org/packages/15/40/b293a4fa769f3b02ab9e387c707c4cbdc34f073f945de0386107d4e669e6/pyflakes-3.3.2-py2.py3-none-any.whl", hash = "sha256:5039c8339cbb1944045f4ee5466908906180f13cc99cc9949348d10f82a5c32a", size = 63164, upload-time = "2025-03-31T13:21:18.503Z" }, ] [[package]] -name = "pyside6" -version = "6.7.3" +name = "pygments" +version = "2.19.1" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyside6-addons" }, - { name = "pyside6-essentials" }, - { name = "shiboken6" }, -] +sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581, upload-time = "2025-01-06T17:26:30.443Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d4/34/86b0fd9b5ce8eee35bd1311a620e8cdfd56d0ca36edca314a6189336682d/PySide6-6.7.3-cp39-abi3-macosx_11_0_universal2.whl", hash = "sha256:1c21c4cf6cdd29bd13bbd7a2514756a19188eab992b92af03e64bf06a9b33d5b", size = 532108 }, - { url = "https://files.pythonhosted.org/packages/36/8a/af0c2c91bcd8a0003e4233a0c74dd6b0e5af5534e96185d48a2c03a8f359/PySide6-6.7.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a21480cc746358f70768975fcc452322f03b3c3622625bfb1743b40ce4e24beb", size = 532727 }, - { url = "https://files.pythonhosted.org/packages/8a/56/a5347e273de35bd63fd3204bd9d80a134db959ff6f2bbeb299867dbe83db/PySide6-6.7.3-cp39-abi3-manylinux_2_31_aarch64.whl", hash = "sha256:c2a1313296c0088d1c3d231d0a8ccf0eda52b84139d0c4065fded76e4a4378f4", size = 532630 }, - { url = "https://files.pythonhosted.org/packages/69/f0/18c6b7f5087eec0d673eb8703c3e9eb76d62cfc427b015d4fc833287c1c4/PySide6-6.7.3-cp39-abi3-win_amd64.whl", hash = "sha256:3ac8dcb4ca82d276e319f89dd99098b01061f255a2b9104216504aece5e0faf8", size = 539981 }, + { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293, upload-time = "2025-01-06T17:26:25.553Z" }, ] [[package]] -name = "pyside6-addons" -version = "6.7.3" +name = "pyparsing" +version = "3.2.3" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyside6-essentials" }, - { name = "shiboken6" }, -] +sdist = { url = "https://files.pythonhosted.org/packages/bb/22/f1129e69d94ffff626bdb5c835506b3a5b4f3d070f17ea295e12c2c6f60f/pyparsing-3.2.3.tar.gz", hash = "sha256:b9c13f1ab8b3b542f72e28f634bad4de758ab3ce4546e4301970ad6fa77c38be", size = 1088608, upload-time = "2025-03-25T05:01:28.114Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/03/35/e4b69e36d3ffdb6818e546188a99ab2c2fe9834f7d06d57a47d9bfce8fd3/PySide6_Addons-6.7.3-cp39-abi3-macosx_11_0_universal2.whl", hash = "sha256:3174cb3a373c09c98740b452e8e8f4945d64cfa18ed8d43964111d570f0dc647", size = 292945102 }, - { url = "https://files.pythonhosted.org/packages/bb/57/1b16719360c23ddf4aafffbb281fc5cd8949db42c868d0df1ac466814508/PySide6_Addons-6.7.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:bde1eb03dbffd089b50cd445847aaecaf4056cea84c49ea592d00f84f247251e", size = 138279265 }, - { url = "https://files.pythonhosted.org/packages/77/4d/547d8d5d7471a725b7221925266f5e8075bfd495f4d39156efdad4f59fa4/PySide6_Addons-6.7.3-cp39-abi3-manylinux_2_31_aarch64.whl", hash = "sha256:5a9e0df31345fe6caea677d916ea48b53ba86f95cc6499c57f89e392447ad6db", size = 123148721 }, - { url = "https://files.pythonhosted.org/packages/3e/ca/1b34d0785298f86bed27582103760f67f7f78d9e2006405e256e9c770993/PySide6_Addons-6.7.3-cp39-abi3-win_amd64.whl", hash = "sha256:d8a19c2b2446407724c81c33ebf3217eaabd092f0f72da8130c17079e04a7813", size = 123578170 }, + { url = "https://files.pythonhosted.org/packages/05/e7/df2285f3d08fee213f2d041540fa4fc9ca6c2d44cf36d3a035bf2a8d2bcc/pyparsing-3.2.3-py3-none-any.whl", hash = "sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf", size = 111120, upload-time = "2025-03-25T05:01:24.908Z" }, ] [[package]] -name = "pyside6-essentials" -version = "6.7.3" +name = "pyproject-hooks" +version = "1.2.0" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "shiboken6" }, -] +sdist = { url = "https://files.pythonhosted.org/packages/e7/82/28175b2414effca1cdac8dc99f76d660e7a4fb0ceefa4b4ab8f5f6742925/pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8", size = 19228, upload-time = "2024-09-29T09:24:13.293Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/d8/053a3a95454ebc5313b3a4a9167b2f959fc6eaee3536b6e4269b75ce9716/PySide6_Essentials-6.7.3-cp39-abi3-macosx_11_0_universal2.whl", hash = "sha256:f9e08a4e9e7dc7b5ab72fde20abce8c97df7af1b802d9743f098f577dfe1f649", size = 158595022 }, - { url = "https://files.pythonhosted.org/packages/8d/d8/a08ca6eca6839be5d604e4035ce899f9d6df0f612191f4ed4d03e51f1e0e/PySide6_Essentials-6.7.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cda6fd26aead48f32e57f044d18aa75dc39265b49d7957f515ce7ac3989e7029", size = 90396416 }, - { url = "https://files.pythonhosted.org/packages/5c/4d/00a5716e56c35c5621368c89abb360f6e953e2be639cd5f11e8075894e41/PySide6_Essentials-6.7.3-cp39-abi3-manylinux_2_31_aarch64.whl", hash = "sha256:acdde06b74f26e7d26b4ae1461081b32a6cb17fcaa2a580050b5e0f0f12236c9", size = 87264397 }, - { url = "https://files.pythonhosted.org/packages/0b/18/8679adff0b7a6ccacc2e0febd15c1b78d02abc0180b56007c24e94c9d582/PySide6_Essentials-6.7.3-cp39-abi3-win_amd64.whl", hash = "sha256:f0950fcdcbcd4f2443336dc6a5fe692172adc225f876839583503ded0ab2f2a7", size = 68887979 }, + { url = "https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913", size = 10216, upload-time = "2024-09-29T09:24:11.978Z" }, ] [[package]] @@ -1413,9 +1500,9 @@ dependencies = [ { name = "pluggy" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891 } +sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634 }, + { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" }, ] [[package]] @@ -1425,53 +1512,53 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "six" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, ] [[package]] name = "pyyaml" version = "6.0.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199 }, - { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758 }, - { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463 }, - { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280 }, - { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239 }, - { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802 }, - { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527 }, - { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052 }, - { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774 }, - { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612 }, - { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040 }, - { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829 }, - { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167 }, - { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952 }, - { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301 }, - { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638 }, - { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850 }, - { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980 }, - { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 }, - { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 }, - { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 }, - { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 }, - { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 }, - { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 }, - { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 }, - { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 }, - { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 }, - { url = "https://files.pythonhosted.org/packages/65/d8/b7a1db13636d7fb7d4ff431593c510c8b8fca920ade06ca8ef20015493c5/PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", size = 184777 }, - { url = "https://files.pythonhosted.org/packages/0a/02/6ec546cd45143fdf9840b2c6be8d875116a64076218b61d68e12548e5839/PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", size = 172318 }, - { url = "https://files.pythonhosted.org/packages/0e/9a/8cc68be846c972bda34f6c2a93abb644fb2476f4dcc924d52175786932c9/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", size = 720891 }, - { url = "https://files.pythonhosted.org/packages/e9/6c/6e1b7f40181bc4805e2e07f4abc10a88ce4648e7e95ff1abe4ae4014a9b2/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", size = 722614 }, - { url = "https://files.pythonhosted.org/packages/3d/32/e7bd8535d22ea2874cef6a81021ba019474ace0d13a4819c2a4bce79bd6a/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", size = 737360 }, - { url = "https://files.pythonhosted.org/packages/d7/12/7322c1e30b9be969670b672573d45479edef72c9a0deac3bb2868f5d7469/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", size = 699006 }, - { url = "https://files.pythonhosted.org/packages/82/72/04fcad41ca56491995076630c3ec1e834be241664c0c09a64c9a2589b507/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", size = 723577 }, - { url = "https://files.pythonhosted.org/packages/ed/5e/46168b1f2757f1fcd442bc3029cd8767d88a98c9c05770d8b420948743bb/PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", size = 144593 }, - { url = "https://files.pythonhosted.org/packages/19/87/5124b1c1f2412bb95c59ec481eaf936cd32f0fe2a7b16b97b81c4c017a6a/PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", size = 162312 }, +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199, upload-time = "2024-08-06T20:31:40.178Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758, upload-time = "2024-08-06T20:31:42.173Z" }, + { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463, upload-time = "2024-08-06T20:31:44.263Z" }, + { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280, upload-time = "2024-08-06T20:31:50.199Z" }, + { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239, upload-time = "2024-08-06T20:31:52.292Z" }, + { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802, upload-time = "2024-08-06T20:31:53.836Z" }, + { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527, upload-time = "2024-08-06T20:31:55.565Z" }, + { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052, upload-time = "2024-08-06T20:31:56.914Z" }, + { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774, upload-time = "2024-08-06T20:31:58.304Z" }, + { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612, upload-time = "2024-08-06T20:32:03.408Z" }, + { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040, upload-time = "2024-08-06T20:32:04.926Z" }, + { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829, upload-time = "2024-08-06T20:32:06.459Z" }, + { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167, upload-time = "2024-08-06T20:32:08.338Z" }, + { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952, upload-time = "2024-08-06T20:32:14.124Z" }, + { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301, upload-time = "2024-08-06T20:32:16.17Z" }, + { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638, upload-time = "2024-08-06T20:32:18.555Z" }, + { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850, upload-time = "2024-08-06T20:32:19.889Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980, upload-time = "2024-08-06T20:32:21.273Z" }, + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" }, + { url = "https://files.pythonhosted.org/packages/65/d8/b7a1db13636d7fb7d4ff431593c510c8b8fca920ade06ca8ef20015493c5/PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", size = 184777, upload-time = "2024-08-06T20:33:25.896Z" }, + { url = "https://files.pythonhosted.org/packages/0a/02/6ec546cd45143fdf9840b2c6be8d875116a64076218b61d68e12548e5839/PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", size = 172318, upload-time = "2024-08-06T20:33:27.212Z" }, + { url = "https://files.pythonhosted.org/packages/0e/9a/8cc68be846c972bda34f6c2a93abb644fb2476f4dcc924d52175786932c9/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", size = 720891, upload-time = "2024-08-06T20:33:28.974Z" }, + { url = "https://files.pythonhosted.org/packages/e9/6c/6e1b7f40181bc4805e2e07f4abc10a88ce4648e7e95ff1abe4ae4014a9b2/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", size = 722614, upload-time = "2024-08-06T20:33:34.157Z" }, + { url = "https://files.pythonhosted.org/packages/3d/32/e7bd8535d22ea2874cef6a81021ba019474ace0d13a4819c2a4bce79bd6a/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", size = 737360, upload-time = "2024-08-06T20:33:35.84Z" }, + { url = "https://files.pythonhosted.org/packages/d7/12/7322c1e30b9be969670b672573d45479edef72c9a0deac3bb2868f5d7469/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", size = 699006, upload-time = "2024-08-06T20:33:37.501Z" }, + { url = "https://files.pythonhosted.org/packages/82/72/04fcad41ca56491995076630c3ec1e834be241664c0c09a64c9a2589b507/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", size = 723577, upload-time = "2024-08-06T20:33:39.389Z" }, + { url = "https://files.pythonhosted.org/packages/ed/5e/46168b1f2757f1fcd442bc3029cd8767d88a98c9c05770d8b420948743bb/PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", size = 144593, upload-time = "2024-08-06T20:33:46.63Z" }, + { url = "https://files.pythonhosted.org/packages/19/87/5124b1c1f2412bb95c59ec481eaf936cd32f0fe2a7b16b97b81c4c017a6a/PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", size = 162312, upload-time = "2024-08-06T20:33:49.073Z" }, ] [[package]] @@ -1484,9 +1571,9 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } +sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218, upload-time = "2024-05-29T15:37:49.536Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, + { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928, upload-time = "2024-05-29T15:37:47.027Z" }, ] [[package]] @@ -1498,9 +1585,9 @@ dependencies = [ { name = "pygments" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078 } +sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078, upload-time = "2025-03-30T14:15:14.23Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229 }, + { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229, upload-time = "2025-03-30T14:15:12.283Z" }, ] [[package]] @@ -1513,28 +1600,28 @@ dependencies = [ { name = "scipy" }, { name = "threadpoolctl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9e/a5/4ae3b3a0755f7b35a280ac90b28817d1f380318973cff14075ab41ef50d9/scikit_learn-1.6.1.tar.gz", hash = "sha256:b4fc2525eca2c69a59260f583c56a7557c6ccdf8deafdba6e060f94c1c59738e", size = 7068312 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2e/3a/f4597eb41049110b21ebcbb0bcb43e4035017545daa5eedcfeb45c08b9c5/scikit_learn-1.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d056391530ccd1e501056160e3c9673b4da4805eb67eb2bdf4e983e1f9c9204e", size = 12067702 }, - { url = "https://files.pythonhosted.org/packages/37/19/0423e5e1fd1c6ec5be2352ba05a537a473c1677f8188b9306097d684b327/scikit_learn-1.6.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:0c8d036eb937dbb568c6242fa598d551d88fb4399c0344d95c001980ec1c7d36", size = 11112765 }, - { url = "https://files.pythonhosted.org/packages/70/95/d5cb2297a835b0f5fc9a77042b0a2d029866379091ab8b3f52cc62277808/scikit_learn-1.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8634c4bd21a2a813e0a7e3900464e6d593162a29dd35d25bdf0103b3fce60ed5", size = 12643991 }, - { url = "https://files.pythonhosted.org/packages/b7/91/ab3c697188f224d658969f678be86b0968ccc52774c8ab4a86a07be13c25/scikit_learn-1.6.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:775da975a471c4f6f467725dff0ced5c7ac7bda5e9316b260225b48475279a1b", size = 13497182 }, - { url = "https://files.pythonhosted.org/packages/17/04/d5d556b6c88886c092cc989433b2bab62488e0f0dafe616a1d5c9cb0efb1/scikit_learn-1.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:8a600c31592bd7dab31e1c61b9bbd6dea1b3433e67d264d17ce1017dbdce8002", size = 11125517 }, - { url = "https://files.pythonhosted.org/packages/6c/2a/e291c29670795406a824567d1dfc91db7b699799a002fdaa452bceea8f6e/scikit_learn-1.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:72abc587c75234935e97d09aa4913a82f7b03ee0b74111dcc2881cba3c5a7b33", size = 12102620 }, - { url = "https://files.pythonhosted.org/packages/25/92/ee1d7a00bb6b8c55755d4984fd82608603a3cc59959245068ce32e7fb808/scikit_learn-1.6.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:b3b00cdc8f1317b5f33191df1386c0befd16625f49d979fe77a8d44cae82410d", size = 11116234 }, - { url = "https://files.pythonhosted.org/packages/30/cd/ed4399485ef364bb25f388ab438e3724e60dc218c547a407b6e90ccccaef/scikit_learn-1.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc4765af3386811c3ca21638f63b9cf5ecf66261cc4815c1db3f1e7dc7b79db2", size = 12592155 }, - { url = "https://files.pythonhosted.org/packages/a8/f3/62fc9a5a659bb58a03cdd7e258956a5824bdc9b4bb3c5d932f55880be569/scikit_learn-1.6.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25fc636bdaf1cc2f4a124a116312d837148b5e10872147bdaf4887926b8c03d8", size = 13497069 }, - { url = "https://files.pythonhosted.org/packages/a1/a6/c5b78606743a1f28eae8f11973de6613a5ee87366796583fb74c67d54939/scikit_learn-1.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:fa909b1a36e000a03c382aade0bd2063fd5680ff8b8e501660c0f59f021a6415", size = 11139809 }, - { url = "https://files.pythonhosted.org/packages/0a/18/c797c9b8c10380d05616db3bfb48e2a3358c767affd0857d56c2eb501caa/scikit_learn-1.6.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:926f207c804104677af4857b2c609940b743d04c4c35ce0ddc8ff4f053cddc1b", size = 12104516 }, - { url = "https://files.pythonhosted.org/packages/c4/b7/2e35f8e289ab70108f8cbb2e7a2208f0575dc704749721286519dcf35f6f/scikit_learn-1.6.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:2c2cae262064e6a9b77eee1c8e768fc46aa0b8338c6a8297b9b6759720ec0ff2", size = 11167837 }, - { url = "https://files.pythonhosted.org/packages/a4/f6/ff7beaeb644bcad72bcfd5a03ff36d32ee4e53a8b29a639f11bcb65d06cd/scikit_learn-1.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1061b7c028a8663fb9a1a1baf9317b64a257fcb036dae5c8752b2abef31d136f", size = 12253728 }, - { url = "https://files.pythonhosted.org/packages/29/7a/8bce8968883e9465de20be15542f4c7e221952441727c4dad24d534c6d99/scikit_learn-1.6.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e69fab4ebfc9c9b580a7a80111b43d214ab06250f8a7ef590a4edf72464dd86", size = 13147700 }, - { url = "https://files.pythonhosted.org/packages/62/27/585859e72e117fe861c2079bcba35591a84f801e21bc1ab85bce6ce60305/scikit_learn-1.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:70b1d7e85b1c96383f872a519b3375f92f14731e279a7b4c6cfd650cf5dffc52", size = 11110613 }, - { url = "https://files.pythonhosted.org/packages/d2/37/b305b759cc65829fe1b8853ff3e308b12cdd9d8884aa27840835560f2b42/scikit_learn-1.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6849dd3234e87f55dce1db34c89a810b489ead832aaf4d4550b7ea85628be6c1", size = 12101868 }, - { url = "https://files.pythonhosted.org/packages/83/74/f64379a4ed5879d9db744fe37cfe1978c07c66684d2439c3060d19a536d8/scikit_learn-1.6.1-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:e7be3fa5d2eb9be7d77c3734ff1d599151bb523674be9b834e8da6abe132f44e", size = 11144062 }, - { url = "https://files.pythonhosted.org/packages/fd/dc/d5457e03dc9c971ce2b0d750e33148dd060fefb8b7dc71acd6054e4bb51b/scikit_learn-1.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44a17798172df1d3c1065e8fcf9019183f06c87609b49a124ebdf57ae6cb0107", size = 12693173 }, - { url = "https://files.pythonhosted.org/packages/79/35/b1d2188967c3204c78fa79c9263668cf1b98060e8e58d1a730fe5b2317bb/scikit_learn-1.6.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8b7a3b86e411e4bce21186e1c180d792f3d99223dcfa3b4f597ecc92fa1a422", size = 13518605 }, - { url = "https://files.pythonhosted.org/packages/fb/d8/8d603bdd26601f4b07e2363032b8565ab82eb857f93d86d0f7956fcf4523/scikit_learn-1.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:7a73d457070e3318e32bdb3aa79a8d990474f19035464dfd8bede2883ab5dc3b", size = 11155078 }, +sdist = { url = "https://files.pythonhosted.org/packages/9e/a5/4ae3b3a0755f7b35a280ac90b28817d1f380318973cff14075ab41ef50d9/scikit_learn-1.6.1.tar.gz", hash = "sha256:b4fc2525eca2c69a59260f583c56a7557c6ccdf8deafdba6e060f94c1c59738e", size = 7068312, upload-time = "2025-01-10T08:07:55.348Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/3a/f4597eb41049110b21ebcbb0bcb43e4035017545daa5eedcfeb45c08b9c5/scikit_learn-1.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d056391530ccd1e501056160e3c9673b4da4805eb67eb2bdf4e983e1f9c9204e", size = 12067702, upload-time = "2025-01-10T08:05:56.515Z" }, + { url = "https://files.pythonhosted.org/packages/37/19/0423e5e1fd1c6ec5be2352ba05a537a473c1677f8188b9306097d684b327/scikit_learn-1.6.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:0c8d036eb937dbb568c6242fa598d551d88fb4399c0344d95c001980ec1c7d36", size = 11112765, upload-time = "2025-01-10T08:06:00.272Z" }, + { url = "https://files.pythonhosted.org/packages/70/95/d5cb2297a835b0f5fc9a77042b0a2d029866379091ab8b3f52cc62277808/scikit_learn-1.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8634c4bd21a2a813e0a7e3900464e6d593162a29dd35d25bdf0103b3fce60ed5", size = 12643991, upload-time = "2025-01-10T08:06:04.813Z" }, + { url = "https://files.pythonhosted.org/packages/b7/91/ab3c697188f224d658969f678be86b0968ccc52774c8ab4a86a07be13c25/scikit_learn-1.6.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:775da975a471c4f6f467725dff0ced5c7ac7bda5e9316b260225b48475279a1b", size = 13497182, upload-time = "2025-01-10T08:06:08.42Z" }, + { url = "https://files.pythonhosted.org/packages/17/04/d5d556b6c88886c092cc989433b2bab62488e0f0dafe616a1d5c9cb0efb1/scikit_learn-1.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:8a600c31592bd7dab31e1c61b9bbd6dea1b3433e67d264d17ce1017dbdce8002", size = 11125517, upload-time = "2025-01-10T08:06:12.783Z" }, + { url = "https://files.pythonhosted.org/packages/6c/2a/e291c29670795406a824567d1dfc91db7b699799a002fdaa452bceea8f6e/scikit_learn-1.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:72abc587c75234935e97d09aa4913a82f7b03ee0b74111dcc2881cba3c5a7b33", size = 12102620, upload-time = "2025-01-10T08:06:16.675Z" }, + { url = "https://files.pythonhosted.org/packages/25/92/ee1d7a00bb6b8c55755d4984fd82608603a3cc59959245068ce32e7fb808/scikit_learn-1.6.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:b3b00cdc8f1317b5f33191df1386c0befd16625f49d979fe77a8d44cae82410d", size = 11116234, upload-time = "2025-01-10T08:06:21.83Z" }, + { url = "https://files.pythonhosted.org/packages/30/cd/ed4399485ef364bb25f388ab438e3724e60dc218c547a407b6e90ccccaef/scikit_learn-1.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc4765af3386811c3ca21638f63b9cf5ecf66261cc4815c1db3f1e7dc7b79db2", size = 12592155, upload-time = "2025-01-10T08:06:27.309Z" }, + { url = "https://files.pythonhosted.org/packages/a8/f3/62fc9a5a659bb58a03cdd7e258956a5824bdc9b4bb3c5d932f55880be569/scikit_learn-1.6.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25fc636bdaf1cc2f4a124a116312d837148b5e10872147bdaf4887926b8c03d8", size = 13497069, upload-time = "2025-01-10T08:06:32.515Z" }, + { url = "https://files.pythonhosted.org/packages/a1/a6/c5b78606743a1f28eae8f11973de6613a5ee87366796583fb74c67d54939/scikit_learn-1.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:fa909b1a36e000a03c382aade0bd2063fd5680ff8b8e501660c0f59f021a6415", size = 11139809, upload-time = "2025-01-10T08:06:35.514Z" }, + { url = "https://files.pythonhosted.org/packages/0a/18/c797c9b8c10380d05616db3bfb48e2a3358c767affd0857d56c2eb501caa/scikit_learn-1.6.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:926f207c804104677af4857b2c609940b743d04c4c35ce0ddc8ff4f053cddc1b", size = 12104516, upload-time = "2025-01-10T08:06:40.009Z" }, + { url = "https://files.pythonhosted.org/packages/c4/b7/2e35f8e289ab70108f8cbb2e7a2208f0575dc704749721286519dcf35f6f/scikit_learn-1.6.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:2c2cae262064e6a9b77eee1c8e768fc46aa0b8338c6a8297b9b6759720ec0ff2", size = 11167837, upload-time = "2025-01-10T08:06:43.305Z" }, + { url = "https://files.pythonhosted.org/packages/a4/f6/ff7beaeb644bcad72bcfd5a03ff36d32ee4e53a8b29a639f11bcb65d06cd/scikit_learn-1.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1061b7c028a8663fb9a1a1baf9317b64a257fcb036dae5c8752b2abef31d136f", size = 12253728, upload-time = "2025-01-10T08:06:47.618Z" }, + { url = "https://files.pythonhosted.org/packages/29/7a/8bce8968883e9465de20be15542f4c7e221952441727c4dad24d534c6d99/scikit_learn-1.6.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e69fab4ebfc9c9b580a7a80111b43d214ab06250f8a7ef590a4edf72464dd86", size = 13147700, upload-time = "2025-01-10T08:06:50.888Z" }, + { url = "https://files.pythonhosted.org/packages/62/27/585859e72e117fe861c2079bcba35591a84f801e21bc1ab85bce6ce60305/scikit_learn-1.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:70b1d7e85b1c96383f872a519b3375f92f14731e279a7b4c6cfd650cf5dffc52", size = 11110613, upload-time = "2025-01-10T08:06:54.115Z" }, + { url = "https://files.pythonhosted.org/packages/d2/37/b305b759cc65829fe1b8853ff3e308b12cdd9d8884aa27840835560f2b42/scikit_learn-1.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6849dd3234e87f55dce1db34c89a810b489ead832aaf4d4550b7ea85628be6c1", size = 12101868, upload-time = "2025-01-10T08:07:34.189Z" }, + { url = "https://files.pythonhosted.org/packages/83/74/f64379a4ed5879d9db744fe37cfe1978c07c66684d2439c3060d19a536d8/scikit_learn-1.6.1-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:e7be3fa5d2eb9be7d77c3734ff1d599151bb523674be9b834e8da6abe132f44e", size = 11144062, upload-time = "2025-01-10T08:07:37.67Z" }, + { url = "https://files.pythonhosted.org/packages/fd/dc/d5457e03dc9c971ce2b0d750e33148dd060fefb8b7dc71acd6054e4bb51b/scikit_learn-1.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44a17798172df1d3c1065e8fcf9019183f06c87609b49a124ebdf57ae6cb0107", size = 12693173, upload-time = "2025-01-10T08:07:42.713Z" }, + { url = "https://files.pythonhosted.org/packages/79/35/b1d2188967c3204c78fa79c9263668cf1b98060e8e58d1a730fe5b2317bb/scikit_learn-1.6.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8b7a3b86e411e4bce21186e1c180d792f3d99223dcfa3b4f597ecc92fa1a422", size = 13518605, upload-time = "2025-01-10T08:07:46.551Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d8/8d603bdd26601f4b07e2363032b8565ab82eb857f93d86d0f7956fcf4523/scikit_learn-1.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:7a73d457070e3318e32bdb3aa79a8d990474f19035464dfd8bede2883ab5dc3b", size = 11155078, upload-time = "2025-01-10T08:07:51.376Z" }, ] [[package]] @@ -1544,61 +1631,50 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6e/1f/91144ba78dccea567a6466262922786ffc97be1e9b06ed9574ef0edc11e1/scipy-1.11.4.tar.gz", hash = "sha256:90a2b78e7f5733b9de748f589f09225013685f9b218275257f8a8168ededaeaa", size = 56336202 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/34/c6/a32add319475d21f89733c034b99c81b3a7c6c7c19f96f80c7ca3ff1bbd4/scipy-1.11.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc9a714581f561af0848e6b69947fda0614915f072dfd14142ed1bfe1b806710", size = 37293259 }, - { url = "https://files.pythonhosted.org/packages/de/0d/4fa68303568c70fd56fbf40668b6c6807cfee4cad975f07d80bdd26d013e/scipy-1.11.4-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:cf00bd2b1b0211888d4dc75656c0412213a8b25e80d73898083f402b50f47e41", size = 29760656 }, - { url = "https://files.pythonhosted.org/packages/13/e5/8012be7857db6cbbbdbeea8a154dbacdfae845e95e1e19c028e82236d4a0/scipy-1.11.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9999c008ccf00e8fbcce1236f85ade5c569d13144f77a1946bef8863e8f6eb4", size = 32922489 }, - { url = "https://files.pythonhosted.org/packages/e0/9e/80e2205d138960a49caea391f3710600895dd8292b6868dc9aff7aa593f9/scipy-1.11.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:933baf588daa8dc9a92c20a0be32f56d43faf3d1a60ab11b3f08c356430f6e56", size = 36442040 }, - { url = "https://files.pythonhosted.org/packages/69/60/30a9c3fbe5066a3a93eefe3e2d44553df13587e6f792e1bff20dfed3d17e/scipy-1.11.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8fce70f39076a5aa62e92e69a7f62349f9574d8405c0a5de6ed3ef72de07f446", size = 36643257 }, - { url = "https://files.pythonhosted.org/packages/f8/ec/b46756f80e3f4c5f0989f6e4492c2851f156d9c239d554754a3c8cffd4e2/scipy-1.11.4-cp310-cp310-win_amd64.whl", hash = "sha256:6550466fbeec7453d7465e74d4f4b19f905642c89a7525571ee91dd7adabb5a3", size = 44149285 }, - { url = "https://files.pythonhosted.org/packages/b8/f2/1aefbd5e54ebd8c6163ccf7f73e5d17bc8cb38738d312befc524fce84bb4/scipy-1.11.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f313b39a7e94f296025e3cffc2c567618174c0b1dde173960cf23808f9fae4be", size = 37159197 }, - { url = "https://files.pythonhosted.org/packages/4b/48/20e77ddb1f473d4717a7d4d3fc8d15557f406f7708496054c59f635b7734/scipy-1.11.4-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:1b7c3dca977f30a739e0409fb001056484661cb2541a01aba0bb0029f7b68db8", size = 29675057 }, - { url = "https://files.pythonhosted.org/packages/75/2e/a781862190d0e7e76afa74752ef363488a9a9d6ea86e46d5e5506cee8df6/scipy-1.11.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00150c5eae7b610c32589dda259eacc7c4f1665aedf25d921907f4d08a951b1c", size = 32882747 }, - { url = "https://files.pythonhosted.org/packages/6b/d4/d62ce38ba00dc67d7ec4ec5cc19d36958d8ed70e63778715ad626bcbc796/scipy-1.11.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:530f9ad26440e85766509dbf78edcfe13ffd0ab7fec2560ee5c36ff74d6269ff", size = 36402732 }, - { url = "https://files.pythonhosted.org/packages/88/86/827b56aea1ed04adbb044a675672a73c84d81076a350092bbfcfc1ae723b/scipy-1.11.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5e347b14fe01003d3b78e196e84bd3f48ffe4c8a7b8a1afbcb8f5505cb710993", size = 36622138 }, - { url = "https://files.pythonhosted.org/packages/43/d0/f3cd75b62e1b90f48dbf091261b2fc7ceec14a700e308c50f6a69c83d337/scipy-1.11.4-cp311-cp311-win_amd64.whl", hash = "sha256:acf8ed278cc03f5aff035e69cb511741e0418681d25fbbb86ca65429c4f4d9cd", size = 44095631 }, - { url = "https://files.pythonhosted.org/packages/df/64/8a690570485b636da614acff35fd725fcbc487f8b1fa9bdb12871b77412f/scipy-1.11.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:028eccd22e654b3ea01ee63705681ee79933652b2d8f873e7949898dda6d11b6", size = 37053653 }, - { url = "https://files.pythonhosted.org/packages/5e/43/abf331745a7e5f4af51f13d40e2a72f516048db41ecbcf3ac6f86ada54a3/scipy-1.11.4-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:2c6ff6ef9cc27f9b3db93a6f8b38f97387e6e0591600369a297a50a8e96e835d", size = 29641601 }, - { url = "https://files.pythonhosted.org/packages/47/9b/62d0ec086dd2871009da8769c504bec6e39b80f4c182c6ead0fcebd8b323/scipy-1.11.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b030c6674b9230d37c5c60ab456e2cf12f6784596d15ce8da9365e70896effc4", size = 32272137 }, - { url = "https://files.pythonhosted.org/packages/08/77/f90f7306d755ac68bd159c50bb86fffe38400e533e8c609dd8484bd0f172/scipy-1.11.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad669df80528aeca5f557712102538f4f37e503f0c5b9541655016dd0932ca79", size = 35777534 }, - { url = "https://files.pythonhosted.org/packages/00/de/b9f6938090c37b5092969ba1c67118e9114e8e6ef9d197251671444e839c/scipy-1.11.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ce7fff2e23ab2cc81ff452a9444c215c28e6305f396b2ba88343a567feec9660", size = 35963721 }, - { url = "https://files.pythonhosted.org/packages/c6/a1/357e4cd43af2748e1e0407ae0e9a5ea8aaaa6b702833c81be11670dcbad8/scipy-1.11.4-cp312-cp312-win_amd64.whl", hash = "sha256:36750b7733d960d7994888f0d148d31ea3017ac15eef664194b4ef68d36a4a97", size = 43730653 }, - { url = "https://files.pythonhosted.org/packages/c5/e0/9872b7923c0ff7a420af8f559d0f5c6831143477b4ce57afe1b2a7c59a63/scipy-1.11.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6e619aba2df228a9b34718efb023966da781e89dd3d21637b27f2e54db0410d7", size = 37317855 }, - { url = "https://files.pythonhosted.org/packages/d1/3a/0ab839bb67043ab35e5dcf8b611ca9e08e5a8933b0bc7506eedcec664aae/scipy-1.11.4-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:f3cd9e7b3c2c1ec26364856f9fbe78695fe631150f94cd1c22228456404cf1ec", size = 29741102 }, - { url = "https://files.pythonhosted.org/packages/a0/c3/1e498aa3d35ccfdf26c0fe81ebc52c540c454377e2690fc3738aabacaf8d/scipy-1.11.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d10e45a6c50211fe256da61a11c34927c68f277e03138777bdebedd933712fea", size = 33035888 }, - { url = "https://files.pythonhosted.org/packages/db/86/bf3f01f003224c00dd94d9443d676023ed65d63ea2e34356888dc7fa8f48/scipy-1.11.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:91af76a68eeae0064887a48e25c4e616fa519fa0d38602eda7e0f97d65d57937", size = 36621737 }, - { url = "https://files.pythonhosted.org/packages/58/b5/c3fb087664b757be3f5501129f0ece9755c5b4ed77590d6520032d25a96f/scipy-1.11.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6df1468153a31cf55ed5ed39647279beb9cfb5d3f84369453b49e4b8502394fd", size = 36776822 }, - { url = "https://files.pythonhosted.org/packages/ac/a0/8b8e5495ba759f99ec99d90973d481e8a6682c320fcf875b4f084591f4d8/scipy-1.11.4-cp39-cp39-win_amd64.whl", hash = "sha256:ee410e6de8f88fd5cf6eadd73c135020bfbbbdfcd0f6162c36a7638a1ea8cc65", size = 44260005 }, +sdist = { url = "https://files.pythonhosted.org/packages/6e/1f/91144ba78dccea567a6466262922786ffc97be1e9b06ed9574ef0edc11e1/scipy-1.11.4.tar.gz", hash = "sha256:90a2b78e7f5733b9de748f589f09225013685f9b218275257f8a8168ededaeaa", size = 56336202, upload-time = "2023-11-18T21:06:08.277Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/c6/a32add319475d21f89733c034b99c81b3a7c6c7c19f96f80c7ca3ff1bbd4/scipy-1.11.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc9a714581f561af0848e6b69947fda0614915f072dfd14142ed1bfe1b806710", size = 37293259, upload-time = "2023-11-18T21:01:18.805Z" }, + { url = "https://files.pythonhosted.org/packages/de/0d/4fa68303568c70fd56fbf40668b6c6807cfee4cad975f07d80bdd26d013e/scipy-1.11.4-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:cf00bd2b1b0211888d4dc75656c0412213a8b25e80d73898083f402b50f47e41", size = 29760656, upload-time = "2023-11-18T21:01:41.815Z" }, + { url = "https://files.pythonhosted.org/packages/13/e5/8012be7857db6cbbbdbeea8a154dbacdfae845e95e1e19c028e82236d4a0/scipy-1.11.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9999c008ccf00e8fbcce1236f85ade5c569d13144f77a1946bef8863e8f6eb4", size = 32922489, upload-time = "2023-11-18T21:01:50.637Z" }, + { url = "https://files.pythonhosted.org/packages/e0/9e/80e2205d138960a49caea391f3710600895dd8292b6868dc9aff7aa593f9/scipy-1.11.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:933baf588daa8dc9a92c20a0be32f56d43faf3d1a60ab11b3f08c356430f6e56", size = 36442040, upload-time = "2023-11-18T21:02:00.119Z" }, + { url = "https://files.pythonhosted.org/packages/69/60/30a9c3fbe5066a3a93eefe3e2d44553df13587e6f792e1bff20dfed3d17e/scipy-1.11.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8fce70f39076a5aa62e92e69a7f62349f9574d8405c0a5de6ed3ef72de07f446", size = 36643257, upload-time = "2023-11-18T21:02:06.798Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ec/b46756f80e3f4c5f0989f6e4492c2851f156d9c239d554754a3c8cffd4e2/scipy-1.11.4-cp310-cp310-win_amd64.whl", hash = "sha256:6550466fbeec7453d7465e74d4f4b19f905642c89a7525571ee91dd7adabb5a3", size = 44149285, upload-time = "2023-11-18T21:02:15.592Z" }, + { url = "https://files.pythonhosted.org/packages/b8/f2/1aefbd5e54ebd8c6163ccf7f73e5d17bc8cb38738d312befc524fce84bb4/scipy-1.11.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f313b39a7e94f296025e3cffc2c567618174c0b1dde173960cf23808f9fae4be", size = 37159197, upload-time = "2023-11-18T21:02:21.959Z" }, + { url = "https://files.pythonhosted.org/packages/4b/48/20e77ddb1f473d4717a7d4d3fc8d15557f406f7708496054c59f635b7734/scipy-1.11.4-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:1b7c3dca977f30a739e0409fb001056484661cb2541a01aba0bb0029f7b68db8", size = 29675057, upload-time = "2023-11-18T21:02:28.169Z" }, + { url = "https://files.pythonhosted.org/packages/75/2e/a781862190d0e7e76afa74752ef363488a9a9d6ea86e46d5e5506cee8df6/scipy-1.11.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00150c5eae7b610c32589dda259eacc7c4f1665aedf25d921907f4d08a951b1c", size = 32882747, upload-time = "2023-11-18T21:02:33.683Z" }, + { url = "https://files.pythonhosted.org/packages/6b/d4/d62ce38ba00dc67d7ec4ec5cc19d36958d8ed70e63778715ad626bcbc796/scipy-1.11.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:530f9ad26440e85766509dbf78edcfe13ffd0ab7fec2560ee5c36ff74d6269ff", size = 36402732, upload-time = "2023-11-18T21:02:39.762Z" }, + { url = "https://files.pythonhosted.org/packages/88/86/827b56aea1ed04adbb044a675672a73c84d81076a350092bbfcfc1ae723b/scipy-1.11.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5e347b14fe01003d3b78e196e84bd3f48ffe4c8a7b8a1afbcb8f5505cb710993", size = 36622138, upload-time = "2023-11-18T21:02:45.968Z" }, + { url = "https://files.pythonhosted.org/packages/43/d0/f3cd75b62e1b90f48dbf091261b2fc7ceec14a700e308c50f6a69c83d337/scipy-1.11.4-cp311-cp311-win_amd64.whl", hash = "sha256:acf8ed278cc03f5aff035e69cb511741e0418681d25fbbb86ca65429c4f4d9cd", size = 44095631, upload-time = "2023-11-18T21:02:52.859Z" }, + { url = "https://files.pythonhosted.org/packages/df/64/8a690570485b636da614acff35fd725fcbc487f8b1fa9bdb12871b77412f/scipy-1.11.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:028eccd22e654b3ea01ee63705681ee79933652b2d8f873e7949898dda6d11b6", size = 37053653, upload-time = "2023-11-18T21:03:00.107Z" }, + { url = "https://files.pythonhosted.org/packages/5e/43/abf331745a7e5f4af51f13d40e2a72f516048db41ecbcf3ac6f86ada54a3/scipy-1.11.4-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:2c6ff6ef9cc27f9b3db93a6f8b38f97387e6e0591600369a297a50a8e96e835d", size = 29641601, upload-time = "2023-11-18T21:03:06.708Z" }, + { url = "https://files.pythonhosted.org/packages/47/9b/62d0ec086dd2871009da8769c504bec6e39b80f4c182c6ead0fcebd8b323/scipy-1.11.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b030c6674b9230d37c5c60ab456e2cf12f6784596d15ce8da9365e70896effc4", size = 32272137, upload-time = "2023-11-18T21:03:14.877Z" }, + { url = "https://files.pythonhosted.org/packages/08/77/f90f7306d755ac68bd159c50bb86fffe38400e533e8c609dd8484bd0f172/scipy-1.11.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad669df80528aeca5f557712102538f4f37e503f0c5b9541655016dd0932ca79", size = 35777534, upload-time = "2023-11-18T21:03:21.451Z" }, + { url = "https://files.pythonhosted.org/packages/00/de/b9f6938090c37b5092969ba1c67118e9114e8e6ef9d197251671444e839c/scipy-1.11.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ce7fff2e23ab2cc81ff452a9444c215c28e6305f396b2ba88343a567feec9660", size = 35963721, upload-time = "2023-11-18T21:03:27.85Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a1/357e4cd43af2748e1e0407ae0e9a5ea8aaaa6b702833c81be11670dcbad8/scipy-1.11.4-cp312-cp312-win_amd64.whl", hash = "sha256:36750b7733d960d7994888f0d148d31ea3017ac15eef664194b4ef68d36a4a97", size = 43730653, upload-time = "2023-11-18T21:03:34.758Z" }, + { url = "https://files.pythonhosted.org/packages/c5/e0/9872b7923c0ff7a420af8f559d0f5c6831143477b4ce57afe1b2a7c59a63/scipy-1.11.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6e619aba2df228a9b34718efb023966da781e89dd3d21637b27f2e54db0410d7", size = 37317855, upload-time = "2023-11-18T21:03:41.716Z" }, + { url = "https://files.pythonhosted.org/packages/d1/3a/0ab839bb67043ab35e5dcf8b611ca9e08e5a8933b0bc7506eedcec664aae/scipy-1.11.4-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:f3cd9e7b3c2c1ec26364856f9fbe78695fe631150f94cd1c22228456404cf1ec", size = 29741102, upload-time = "2023-11-18T21:03:47.368Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c3/1e498aa3d35ccfdf26c0fe81ebc52c540c454377e2690fc3738aabacaf8d/scipy-1.11.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d10e45a6c50211fe256da61a11c34927c68f277e03138777bdebedd933712fea", size = 33035888, upload-time = "2023-11-18T21:03:53.391Z" }, + { url = "https://files.pythonhosted.org/packages/db/86/bf3f01f003224c00dd94d9443d676023ed65d63ea2e34356888dc7fa8f48/scipy-1.11.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:91af76a68eeae0064887a48e25c4e616fa519fa0d38602eda7e0f97d65d57937", size = 36621737, upload-time = "2023-11-18T21:03:59.713Z" }, + { url = "https://files.pythonhosted.org/packages/58/b5/c3fb087664b757be3f5501129f0ece9755c5b4ed77590d6520032d25a96f/scipy-1.11.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6df1468153a31cf55ed5ed39647279beb9cfb5d3f84369453b49e4b8502394fd", size = 36776822, upload-time = "2023-11-18T21:04:06.315Z" }, + { url = "https://files.pythonhosted.org/packages/ac/a0/8b8e5495ba759f99ec99d90973d481e8a6682c320fcf875b4f084591f4d8/scipy-1.11.4-cp39-cp39-win_amd64.whl", hash = "sha256:ee410e6de8f88fd5cf6eadd73c135020bfbbbdfcd0f6162c36a7638a1ea8cc65", size = 44260005, upload-time = "2023-11-18T21:04:13.598Z" }, ] [[package]] name = "setuptools" version = "75.8.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/92/ec/089608b791d210aec4e7f97488e67ab0d33add3efccb83a056cbafe3a2a6/setuptools-75.8.0.tar.gz", hash = "sha256:c5afc8f407c626b8313a86e10311dd3f661c6cd9c09d4bf8c15c0e11f9f2b0e6", size = 1343222 } +sdist = { url = "https://files.pythonhosted.org/packages/92/ec/089608b791d210aec4e7f97488e67ab0d33add3efccb83a056cbafe3a2a6/setuptools-75.8.0.tar.gz", hash = "sha256:c5afc8f407c626b8313a86e10311dd3f661c6cd9c09d4bf8c15c0e11f9f2b0e6", size = 1343222, upload-time = "2025-01-08T18:28:23.98Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/69/8a/b9dc7678803429e4a3bc9ba462fa3dd9066824d3c607490235c6a796be5a/setuptools-75.8.0-py3-none-any.whl", hash = "sha256:e3982f444617239225d675215d51f6ba05f845d4eec313da4418fdbb56fb27e3", size = 1228782 }, -] - -[[package]] -name = "shiboken6" -version = "6.7.3" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/7f/85d2234371fd260393760656e1f5977c0b6e270a0a6aca6bf7231a3e60de/shiboken6-6.7.3-cp39-abi3-macosx_11_0_universal2.whl", hash = "sha256:285fe3cf79be3135fe1ad1e2b9ff6db3a48698887425af6aa6ed7a05a9abc3d6", size = 388568 }, - { url = "https://files.pythonhosted.org/packages/b2/e9/5c0c67de510da7818703d01123f7998cc495e99c0e6979a6ed6eb2ee09d0/shiboken6-6.7.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f0852e5781de78be5b13c140ec4c7fb9734e2aaf2986eb2d6a224363e03efccc", size = 189890 }, - { url = "https://files.pythonhosted.org/packages/ef/3c/bb524437df11ae79845b230d682b162935c7226b1fb7be2544dceca01e0f/shiboken6-6.7.3-cp39-abi3-manylinux_2_31_aarch64.whl", hash = "sha256:f0dd635178e64a45be2f84c9f33dd79ac30328da87f834f21a0baf69ae210e6e", size = 178147 }, - { url = "https://files.pythonhosted.org/packages/3e/cc/e2b95aedb5e5f900c20325ef347e51c7750d18369d157f9b7da99943dae1/shiboken6-6.7.3-cp39-abi3-win_amd64.whl", hash = "sha256:5f29325dfa86fde0274240f1f38e421303749d3174ce3ada178715b5f4719db9", size = 1136685 }, + { url = "https://files.pythonhosted.org/packages/69/8a/b9dc7678803429e4a3bc9ba462fa3dd9066824d3c607490235c6a796be5a/setuptools-75.8.0-py3-none-any.whl", hash = "sha256:e3982f444617239225d675215d51f6ba05f845d4eec313da4418fdbb56fb27e3", size = 1228782, upload-time = "2025-01-08T18:28:20.912Z" }, ] [[package]] name = "six" version = "1.17.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] [[package]] @@ -1610,7 +1686,7 @@ dependencies = [ { name = "matplotlib" }, { name = "numpy" }, { name = "opencv-contrib-python" }, - { name = "pyside6" }, + { name = "pydantic" }, { name = "scipy" }, { name = "setuptools" }, { name = "toml" }, @@ -1642,7 +1718,7 @@ requires-dist = [ { name = "numpy", specifier = "==1.26.2" }, { name = "opencv-contrib-python", specifier = "==4.8.*" }, { name = "pip-tools", marker = "extra == 'dev'" }, - { name = "pyside6", specifier = ">=6.6,<6.8" }, + { name = "pydantic", specifier = ">=2.0" }, { name = "pytest", marker = "extra == 'dev'" }, { name = "scipy", specifier = "==1.11.4" }, { name = "setuptools", specifier = "==75.8.0" }, @@ -1658,15 +1734,15 @@ dependencies = [ { name = "cffi" }, { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e1/41/9b873a8c055582859b239be17902a85339bec6a30ad162f98c9b0288a2cc/soundfile-0.13.1.tar.gz", hash = "sha256:b2c68dab1e30297317080a5b43df57e302584c49e2942defdde0acccc53f0e5b", size = 46156 } +sdist = { url = "https://files.pythonhosted.org/packages/e1/41/9b873a8c055582859b239be17902a85339bec6a30ad162f98c9b0288a2cc/soundfile-0.13.1.tar.gz", hash = "sha256:b2c68dab1e30297317080a5b43df57e302584c49e2942defdde0acccc53f0e5b", size = 46156, upload-time = "2025-01-25T09:17:04.831Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/64/28/e2a36573ccbcf3d57c00626a21fe51989380636e821b341d36ccca0c1c3a/soundfile-0.13.1-py2.py3-none-any.whl", hash = "sha256:a23c717560da2cf4c7b5ae1142514e0fd82d6bbd9dfc93a50423447142f2c445", size = 25751 }, - { url = "https://files.pythonhosted.org/packages/ea/ab/73e97a5b3cc46bba7ff8650a1504348fa1863a6f9d57d7001c6b67c5f20e/soundfile-0.13.1-py2.py3-none-macosx_10_9_x86_64.whl", hash = "sha256:82dc664d19831933fe59adad199bf3945ad06d84bc111a5b4c0d3089a5b9ec33", size = 1142250 }, - { url = "https://files.pythonhosted.org/packages/a0/e5/58fd1a8d7b26fc113af244f966ee3aecf03cb9293cb935daaddc1e455e18/soundfile-0.13.1-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:743f12c12c4054921e15736c6be09ac26b3b3d603aef6fd69f9dde68748f2593", size = 1101406 }, - { url = "https://files.pythonhosted.org/packages/58/ae/c0e4a53d77cf6e9a04179535766b3321b0b9ced5f70522e4caf9329f0046/soundfile-0.13.1-py2.py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:9c9e855f5a4d06ce4213f31918653ab7de0c5a8d8107cd2427e44b42df547deb", size = 1235729 }, - { url = "https://files.pythonhosted.org/packages/57/5e/70bdd9579b35003a489fc850b5047beeda26328053ebadc1fb60f320f7db/soundfile-0.13.1-py2.py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:03267c4e493315294834a0870f31dbb3b28a95561b80b134f0bd3cf2d5f0e618", size = 1313646 }, - { url = "https://files.pythonhosted.org/packages/fe/df/8c11dc4dfceda14e3003bb81a0d0edcaaf0796dd7b4f826ea3e532146bba/soundfile-0.13.1-py2.py3-none-win32.whl", hash = "sha256:c734564fab7c5ddf8e9be5bf70bab68042cd17e9c214c06e365e20d64f9a69d5", size = 899881 }, - { url = "https://files.pythonhosted.org/packages/14/e9/6b761de83277f2f02ded7e7ea6f07828ec78e4b229b80e4ca55dd205b9dc/soundfile-0.13.1-py2.py3-none-win_amd64.whl", hash = "sha256:1e70a05a0626524a69e9f0f4dd2ec174b4e9567f4d8b6c11d38b5c289be36ee9", size = 1019162 }, + { url = "https://files.pythonhosted.org/packages/64/28/e2a36573ccbcf3d57c00626a21fe51989380636e821b341d36ccca0c1c3a/soundfile-0.13.1-py2.py3-none-any.whl", hash = "sha256:a23c717560da2cf4c7b5ae1142514e0fd82d6bbd9dfc93a50423447142f2c445", size = 25751, upload-time = "2025-01-25T09:16:44.235Z" }, + { url = "https://files.pythonhosted.org/packages/ea/ab/73e97a5b3cc46bba7ff8650a1504348fa1863a6f9d57d7001c6b67c5f20e/soundfile-0.13.1-py2.py3-none-macosx_10_9_x86_64.whl", hash = "sha256:82dc664d19831933fe59adad199bf3945ad06d84bc111a5b4c0d3089a5b9ec33", size = 1142250, upload-time = "2025-01-25T09:16:47.583Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e5/58fd1a8d7b26fc113af244f966ee3aecf03cb9293cb935daaddc1e455e18/soundfile-0.13.1-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:743f12c12c4054921e15736c6be09ac26b3b3d603aef6fd69f9dde68748f2593", size = 1101406, upload-time = "2025-01-25T09:16:49.662Z" }, + { url = "https://files.pythonhosted.org/packages/58/ae/c0e4a53d77cf6e9a04179535766b3321b0b9ced5f70522e4caf9329f0046/soundfile-0.13.1-py2.py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:9c9e855f5a4d06ce4213f31918653ab7de0c5a8d8107cd2427e44b42df547deb", size = 1235729, upload-time = "2025-01-25T09:16:53.018Z" }, + { url = "https://files.pythonhosted.org/packages/57/5e/70bdd9579b35003a489fc850b5047beeda26328053ebadc1fb60f320f7db/soundfile-0.13.1-py2.py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:03267c4e493315294834a0870f31dbb3b28a95561b80b134f0bd3cf2d5f0e618", size = 1313646, upload-time = "2025-01-25T09:16:54.872Z" }, + { url = "https://files.pythonhosted.org/packages/fe/df/8c11dc4dfceda14e3003bb81a0d0edcaaf0796dd7b4f826ea3e532146bba/soundfile-0.13.1-py2.py3-none-win32.whl", hash = "sha256:c734564fab7c5ddf8e9be5bf70bab68042cd17e9c214c06e365e20d64f9a69d5", size = 899881, upload-time = "2025-01-25T09:16:56.663Z" }, + { url = "https://files.pythonhosted.org/packages/14/e9/6b761de83277f2f02ded7e7ea6f07828ec78e4b229b80e4ca55dd205b9dc/soundfile-0.13.1-py2.py3-none-win_amd64.whl", hash = "sha256:1e70a05a0626524a69e9f0f4dd2ec174b4e9567f4d8b6c11d38b5c289be36ee9", size = 1019162, upload-time = "2025-01-25T09:16:59.573Z" }, ] [[package]] @@ -1676,28 +1752,28 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/02/c0/4429bf9b3be10e749149e286aa5c53775399ec62891c6b970456c6dca325/soxr-0.5.0.post1.tar.gz", hash = "sha256:7092b9f3e8a416044e1fa138c8172520757179763b85dc53aa9504f4813cff73", size = 170853 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7d/96/bee1eb69d66fc28c3b219ba9b8674b49d3dcc6cd2f9b3e5114ff28cf88b5/soxr-0.5.0.post1-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:7406d782d85f8cf64e66b65e6b7721973de8a1dc50b9e88bc2288c343a987484", size = 203841 }, - { url = "https://files.pythonhosted.org/packages/1f/5d/56ad3d181d30d103128f65cc44f4c4e24c199e6d5723e562704e47c89f78/soxr-0.5.0.post1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fa0a382fb8d8e2afed2c1642723b2d2d1b9a6728ff89f77f3524034c8885b8c9", size = 160192 }, - { url = "https://files.pythonhosted.org/packages/7f/09/e43c39390e26b4c1b8d46f8a1c252a5077fa9f81cc2326b03c3d2b85744e/soxr-0.5.0.post1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b01d3efb95a2851f78414bcd00738b0253eec3f5a1e5482838e965ffef84969", size = 221176 }, - { url = "https://files.pythonhosted.org/packages/ba/e6/059070b4cdb7fdd8ffbb67c5087c1da9716577127fb0540cd11dbf77923b/soxr-0.5.0.post1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fcc049b0a151a65aa75b92f0ac64bb2dba785d16b78c31c2b94e68c141751d6d", size = 252779 }, - { url = "https://files.pythonhosted.org/packages/ad/64/86082b6372e5ff807dfa79b857da9f50e94e155706000daa43fdc3b59851/soxr-0.5.0.post1-cp310-cp310-win_amd64.whl", hash = "sha256:97f269bc26937c267a2ace43a77167d0c5c8bba5a2b45863bb6042b5b50c474e", size = 166881 }, - { url = "https://files.pythonhosted.org/packages/29/28/dc62dae260a77603e8257e9b79078baa2ca4c0b4edc6f9f82c9113d6ef18/soxr-0.5.0.post1-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:6fb77b626773a966e3d8f6cb24f6f74b5327fa5dc90f1ff492450e9cdc03a378", size = 203648 }, - { url = "https://files.pythonhosted.org/packages/0e/48/3e88329a695f6e0e38a3b171fff819d75d7cc055dae1ec5d5074f34d61e3/soxr-0.5.0.post1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:39e0f791ba178d69cd676485dbee37e75a34f20daa478d90341ecb7f6d9d690f", size = 159933 }, - { url = "https://files.pythonhosted.org/packages/9c/a5/6b439164be6871520f3d199554568a7656e96a867adbbe5bac179caf5776/soxr-0.5.0.post1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f0b558f445ba4b64dbcb37b5f803052eee7d93b1dbbbb97b3ec1787cb5a28eb", size = 221010 }, - { url = "https://files.pythonhosted.org/packages/9f/e5/400e3bf7f29971abad85cb877e290060e5ec61fccd2fa319e3d85709c1be/soxr-0.5.0.post1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca6903671808e0a6078b0d146bb7a2952b118dfba44008b2aa60f221938ba829", size = 252471 }, - { url = "https://files.pythonhosted.org/packages/86/94/6a7e91bea7e6ca193ee429869b8f18548cd79759e064021ecb5756024c7c/soxr-0.5.0.post1-cp311-cp311-win_amd64.whl", hash = "sha256:c4d8d5283ed6f5efead0df2c05ae82c169cfdfcf5a82999c2d629c78b33775e8", size = 166723 }, - { url = "https://files.pythonhosted.org/packages/5d/e3/d422d279e51e6932e7b64f1170a4f61a7ee768e0f84c9233a5b62cd2c832/soxr-0.5.0.post1-cp312-abi3-macosx_10_14_x86_64.whl", hash = "sha256:fef509466c9c25f65eae0ce1e4b9ac9705d22c6038c914160ddaf459589c6e31", size = 199993 }, - { url = "https://files.pythonhosted.org/packages/20/f1/88adaca3c52e03bcb66b63d295df2e2d35bf355d19598c6ce84b20be7fca/soxr-0.5.0.post1-cp312-abi3-macosx_11_0_arm64.whl", hash = "sha256:4704ba6b13a3f1e41d12acf192878384c1c31f71ce606829c64abdf64a8d7d32", size = 156373 }, - { url = "https://files.pythonhosted.org/packages/b8/38/bad15a9e615215c8219652ca554b601663ac3b7ac82a284aca53ec2ff48c/soxr-0.5.0.post1-cp312-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd052a66471a7335b22a6208601a9d0df7b46b8d087dce4ff6e13eed6a33a2a1", size = 216564 }, - { url = "https://files.pythonhosted.org/packages/e1/1a/569ea0420a0c4801c2c8dd40d8d544989522f6014d51def689125f3f2935/soxr-0.5.0.post1-cp312-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3f16810dd649ab1f433991d2a9661e9e6a116c2b4101039b53b3c3e90a094fc", size = 248455 }, - { url = "https://files.pythonhosted.org/packages/bc/10/440f1ba3d4955e0dc740bbe4ce8968c254a3d644d013eb75eea729becdb8/soxr-0.5.0.post1-cp312-abi3-win_amd64.whl", hash = "sha256:b1be9fee90afb38546bdbd7bde714d1d9a8c5a45137f97478a83b65e7f3146f6", size = 164937 }, - { url = "https://files.pythonhosted.org/packages/d9/7b/c8d797235d06ae316e0c9bc2b1d0d5d948834dfac17eba0ad10fd177524b/soxr-0.5.0.post1-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:c5af7b355959061beb90a1d73c4834ece4549f07b708f8c73c088153cec29935", size = 204073 }, - { url = "https://files.pythonhosted.org/packages/88/5c/f6cf6b90ce1628def17c746d6cde9991fdd29667ef1d5fb5bd3b22eb788f/soxr-0.5.0.post1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e1dda616fc797b1507b65486f3116ed2c929f13c722922963dd419d64ada6c07", size = 160375 }, - { url = "https://files.pythonhosted.org/packages/a5/4a/6a11d62cfd6383c88f4918bdc5191d9c437f649c9101ceb5eec7e2837f0b/soxr-0.5.0.post1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94de2812368e98cb42b4eaeddf8ee1657ecc19bd053f8e67b9b5aa12a3592012", size = 221450 }, - { url = "https://files.pythonhosted.org/packages/25/d1/83a66e795381ddfc5c3ebf34cc0ac68735c7c459ed1fe65a2193a52c57b1/soxr-0.5.0.post1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c8e9c980637e03d3f345a4fd81d56477a58c294fb26205fa121bc4eb23d9d01", size = 253025 }, - { url = "https://files.pythonhosted.org/packages/cd/2e/1fbad5cc8c49c45a0b94221d16445792f55b63fe4f6d3885db960d92892c/soxr-0.5.0.post1-cp39-cp39-win_amd64.whl", hash = "sha256:7e71b0b0db450f36de70f1047505231db77a713f8c47df9342582ae8a4b828f2", size = 167292 }, +sdist = { url = "https://files.pythonhosted.org/packages/02/c0/4429bf9b3be10e749149e286aa5c53775399ec62891c6b970456c6dca325/soxr-0.5.0.post1.tar.gz", hash = "sha256:7092b9f3e8a416044e1fa138c8172520757179763b85dc53aa9504f4813cff73", size = 170853, upload-time = "2024-08-31T03:43:33.058Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/96/bee1eb69d66fc28c3b219ba9b8674b49d3dcc6cd2f9b3e5114ff28cf88b5/soxr-0.5.0.post1-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:7406d782d85f8cf64e66b65e6b7721973de8a1dc50b9e88bc2288c343a987484", size = 203841, upload-time = "2024-08-31T03:42:59.186Z" }, + { url = "https://files.pythonhosted.org/packages/1f/5d/56ad3d181d30d103128f65cc44f4c4e24c199e6d5723e562704e47c89f78/soxr-0.5.0.post1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fa0a382fb8d8e2afed2c1642723b2d2d1b9a6728ff89f77f3524034c8885b8c9", size = 160192, upload-time = "2024-08-31T03:43:01.128Z" }, + { url = "https://files.pythonhosted.org/packages/7f/09/e43c39390e26b4c1b8d46f8a1c252a5077fa9f81cc2326b03c3d2b85744e/soxr-0.5.0.post1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b01d3efb95a2851f78414bcd00738b0253eec3f5a1e5482838e965ffef84969", size = 221176, upload-time = "2024-08-31T03:43:02.663Z" }, + { url = "https://files.pythonhosted.org/packages/ba/e6/059070b4cdb7fdd8ffbb67c5087c1da9716577127fb0540cd11dbf77923b/soxr-0.5.0.post1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fcc049b0a151a65aa75b92f0ac64bb2dba785d16b78c31c2b94e68c141751d6d", size = 252779, upload-time = "2024-08-31T03:43:04.582Z" }, + { url = "https://files.pythonhosted.org/packages/ad/64/86082b6372e5ff807dfa79b857da9f50e94e155706000daa43fdc3b59851/soxr-0.5.0.post1-cp310-cp310-win_amd64.whl", hash = "sha256:97f269bc26937c267a2ace43a77167d0c5c8bba5a2b45863bb6042b5b50c474e", size = 166881, upload-time = "2024-08-31T03:43:06.255Z" }, + { url = "https://files.pythonhosted.org/packages/29/28/dc62dae260a77603e8257e9b79078baa2ca4c0b4edc6f9f82c9113d6ef18/soxr-0.5.0.post1-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:6fb77b626773a966e3d8f6cb24f6f74b5327fa5dc90f1ff492450e9cdc03a378", size = 203648, upload-time = "2024-08-31T03:43:08.339Z" }, + { url = "https://files.pythonhosted.org/packages/0e/48/3e88329a695f6e0e38a3b171fff819d75d7cc055dae1ec5d5074f34d61e3/soxr-0.5.0.post1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:39e0f791ba178d69cd676485dbee37e75a34f20daa478d90341ecb7f6d9d690f", size = 159933, upload-time = "2024-08-31T03:43:10.053Z" }, + { url = "https://files.pythonhosted.org/packages/9c/a5/6b439164be6871520f3d199554568a7656e96a867adbbe5bac179caf5776/soxr-0.5.0.post1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f0b558f445ba4b64dbcb37b5f803052eee7d93b1dbbbb97b3ec1787cb5a28eb", size = 221010, upload-time = "2024-08-31T03:43:11.839Z" }, + { url = "https://files.pythonhosted.org/packages/9f/e5/400e3bf7f29971abad85cb877e290060e5ec61fccd2fa319e3d85709c1be/soxr-0.5.0.post1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca6903671808e0a6078b0d146bb7a2952b118dfba44008b2aa60f221938ba829", size = 252471, upload-time = "2024-08-31T03:43:13.347Z" }, + { url = "https://files.pythonhosted.org/packages/86/94/6a7e91bea7e6ca193ee429869b8f18548cd79759e064021ecb5756024c7c/soxr-0.5.0.post1-cp311-cp311-win_amd64.whl", hash = "sha256:c4d8d5283ed6f5efead0df2c05ae82c169cfdfcf5a82999c2d629c78b33775e8", size = 166723, upload-time = "2024-08-31T03:43:15.212Z" }, + { url = "https://files.pythonhosted.org/packages/5d/e3/d422d279e51e6932e7b64f1170a4f61a7ee768e0f84c9233a5b62cd2c832/soxr-0.5.0.post1-cp312-abi3-macosx_10_14_x86_64.whl", hash = "sha256:fef509466c9c25f65eae0ce1e4b9ac9705d22c6038c914160ddaf459589c6e31", size = 199993, upload-time = "2024-08-31T03:43:17.24Z" }, + { url = "https://files.pythonhosted.org/packages/20/f1/88adaca3c52e03bcb66b63d295df2e2d35bf355d19598c6ce84b20be7fca/soxr-0.5.0.post1-cp312-abi3-macosx_11_0_arm64.whl", hash = "sha256:4704ba6b13a3f1e41d12acf192878384c1c31f71ce606829c64abdf64a8d7d32", size = 156373, upload-time = "2024-08-31T03:43:18.633Z" }, + { url = "https://files.pythonhosted.org/packages/b8/38/bad15a9e615215c8219652ca554b601663ac3b7ac82a284aca53ec2ff48c/soxr-0.5.0.post1-cp312-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd052a66471a7335b22a6208601a9d0df7b46b8d087dce4ff6e13eed6a33a2a1", size = 216564, upload-time = "2024-08-31T03:43:20.789Z" }, + { url = "https://files.pythonhosted.org/packages/e1/1a/569ea0420a0c4801c2c8dd40d8d544989522f6014d51def689125f3f2935/soxr-0.5.0.post1-cp312-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3f16810dd649ab1f433991d2a9661e9e6a116c2b4101039b53b3c3e90a094fc", size = 248455, upload-time = "2024-08-31T03:43:22.165Z" }, + { url = "https://files.pythonhosted.org/packages/bc/10/440f1ba3d4955e0dc740bbe4ce8968c254a3d644d013eb75eea729becdb8/soxr-0.5.0.post1-cp312-abi3-win_amd64.whl", hash = "sha256:b1be9fee90afb38546bdbd7bde714d1d9a8c5a45137f97478a83b65e7f3146f6", size = 164937, upload-time = "2024-08-31T03:43:23.671Z" }, + { url = "https://files.pythonhosted.org/packages/d9/7b/c8d797235d06ae316e0c9bc2b1d0d5d948834dfac17eba0ad10fd177524b/soxr-0.5.0.post1-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:c5af7b355959061beb90a1d73c4834ece4549f07b708f8c73c088153cec29935", size = 204073, upload-time = "2024-08-31T03:43:24.935Z" }, + { url = "https://files.pythonhosted.org/packages/88/5c/f6cf6b90ce1628def17c746d6cde9991fdd29667ef1d5fb5bd3b22eb788f/soxr-0.5.0.post1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e1dda616fc797b1507b65486f3116ed2c929f13c722922963dd419d64ada6c07", size = 160375, upload-time = "2024-08-31T03:43:26.742Z" }, + { url = "https://files.pythonhosted.org/packages/a5/4a/6a11d62cfd6383c88f4918bdc5191d9c437f649c9101ceb5eec7e2837f0b/soxr-0.5.0.post1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94de2812368e98cb42b4eaeddf8ee1657ecc19bd053f8e67b9b5aa12a3592012", size = 221450, upload-time = "2024-08-31T03:43:28.048Z" }, + { url = "https://files.pythonhosted.org/packages/25/d1/83a66e795381ddfc5c3ebf34cc0ac68735c7c459ed1fe65a2193a52c57b1/soxr-0.5.0.post1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c8e9c980637e03d3f345a4fd81d56477a58c294fb26205fa121bc4eb23d9d01", size = 253025, upload-time = "2024-08-31T03:43:29.893Z" }, + { url = "https://files.pythonhosted.org/packages/cd/2e/1fbad5cc8c49c45a0b94221d16445792f55b63fe4f6d3885db960d92892c/soxr-0.5.0.post1-cp39-cp39-win_amd64.whl", hash = "sha256:7e71b0b0db450f36de70f1047505231db77a713f8c47df9342582ae8a4b828f2", size = 167292, upload-time = "2024-08-31T03:43:31.313Z" }, ] [[package]] @@ -1707,56 +1783,56 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pbr" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/28/3f/13cacea96900bbd31bb05c6b74135f85d15564fc583802be56976c940470/stevedore-5.4.1.tar.gz", hash = "sha256:3135b5ae50fe12816ef291baff420acb727fcd356106e3e9cbfa9e5985cd6f4b", size = 513858 } +sdist = { url = "https://files.pythonhosted.org/packages/28/3f/13cacea96900bbd31bb05c6b74135f85d15564fc583802be56976c940470/stevedore-5.4.1.tar.gz", hash = "sha256:3135b5ae50fe12816ef291baff420acb727fcd356106e3e9cbfa9e5985cd6f4b", size = 513858, upload-time = "2025-02-20T14:03:57.285Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/45/8c4ebc0c460e6ec38e62ab245ad3c7fc10b210116cea7c16d61602aa9558/stevedore-5.4.1-py3-none-any.whl", hash = "sha256:d10a31c7b86cba16c1f6e8d15416955fc797052351a56af15e608ad20811fcfe", size = 49533 }, + { url = "https://files.pythonhosted.org/packages/f7/45/8c4ebc0c460e6ec38e62ab245ad3c7fc10b210116cea7c16d61602aa9558/stevedore-5.4.1-py3-none-any.whl", hash = "sha256:d10a31c7b86cba16c1f6e8d15416955fc797052351a56af15e608ad20811fcfe", size = 49533, upload-time = "2025-02-20T14:03:55.849Z" }, ] [[package]] name = "threadpoolctl" version = "3.6.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274 } +sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274, upload-time = "2025-03-13T13:49:23.031Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638 }, + { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" }, ] [[package]] name = "toml" version = "0.10.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253 } +sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253, upload-time = "2020-11-01T01:40:22.204Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588 }, + { url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588, upload-time = "2020-11-01T01:40:20.672Z" }, ] [[package]] name = "tomli" version = "2.2.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 }, - { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 }, - { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 }, - { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 }, - { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 }, - { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 }, - { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 }, - { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 }, - { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 }, - { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 }, - { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762 }, - { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453 }, - { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486 }, - { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349 }, - { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159 }, - { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243 }, - { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645 }, - { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584 }, - { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 }, - { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 }, - { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" }, + { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" }, + { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" }, + { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" }, + { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" }, + { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" }, + { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" }, + { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" }, + { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" }, + { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload-time = "2024-11-27T22:38:07.731Z" }, + { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload-time = "2024-11-27T22:38:09.384Z" }, + { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload-time = "2024-11-27T22:38:10.329Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload-time = "2024-11-27T22:38:11.443Z" }, + { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload-time = "2024-11-27T22:38:13.099Z" }, + { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload-time = "2024-11-27T22:38:14.766Z" }, + { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload-time = "2024-11-27T22:38:15.843Z" }, + { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload-time = "2024-11-27T22:38:17.645Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" }, + { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" }, + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, ] [[package]] @@ -1766,43 +1842,55 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737 } +sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540 }, + { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, ] [[package]] name = "typing-extensions" -version = "4.13.2" +version = "4.16.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967 } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806 }, + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, ] [[package]] name = "urllib3" version = "2.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672 } +sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672, upload-time = "2025-04-10T15:23:39.232Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680 }, + { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680, upload-time = "2025-04-10T15:23:37.377Z" }, ] [[package]] name = "wheel" version = "0.45.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8a/98/2d9906746cdc6a6ef809ae6338005b3f21bb568bea3165cfc6a243fdc25c/wheel-0.45.1.tar.gz", hash = "sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729", size = 107545 } +sdist = { url = "https://files.pythonhosted.org/packages/8a/98/2d9906746cdc6a6ef809ae6338005b3f21bb568bea3165cfc6a243fdc25c/wheel-0.45.1.tar.gz", hash = "sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729", size = 107545, upload-time = "2024-11-23T00:18:23.513Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/2c/87f3254fd8ffd29e4c02732eee68a83a1d3c346ae39bc6822dcbcb697f2b/wheel-0.45.1-py3-none-any.whl", hash = "sha256:708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248", size = 72494 }, + { url = "https://files.pythonhosted.org/packages/0b/2c/87f3254fd8ffd29e4c02732eee68a83a1d3c346ae39bc6822dcbcb697f2b/wheel-0.45.1-py3-none-any.whl", hash = "sha256:708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248", size = 72494, upload-time = "2024-11-23T00:18:21.207Z" }, ] [[package]] name = "zipp" version = "3.21.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3f/50/bad581df71744867e9468ebd0bcd6505de3b275e06f202c2cb016e3ff56f/zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4", size = 24545 } +sdist = { url = "https://files.pythonhosted.org/packages/3f/50/bad581df71744867e9468ebd0bcd6505de3b275e06f202c2cb016e3ff56f/zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4", size = 24545, upload-time = "2024-11-10T15:05:20.202Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/1a/7e4798e9339adc931158c9d69ecc34f5e6791489d469f5e50ec15e35f458/zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931", size = 9630 }, + { url = "https://files.pythonhosted.org/packages/b7/1a/7e4798e9339adc931158c9d69ecc34f5e6791489d469f5e50ec15e35f458/zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931", size = 9630, upload-time = "2024-11-10T15:05:19.275Z" }, ] From 4b99e13d55fadd37b39f51d565590019a58a588e Mon Sep 17 00:00:00 2001 From: philipqueen Date: Mon, 10 Aug 2026 11:01:38 -0600 Subject: [PATCH 04/16] Guard against mpl failures, better checks/reporting on final frame count --- pyproject.toml | 5 +- skelly_synchronize/__main__.py | 6 + skelly_synchronize/cli/__init__.py | 0 skelly_synchronize/cli/main.py | 135 +++++++++++++++++++ skelly_synchronize/core/debug.py | 9 +- skelly_synchronize/core/models.py | 3 + skelly_synchronize/core/pipeline/runner.py | 1 + skelly_synchronize/core/pipeline/stages.py | 83 +++++++----- skelly_synchronize/tests/cli/test_main.py | 98 ++++++++++++++ skelly_synchronize/tests/core/test_models.py | 13 ++ 10 files changed, 316 insertions(+), 37 deletions(-) create mode 100644 skelly_synchronize/__main__.py create mode 100644 skelly_synchronize/cli/__init__.py create mode 100644 skelly_synchronize/cli/main.py create mode 100644 skelly_synchronize/tests/cli/test_main.py diff --git a/pyproject.toml b/pyproject.toml index 4d6663d..aabee0c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,7 +34,7 @@ dependencies = [ "setuptools==75.8.0" ] -requires-python = ">=3.9,<3.13" +requires-python = ">=3.10,<3.13" dynamic = ["version"] @@ -56,6 +56,9 @@ Homepage = "https://freemocap.org" Documentation = "https://freemocap.github.io/skelly_synchronize/" Github = "https://github.com/freemocap/skelly_synchronize" +[project.scripts] +skelly-synchronize = "skelly_synchronize.cli.main:main" + [tool.bumpver] #bump the version by entering `bumpver update` in the terminal current_version = "v2025.04.1037" version_pattern = "vYYYY.0M.BUILD[-TAG]" diff --git a/skelly_synchronize/__main__.py b/skelly_synchronize/__main__.py new file mode 100644 index 0000000..c329c86 --- /dev/null +++ b/skelly_synchronize/__main__.py @@ -0,0 +1,6 @@ +import sys + +from skelly_synchronize.cli.main import main + +if __name__ == "__main__": + sys.exit(main()) diff --git a/skelly_synchronize/cli/__init__.py b/skelly_synchronize/cli/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/skelly_synchronize/cli/main.py b/skelly_synchronize/cli/main.py new file mode 100644 index 0000000..53c5d9b --- /dev/null +++ b/skelly_synchronize/cli/main.py @@ -0,0 +1,135 @@ +import argparse +import logging +import sys +from pathlib import Path + +from skelly_synchronize import __version__ +from skelly_synchronize.core.exceptions import SkellySyncError +from skelly_synchronize.core.logging_setup import configure_logging +from skelly_synchronize.core.models import SyncMethod, SyncRequest, VideoBackendKind +from skelly_synchronize.core.pipeline.runner import run_pipeline + +_METHOD_CHOICES = { + "audio": SyncMethod.AUDIO, + "brightness": SyncMethod.BRIGHTNESS, +} + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + prog="skelly-synchronize", + description=( + "Synchronize multi-camera video recordings post-recording, " + "without needing timestamps." + ), + ) + parser.add_argument( + "raw_video_folder_path", + type=Path, + help="Folder containing the raw video files to synchronize.", + ) + parser.add_argument( + "-o", + "--output", + dest="synchronized_video_folder_path", + type=Path, + default=None, + help=( + "Folder to write synchronized videos to. Defaults to a " + "'synchronized_videos' folder next to the raw video folder." + ), + ) + parser.add_argument( + "-m", + "--method", + choices=sorted(_METHOD_CHOICES), + default="audio", + help="Synchronization strategy to use (default: %(default)s).", + ) + parser.add_argument( + "--video-handler", + choices=[kind.value for kind in VideoBackendKind], + default=VideoBackendKind.DEFFCODE.value, + help="Backend used to trim videos (default: %(default)s).", + ) + parser.add_argument( + "--brightness-threshold", + dest="brightness_ratio_threshold", + type=float, + default=1000.0, + help=( + "Brightness ratio threshold used to detect the sync event " + "(only used with --method brightness; default: %(default)s)." + ), + ) + parser.add_argument( + "--no-debug-artifacts", + dest="create_debug_artifacts", + action="store_false", + default=True, + help="Skip writing synchronization_debug.toml and debug_plot.png.", + ) + parser.add_argument( + "-v", + "--verbose", + action="store_true", + help="Enable debug logging.", + ) + parser.add_argument( + "--version", + action="version", + version=f"%(prog)s {__version__}", + ) + return parser + + +def main(argv: list[str] | None = None) -> int: + parser = build_parser() + args = parser.parse_args(argv) + + configure_logging(level=logging.DEBUG if args.verbose else logging.INFO) + + if not args.raw_video_folder_path.is_dir(): + print( + f"Error: raw video folder does not exist or is not a directory: " + f"{args.raw_video_folder_path}", + file=sys.stderr, + ) + return 1 + + request = SyncRequest( + raw_video_folder_path=args.raw_video_folder_path, + synchronized_video_folder_path=args.synchronized_video_folder_path, + method=_METHOD_CHOICES[args.method], + video_handler=VideoBackendKind(args.video_handler), + brightness_ratio_threshold=args.brightness_ratio_threshold, + create_debug_artifacts=args.create_debug_artifacts, + ) + + def progress_callback(camera_name: str, progress: float) -> None: + if progress >= 1.0: + print(f" {camera_name}: trimmed") + + try: + result = run_pipeline(request, progress_callback) + except SkellySyncError as e: + print(f"Error: {e}", file=sys.stderr) + return 1 + + print(f"Synchronized videos written to: {result.synchronized_video_folder_path}") + print(f"Elapsed: {result.elapsed_seconds:.2f}s") + print(f"Synchronized frame count: {result.synchronized_frame_count}") + print("Lags:") + for lag in result.lags: + print(f" {lag.camera_name}: {lag.lag_seconds:.4f}s") + print(f"Final video length: {result.synchronized_frame_count} frames") + if result.debug_artifact_paths: + print("Debug artifacts:") + for artifact_path in result.debug_artifact_paths: + print(f" {artifact_path}") + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/skelly_synchronize/core/debug.py b/skelly_synchronize/core/debug.py index b06444a..f14c63d 100644 --- a/skelly_synchronize/core/debug.py +++ b/skelly_synchronize/core/debug.py @@ -2,9 +2,16 @@ from pathlib import Path import librosa +import matplotlib import numpy as np import toml -from matplotlib import pyplot as plt + +# Force the non-interactive Agg backend: these plots are only ever saved to +# file, never shown, and matplotlib's auto-selected GUI backend (macosx/Qt/Tk) +# can crash when the pipeline is invoked from a thread or process other than +# a GUI app's main thread (e.g. from a Qt-based caller, or a worker process). +matplotlib.use("Agg") +from matplotlib import pyplot as plt # noqa: E402 from skelly_synchronize.core.models import LagResult, VideoInfo diff --git a/skelly_synchronize/core/models.py b/skelly_synchronize/core/models.py index 8f5496e..2d888ef 100644 --- a/skelly_synchronize/core/models.py +++ b/skelly_synchronize/core/models.py @@ -54,3 +54,6 @@ class SyncResult(BaseModel): lags: list[LagResult] debug_artifact_paths: list[Path] elapsed_seconds: float + # The single frame count shared by every synchronized video -- verified + # identical across cameras by VerifySynchronizedFrameCountStage + synchronized_frame_count: int | None = None diff --git a/skelly_synchronize/core/pipeline/runner.py b/skelly_synchronize/core/pipeline/runner.py index 3fe77a7..93990a2 100644 --- a/skelly_synchronize/core/pipeline/runner.py +++ b/skelly_synchronize/core/pipeline/runner.py @@ -71,6 +71,7 @@ def run( lags=context.lags, debug_artifact_paths=context.debug_artifact_paths, elapsed_seconds=time.time() - start_time, + synchronized_frame_count=context.synchronized_frame_count, ) diff --git a/skelly_synchronize/core/pipeline/stages.py b/skelly_synchronize/core/pipeline/stages.py index 1a6ca17..24d2fc2 100644 --- a/skelly_synchronize/core/pipeline/stages.py +++ b/skelly_synchronize/core/pipeline/stages.py @@ -447,45 +447,58 @@ def run( artifact_paths.append(toml_path) plot_path = synced_folder / DEBUG_PLOT_NAME - if context.request.method == SyncMethod.AUDIO: - raw_audio_paths = sorted( - context.audio_folder_path.glob(f"*.{AudioExtension.WAV.value}") - ) - trimmed_audio_paths = sorted( - (context.audio_folder_path / TRIMMED_AUDIO_FOLDER_NAME).glob( - f"*.{AudioExtension.WAV.value}" + try: + if context.request.method == SyncMethod.AUDIO: + raw_audio_paths = sorted( + context.audio_folder_path.glob(f"*.{AudioExtension.WAV.value}") ) - ) - plot_audio_waveforms(raw_audio_paths, trimmed_audio_paths, plot_path) - else: - # Whether debug data comes from the raw or normalized folder is - # resolved from pipeline state, not a filesystem existence check - # (resolves KI-10). - source_folder = context.normalized_folder_path or Path( - context.request.raw_video_folder_path - ) - - before_series = { - video.camera_name: compute_brightness_series(video) - for video in context.pre_trim_videos - } - after_series = { - video.camera_name: compute_brightness_series(video) - for video in context.videos - } - for camera_name, series in before_series.items(): - save_brightness_series( - series, - source_folder - / f"{camera_name}{BRIGHTNESS_SUFFIX}.{NUMPY_EXTENSION}", + trimmed_audio_paths = sorted( + (context.audio_folder_path / TRIMMED_AUDIO_FOLDER_NAME).glob( + f"*.{AudioExtension.WAV.value}" + ) + ) + plot_audio_waveforms(raw_audio_paths, trimmed_audio_paths, plot_path) + else: + # Whether debug data comes from the raw or normalized folder is + # resolved from pipeline state, not a filesystem existence check + # (resolves KI-10). + source_folder = context.normalized_folder_path or Path( + context.request.raw_video_folder_path ) - before_fps = {v.camera_name: v.fps for v in context.pre_trim_videos} - after_fps = {v.camera_name: v.fps for v in context.videos} - plot_brightness_series( - before_series, before_fps, after_series, after_fps, plot_path + before_series = { + video.camera_name: compute_brightness_series(video) + for video in context.pre_trim_videos + } + after_series = { + video.camera_name: compute_brightness_series(video) + for video in context.videos + } + for camera_name, series in before_series.items(): + save_brightness_series( + series, + source_folder + / f"{camera_name}{BRIGHTNESS_SUFFIX}.{NUMPY_EXTENSION}", + ) + + before_fps = {v.camera_name: v.fps for v in context.pre_trim_videos} + after_fps = {v.camera_name: v.fps for v in context.videos} + plot_brightness_series( + before_series, before_fps, after_series, after_fps, plot_path + ) + except Exception: + # Plotting is best-effort debug output, not core functionality -- + # matplotlib can fail when invoked from a thread/process other + # than a GUI app's main thread (e.g. called from a Qt app or a + # worker process). Don't let that discard an otherwise-successful + # synchronization result. + logger.warning( + "Failed to generate debug plot at %s; continuing without it.", + plot_path, + exc_info=True, ) - artifact_paths.append(plot_path) + else: + artifact_paths.append(plot_path) context.debug_artifact_paths = artifact_paths return context diff --git a/skelly_synchronize/tests/cli/test_main.py b/skelly_synchronize/tests/cli/test_main.py new file mode 100644 index 0000000..3d7060d --- /dev/null +++ b/skelly_synchronize/tests/cli/test_main.py @@ -0,0 +1,98 @@ +from pathlib import Path + +import pytest + +from skelly_synchronize.cli import main as main_module +from skelly_synchronize.cli.main import build_parser, main +from skelly_synchronize.core.models import SyncMethod, SyncResult, VideoBackendKind + + +def test_parser_requires_raw_video_folder_path(): + parser = build_parser() + with pytest.raises(SystemExit): + parser.parse_args(["--method", "audio"]) + + +def test_parser_defaults(): + parser = build_parser() + args = parser.parse_args(["some_folder"]) + + assert args.raw_video_folder_path == Path("some_folder") + assert args.synchronized_video_folder_path is None + assert args.method == "audio" + assert args.video_handler == VideoBackendKind.DEFFCODE.value + assert args.brightness_ratio_threshold == 1000.0 + assert args.create_debug_artifacts is True + assert args.verbose is False + + +def test_parser_overrides(): + parser = build_parser() + args = parser.parse_args( + [ + "some_folder", + "--method", + "audio", + "--output", + "out_folder", + "--video-handler", + "ffmpeg", + "--brightness-threshold", + "500", + "--no-debug-artifacts", + "--verbose", + ] + ) + + assert args.synchronized_video_folder_path == Path("out_folder") + assert args.method == "audio" + assert args.video_handler == "ffmpeg" + assert args.brightness_ratio_threshold == 500.0 + assert args.create_debug_artifacts is False + assert args.verbose is True + + +def test_main_rejects_missing_raw_video_folder(tmp_path, capsys): + missing_folder = tmp_path / "does_not_exist" + + exit_code = main([str(missing_folder), "--method", "audio"]) + + assert exit_code == 1 + assert "does not exist" in capsys.readouterr().err + + +def test_main_happy_path_builds_request_and_prints_summary( + tmp_path, capsys, monkeypatch +): + raw_folder = tmp_path / "raw_videos" + raw_folder.mkdir() + synced_folder = tmp_path / "synchronized_videos" + + captured_request = {} + + def fake_run_pipeline(request, progress_callback=None): + captured_request["request"] = request + if progress_callback is not None: + progress_callback("cam_1", 1.0) + return SyncResult( + synchronized_video_folder_path=synced_folder, + videos_before=[], + videos_after=[], + lags=[], + debug_artifact_paths=[], + elapsed_seconds=1.5, + ) + + monkeypatch.setattr(main_module, "run_pipeline", fake_run_pipeline) + + exit_code = main([str(raw_folder), "--method", "brightness"]) + + assert exit_code == 0 + request = captured_request["request"] + assert request.raw_video_folder_path == raw_folder + assert request.method == SyncMethod.BRIGHTNESS + assert request.video_handler == VideoBackendKind.DEFFCODE + + out = capsys.readouterr().out + assert str(synced_folder) in out + assert "cam_1: trimmed" in out diff --git a/skelly_synchronize/tests/core/test_models.py b/skelly_synchronize/tests/core/test_models.py index cc6ae95..52e9d4e 100644 --- a/skelly_synchronize/tests/core/test_models.py +++ b/skelly_synchronize/tests/core/test_models.py @@ -6,6 +6,7 @@ from skelly_synchronize.core.models import ( LagResult, SyncMethod, + SyncResult, VideoBackendKind, VideoInfo, ) @@ -37,3 +38,15 @@ def test_sync_method_values(): def test_video_backend_kind_values(): assert VideoBackendKind.FFMPEG == "ffmpeg" assert VideoBackendKind.DEFFCODE == "deffcode" + + +def test_sync_result_synchronized_frame_count_defaults_to_none(): + result = SyncResult( + synchronized_video_folder_path=Path("synchronized_videos"), + videos_before=[], + videos_after=[], + lags=[], + debug_artifact_paths=[], + elapsed_seconds=1.0, + ) + assert result.synchronized_frame_count is None From bbabf5b6cb7e31fa27321db8d370de6be33412d5 Mon Sep 17 00:00:00 2001 From: philipqueen Date: Mon, 10 Aug 2026 11:47:35 -0600 Subject: [PATCH 05/16] integration/e2e tests --- pyproject.toml | 15 +++-- skelly_synchronize/tests/fixtures/README.md | 45 +++++++++++++ .../tests/fixtures/audio_sync/cam_a.mp4 | Bin 0 -> 32263 bytes .../tests/fixtures/audio_sync/cam_b.mp4 | Bin 0 -> 32331 bytes .../tests/fixtures/brightness_sync/cam_a.mp4 | Bin 0 -> 2679 bytes .../tests/fixtures/brightness_sync/cam_b.mp4 | Bin 0 -> 2678 bytes .../tests/integration/conftest.py | 25 +++++++ .../tests/integration/slow/conftest.py | 48 ++++++++++++++ .../integration/slow/test_sample_dataset.py | 38 +++++++++++ .../integration/test_audio_sync_pipeline.py | 61 ++++++++++++++++++ .../test_brightness_sync_pipeline.py | 51 +++++++++++++++ 11 files changed, 279 insertions(+), 4 deletions(-) create mode 100644 skelly_synchronize/tests/fixtures/README.md create mode 100644 skelly_synchronize/tests/fixtures/audio_sync/cam_a.mp4 create mode 100644 skelly_synchronize/tests/fixtures/audio_sync/cam_b.mp4 create mode 100644 skelly_synchronize/tests/fixtures/brightness_sync/cam_a.mp4 create mode 100644 skelly_synchronize/tests/fixtures/brightness_sync/cam_b.mp4 create mode 100644 skelly_synchronize/tests/integration/conftest.py create mode 100644 skelly_synchronize/tests/integration/slow/conftest.py create mode 100644 skelly_synchronize/tests/integration/slow/test_sample_dataset.py create mode 100644 skelly_synchronize/tests/integration/test_audio_sync_pipeline.py create mode 100644 skelly_synchronize/tests/integration/test_brightness_sync_pipeline.py diff --git a/pyproject.toml b/pyproject.toml index aabee0c..11c6afb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,16 +41,23 @@ dynamic = ["version"] [project.optional-dependencies] dev = [ "pytest", + "requests", "black", - "bumpver", - "isort", - "pip-tools", - "pytest", + "bumpver", + "isort", + "pip-tools", + "pytest", "flake8", "flake8-bandit", "flake8-bugbear", ] +[tool.pytest.ini_options] +addopts = "-m 'not slow'" +markers = [ + "slow: opt-in integration tests against the full Figshare sample dataset (real network download + real ffmpeg/deffcode); run explicitly with `pytest -m slow`.", +] + [project.urls] Homepage = "https://freemocap.org" Documentation = "https://freemocap.github.io/skelly_synchronize/" diff --git a/skelly_synchronize/tests/fixtures/README.md b/skelly_synchronize/tests/fixtures/README.md new file mode 100644 index 0000000..21cf465 --- /dev/null +++ b/skelly_synchronize/tests/fixtures/README.md @@ -0,0 +1,45 @@ +# Integration test fixtures + +Tiny, synthetic, checked-in video pairs used by `tests/integration/`. Each pair +simulates two cameras recording the same real-world sync event (an audio tone +or a brightness flash) starting at different wall-clock times -- exactly the +scenario the sync algorithms are meant to solve -- without needing the full +Figshare sample dataset. Real ffmpeg/deffcode subprocesses run against these +files, so they exercise real codec/container behavior, unlike the mocked unit +tests elsewhere in `core`. + +Both `cam_a`/`cam_b` pairs are 4 seconds, 64x64, 10fps. `cam_a`'s sync event +fires 1 second later in its own timeline than `cam_b`'s -- equivalent to +`cam_a` having started recording 1 second earlier than `cam_b` relative to the +same real event. The expected post-sync lags are therefore `cam_a: 0.0s`, +`cam_b: 1.0s` (brightness) and `cam_a: 1.0s`, `cam_b: 0.0s` (audio -- lag +direction is inverted from brightness because of how each raw lag is +normalized, see `LagResult`/KI-02). + +## Regenerating + +```bash +# audio_sync/ -- 880Hz, 1s tone. cam_a: tone at t=2s. cam_b: tone at t=1s. +ffmpeg -y -f lavfi -i "testsrc2=size=64x64:rate=10:duration=4" \ + -f lavfi -i "sine=frequency=880:duration=1" \ + -filter_complex "[1:a]adelay=2000,apad=whole_dur=4[a]" \ + -map 0:v -map "[a]" -c:v libx264 -pix_fmt yuv420p -c:a aac -shortest \ + audio_sync/cam_a.mp4 + +ffmpeg -y -f lavfi -i "testsrc2=size=64x64:rate=10:duration=4" \ + -f lavfi -i "sine=frequency=880:duration=1" \ + -filter_complex "[1:a]adelay=1000,apad=whole_dur=4[a]" \ + -map 0:v -map "[a]" -c:v libx264 -pix_fmt yuv420p -c:a aac -shortest \ + audio_sync/cam_b.mp4 + +# brightness_sync/ -- 0.3s white flash. cam_a: flash at t=2s. cam_b: flash at t=1s. +ffmpeg -y -f lavfi -i "color=c=black:s=64x64:r=10:d=4" \ + -vf "drawbox=x=0:y=0:w=64:h=64:color=white:t=fill:enable='between(t,2,2.3)'" \ + -pix_fmt yuv420p -c:v libx264 \ + brightness_sync/cam_a.mp4 + +ffmpeg -y -f lavfi -i "color=c=black:s=64x64:r=10:d=4" \ + -vf "drawbox=x=0:y=0:w=64:h=64:color=white:t=fill:enable='between(t,1,1.3)'" \ + -pix_fmt yuv420p -c:v libx264 \ + brightness_sync/cam_b.mp4 +``` diff --git a/skelly_synchronize/tests/fixtures/audio_sync/cam_a.mp4 b/skelly_synchronize/tests/fixtures/audio_sync/cam_a.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..08ce584de9e6f822c7c61d2d592f34c293b403cd GIT binary patch literal 32263 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 T6$m|4^C@tsCMWR6!S%lYQrdAh literal 0 HcmV?d00001 diff --git a/skelly_synchronize/tests/fixtures/brightness_sync/cam_a.mp4 b/skelly_synchronize/tests/fixtures/brightness_sync/cam_a.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..78a5d828c135bf151b297908636bef906b1ce56b GIT binary patch literal 2679 zcmc&$U2GIp6rQ$Jn})weFg3w&`va2L-OkLmrBo*ATB;?1h@xPO$>#3N-R{tto$1W9 z-6fi;@uxm%VkEIjmj?wwKp!v~BB3TE26;6>5+WfQ`HSKkywLSKv%Boj@WP9p?45J& zIp;fncYaoqB$f#ZuIW3rB-KmAu~a^(E4JICNRrf)@r011#!a@t1Na<&sIgQ!a&h?D zsXs4_e7CRbS9YP}@0)qGuZP8$?s$SJeZ9;}sH)1+iQYcBm%}g;heP-F9mCJYdRTJT z5IhZ`!!YEy1xsWCrpj_6rpjtBl-VF~2fDkbrl#VPrXd`Q=i-hx+D%#F*}%5oQC72=7mZOhp%78o|GoIsC!55mGPYlR;MM*GQXjzeG{zMwmM&|kj zKygiZhNi@2fN){9nHNTiA}T8+rgiAGb9b zC2(V}lM|Y%bSVnUa6d@7{+Q{KM>)W_n{qN4Uj$lAWkJ>h!q-%0InEf*Vw0-s6y{r| zj#{b;ndQ7Nhi=+DpgPQ4AUuo13Hr1(;qgLBcWjpj*y|Wk;BhmD5ODN3`D8q93m?Nu zr`!T$(?ITYiW}UeF4C#A$$d&YBAPt-2id<^cabYbhT|mb~0kJi;SFVUD z+stW-%zRzsggy~yJu;Q)aT=*7{4C&}p1RvE**#s;y$rL{m@Bn~jix5zG8?C6G&$Y} z(WSs4=XtGf6BK?RT&;(hE)EGd1kMn#_&Cm=PE&dVcW|4Sd0T2JE!>(}tDbW{+jHJ) zxpMTyJI{1HX8Tt%CtD8{;MAO)?X;3>7ruS+Cf)SOxu^5*O*ft*xo(aJL(>nxR+jZW zkY9(qGC7;w`eicv$+o7BQU%wXoO^e;rR%9u75N1r$s17)5t5<+c@#(s$hOkLk=?&R z2jtJgt0Ty3k(@Uo$bTxx>C(dC{k-8JN=i3-vwEgbq_ri^?Z$c?2+ zg+;IGjgh@2Ejhh#>)53?x6cp$HFk1$|A~)}{n7AI?3*pW?3#<;3gNTu*3dM9y^kFl zOcs`v!Uo%uGat_H#KeDkZvNo)i|yz34{L30$LC8g*v*}1R-C;w@*@x{lCz^*FRxg? zv%V=ZzjuaPHe-H2ltX@A>t*g>wK~l2tIFiV`CST&nLB&WoDC7jN;KqMOpEf|H}4=ov?Qrh7=aJIMp!;R z3?U%`#cit8-0;0^m>d>n8;g720QCXsr86NemNhIdlyIR}epM#Ei#`D(JZD&fd>VAT z1Tqj-P1<45XxW`c*|x^On})z3vM-(RELN5qI{YAQK|bvVe)T}e=WF`vffGezmNkQD zdjujaqPo@F;NPdl)&965GM=JhS%0o3A`6xLv@DKT)Hf^j)EV(ux0)H7W5%4 zq05fVBl}*ilOD?6`2Ae_{GHpk3A@tez6(xe&{~DJVr9n7Y3zww{UDq_$>f89B4DWZ z>0F3#FNC($U?Y0X|B14`iF|-vRpHe=ZAl--`w#3ZDIaRB=Bw7RbWt5u{r_6;8r(KI z6$YZY)7ntsCt%wPxk}L2pT}l77By2{)Ie1CAp3O@?0U$LMD`T_L1KmeexN(R5m7HP zR(iq+IQT2J2suQlE&7b&x%Rm0T4bnF%q+hM)*TKU3ivqwFj5c-D&b+t;9*hllhuC# DE(|vt literal 0 HcmV?d00001 diff --git a/skelly_synchronize/tests/fixtures/brightness_sync/cam_b.mp4 b/skelly_synchronize/tests/fixtures/brightness_sync/cam_b.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..9e42ce6777f1f394b971094ab45046941743f1a8 GIT binary patch literal 2678 zcmcguU2GIp6uzZ^wGb#tNlJitODiFfZfB-H*6Ji(OSL2r2qG9Y+1#DE+Z{TyGo6{X zyF`N;gY}6RA`y_57eNRh52y)|P!kL>_^3e=A|V=4jPeLEq3d^MciEw7!UKsXd*_^c z&iT&Iy>ss@V~l6Qf@cM;!`M7V977XhhU$2oDr2lJ<4eidf~Yygu;f zh2O6Yetls5&-_};KX>w4cPDS-hU-hNc6V{VL(?>#?&#`Px&#b^2{^QG-a7DdTPIKM z=!d5%4H)`euVBke$TdajXwwv}3(9O5dcE!KuRskt8kGMb|H|ulJDqM@~Yaw9jRw!o(H38NY8RF zC;*CQ$~Sd2p#X#jcdWcLQxs7JBIS$Rkkr*~Ze)Ge5h*}b9{SR@tpHL_zQ;5|2*!w` zD=0yjyWO1BHFdqJ@{9<=lot$J9(j}kjCd(GlL=&~w`n}g`alG_#%^bXDPjA#>6g4fRfi%Jc;-sV{>p;J%T1&@S11LpS^kchXo_Y6}xx>qsbkgqqQn zL^ni_0!Lir_3r1O2tw)Uo!s)UNq8Z!hlnLcu>Z81>>a$p&2r*>wyZdFcj77Siu>uV ztNyZEr?%gJsbz%|+{&D9I$nTNeezJNom@I|a_2Af(kCZh%zvw_aNM{f)tIOGQj+8b-e= zJv4!O*1WIkIhQn#vH3kXa>Fpb84gD|Yh#Y(XvjO8X62bPZy!K37;BYg7=W*mc7X3g zM2J9f>&i9Po^?!1z``AKcJJ$;KFVIb6ye&krtL=(9(40pWa2yMOE4&MrY*^*_6^(x z83>JIP82j=cB@&ktuk@p5Ew-E#iPE>OLFbXAWYkkF9u;y*%0#ifq43 zTVEJF@LCO9nEmV5D{H3j-@8ZH22TVYIF&)85tn0mjq6EI)D(o#`jbpPSWpCv^a1S) z5#EK!wg{}xsQRxh>FdY`*v2xi%xOycDBh#&$Wg&H5DxPSuA+ae_>ao}KiH!_R^fFq zs6h~NMq^!>pMz~T8Oj9xysMCxU@VReT8Oxg=1 Callable[[str], Path]: + """Copy a checked-in fixture set (e.g. "audio_sync") into an isolated raw_videos folder. + + Copying into `tmp_path` keeps each test's pipeline run (and any + normalized/audio/debug output it writes alongside the input) isolated + from the checked-in fixtures and from other tests. + """ + + def _make(fixture_set_name: str) -> Path: + source_dir = FIXTURES_DIR / fixture_set_name + raw_video_folder_path = tmp_path / "raw_videos" + shutil.copytree(source_dir, raw_video_folder_path) + return raw_video_folder_path + + return _make diff --git a/skelly_synchronize/tests/integration/slow/conftest.py b/skelly_synchronize/tests/integration/slow/conftest.py new file mode 100644 index 0000000..b615152 --- /dev/null +++ b/skelly_synchronize/tests/integration/slow/conftest.py @@ -0,0 +1,48 @@ +import io +import zipfile +from pathlib import Path + +import pytest +import requests + +from skelly_synchronize.core.config import RAW_VIDEOS_FOLDER_NAME + +# Real multi-camera capture session used for full end-to-end confidence, +# hosted as a GitHub release asset. Only used by the @pytest.mark.slow tier -- +# not needed for the fast fixture-based integration tests in tests/integration/. +SAMPLE_DATA_ZIP_URL = "https://github.com/freemocap/skellysamples/releases/download/synch_test_data/audio_synchronization_test_data.zip" +SAMPLE_DATA_FILE_NAME = "audio_synchronization_test_data" + +CACHE_DIR = Path.home() / ".cache" / "skelly_synchronize" + + +def _download_and_extract_sample_data() -> Path: + CACHE_DIR.mkdir(parents=True, exist_ok=True) + sample_data_path = CACHE_DIR / SAMPLE_DATA_FILE_NAME + + if not sample_data_path.exists(): + response = requests.get(SAMPLE_DATA_ZIP_URL, timeout=(10, 60)) + response.raise_for_status() + with zipfile.ZipFile(io.BytesIO(response.content)) as zip_file: + zip_file.extractall(sample_data_path) + + return sample_data_path + + +@pytest.fixture(scope="session") +def sample_dataset_raw_video_folder_path() -> Path: + """Session-scoped, opt-in only (used exclusively by @pytest.mark.slow tests). + + Downloads once and caches under ~/.cache/skelly_synchronize -- subsequent + runs (local or CI) reuse the cached extraction instead of re-downloading. + """ + sample_session_folder_path = _download_and_extract_sample_data() + + for subfolder_path in sample_session_folder_path.iterdir(): + if subfolder_path.name == RAW_VIDEOS_FOLDER_NAME: + return subfolder_path + + raise FileNotFoundError( + f"Could not find a '{RAW_VIDEOS_FOLDER_NAME}' folder in " + f"{sample_session_folder_path}" + ) diff --git a/skelly_synchronize/tests/integration/slow/test_sample_dataset.py b/skelly_synchronize/tests/integration/slow/test_sample_dataset.py new file mode 100644 index 0000000..a49d045 --- /dev/null +++ b/skelly_synchronize/tests/integration/slow/test_sample_dataset.py @@ -0,0 +1,38 @@ +from pathlib import Path + +import pytest + +from skelly_synchronize.core.config import DEBUG_PLOT_NAME, DEBUG_TOML_NAME +from skelly_synchronize.core.discovery import get_video_file_list +from skelly_synchronize.core.models import SyncMethod, SyncRequest +from skelly_synchronize.core.pipeline.runner import run_pipeline + +pytestmark = pytest.mark.slow + + +def test_audio_sync_pipeline_against_sample_dataset( + sample_dataset_raw_video_folder_path: Path, tmp_path: Path +): + request = SyncRequest( + raw_video_folder_path=sample_dataset_raw_video_folder_path, + synchronized_video_folder_path=tmp_path / "synchronized_videos", + method=SyncMethod.AUDIO, + ) + + result = run_pipeline(request) + + assert result.synchronized_video_folder_path.exists() + + raw_video_paths = get_video_file_list(sample_dataset_raw_video_folder_path) + synced_video_paths = get_video_file_list(result.synchronized_video_folder_path) + assert len(raw_video_paths) == len(synced_video_paths) + + assert result.synchronized_frame_count is not None + assert result.synchronized_frame_count > 0 + assert all( + video.frame_count == result.synchronized_frame_count + for video in result.videos_after + ) + + assert (result.synchronized_video_folder_path / DEBUG_TOML_NAME).exists() + assert (result.synchronized_video_folder_path / DEBUG_PLOT_NAME).exists() diff --git a/skelly_synchronize/tests/integration/test_audio_sync_pipeline.py b/skelly_synchronize/tests/integration/test_audio_sync_pipeline.py new file mode 100644 index 0000000..3d72f66 --- /dev/null +++ b/skelly_synchronize/tests/integration/test_audio_sync_pipeline.py @@ -0,0 +1,61 @@ +from pathlib import Path +from typing import Callable + +import pytest + +from skelly_synchronize.core.config import ( + AUDIO_FILES_FOLDER_NAME, + DEBUG_PLOT_NAME, + DEBUG_TOML_NAME, + TRIMMED_AUDIO_FOLDER_NAME, +) +from skelly_synchronize.core.discovery import get_video_file_list +from skelly_synchronize.core.models import SyncMethod, SyncRequest, VideoBackendKind +from skelly_synchronize.core.pipeline.runner import run_pipeline + +# cam_a's audio marker sits 1s later, within its own timeline, than cam_b's -- +# equivalent to cam_a having started recording 1s earlier than cam_b relative +# to the same real-world event. See skelly_synchronize/tests/fixtures/README.md. +EXPECTED_LAG_SECONDS = {"cam_a": 1.0, "cam_b": 0.0} +LAG_TOLERANCE_SECONDS = 0.05 + + +@pytest.mark.parametrize( + "video_handler", [VideoBackendKind.FFMPEG, VideoBackendKind.DEFFCODE] +) +def test_audio_sync_pipeline_end_to_end( + raw_video_folder_factory: Callable[[str], Path], video_handler: VideoBackendKind +): + raw_video_folder_path = raw_video_folder_factory("audio_sync") + + request = SyncRequest( + raw_video_folder_path=raw_video_folder_path, + method=SyncMethod.AUDIO, + video_handler=video_handler, + ) + + result = run_pipeline(request) + + assert result.synchronized_video_folder_path.exists() + + lags_by_camera = {lag.camera_name: lag.lag_seconds for lag in result.lags} + for camera_name, expected_lag in EXPECTED_LAG_SECONDS.items(): + assert lags_by_camera[camera_name] == pytest.approx( + expected_lag, abs=LAG_TOLERANCE_SECONDS + ) + + synced_video_paths = get_video_file_list(result.synchronized_video_folder_path) + assert len(synced_video_paths) == len(EXPECTED_LAG_SECONDS) + + assert result.synchronized_frame_count is not None + assert result.synchronized_frame_count > 0 + + debug_toml_path = result.synchronized_video_folder_path / DEBUG_TOML_NAME + debug_plot_path = result.synchronized_video_folder_path / DEBUG_PLOT_NAME + assert debug_toml_path.exists() + assert debug_plot_path.exists() + + audio_files_folder = result.synchronized_video_folder_path / AUDIO_FILES_FOLDER_NAME + trimmed_audio_folder = audio_files_folder / TRIMMED_AUDIO_FOLDER_NAME + assert audio_files_folder.exists() + assert trimmed_audio_folder.exists() diff --git a/skelly_synchronize/tests/integration/test_brightness_sync_pipeline.py b/skelly_synchronize/tests/integration/test_brightness_sync_pipeline.py new file mode 100644 index 0000000..d278ba8 --- /dev/null +++ b/skelly_synchronize/tests/integration/test_brightness_sync_pipeline.py @@ -0,0 +1,51 @@ +from pathlib import Path +from typing import Callable + +import pytest + +from skelly_synchronize.core.config import DEBUG_PLOT_NAME, DEBUG_TOML_NAME +from skelly_synchronize.core.discovery import get_video_file_list +from skelly_synchronize.core.models import SyncMethod, SyncRequest, VideoBackendKind +from skelly_synchronize.core.pipeline.runner import run_pipeline + +# cam_a's flash fires 1s later, within its own timeline, than cam_b's -- +# equivalent to cam_a having started recording 1s earlier than cam_b relative +# to the same real-world flash event. See skelly_synchronize/tests/fixtures/README.md. +EXPECTED_LAG_SECONDS = {"cam_a": 0.0, "cam_b": 1.0} +LAG_TOLERANCE_SECONDS = 0.05 + + +@pytest.mark.parametrize( + "video_handler", [VideoBackendKind.FFMPEG, VideoBackendKind.DEFFCODE] +) +def test_brightness_sync_pipeline_end_to_end( + raw_video_folder_factory: Callable[[str], Path], video_handler: VideoBackendKind +): + raw_video_folder_path = raw_video_folder_factory("brightness_sync") + + request = SyncRequest( + raw_video_folder_path=raw_video_folder_path, + method=SyncMethod.BRIGHTNESS, + video_handler=video_handler, + ) + + result = run_pipeline(request) + + assert result.synchronized_video_folder_path.exists() + + lags_by_camera = {lag.camera_name: lag.lag_seconds for lag in result.lags} + for camera_name, expected_lag in EXPECTED_LAG_SECONDS.items(): + assert lags_by_camera[camera_name] == pytest.approx( + expected_lag, abs=LAG_TOLERANCE_SECONDS + ) + + synced_video_paths = get_video_file_list(result.synchronized_video_folder_path) + assert len(synced_video_paths) == len(EXPECTED_LAG_SECONDS) + + assert result.synchronized_frame_count is not None + assert result.synchronized_frame_count > 0 + + debug_toml_path = result.synchronized_video_folder_path / DEBUG_TOML_NAME + debug_plot_path = result.synchronized_video_folder_path / DEBUG_PLOT_NAME + assert debug_toml_path.exists() + assert debug_plot_path.exists() From 086e12685ce388a362f64a183f1e119f848ed495 Mon Sep 17 00:00:00 2001 From: philipqueen Date: Mon, 10 Aug 2026 12:18:24 -0600 Subject: [PATCH 06/16] API time --- docs/architecture/02-core-library.md | 18 +- docs/architecture/03-api-design.md | 2 +- docs/architecture/04-frontend.md | 4 +- pyproject.toml | 6 + skelly_synchronize/api/__init__.py | 0 skelly_synchronize/api/jobs.py | 185 ++++++++++++++++++ skelly_synchronize/api/main.py | 52 +++++ skelly_synchronize/api/routers/__init__.py | 0 skelly_synchronize/api/routers/health.py | 8 + skelly_synchronize/api/routers/jobs.py | 64 ++++++ skelly_synchronize/api/routers/videos.py | 20 ++ skelly_synchronize/api/schemas.py | 28 +++ skelly_synchronize/cli/main.py | 6 +- skelly_synchronize/core/audio.py | 46 ++--- skelly_synchronize/core/backends/ffmpeg.py | 2 +- skelly_synchronize/core/debug.py | 20 +- skelly_synchronize/core/models.py | 10 +- skelly_synchronize/core/pipeline/stages.py | 92 +++++---- skelly_synchronize/tests/api/test_health.py | 11 ++ skelly_synchronize/tests/api/test_jobs.py | 163 +++++++++++++++ skelly_synchronize/tests/api/test_videos.py | 27 +++ .../tests/core/backends/test_ffmpeg.py | 2 +- .../tests/core/pipeline/test_stages.py | 46 ++--- skelly_synchronize/tests/core/test_audio.py | 24 +-- skelly_synchronize/tests/core/test_debug.py | 8 +- skelly_synchronize/tests/core/test_models.py | 6 +- .../integration/test_audio_sync_pipeline.py | 6 +- .../test_brightness_sync_pipeline.py | 6 +- 28 files changed, 712 insertions(+), 150 deletions(-) create mode 100644 skelly_synchronize/api/__init__.py create mode 100644 skelly_synchronize/api/jobs.py create mode 100644 skelly_synchronize/api/main.py create mode 100644 skelly_synchronize/api/routers/__init__.py create mode 100644 skelly_synchronize/api/routers/health.py create mode 100644 skelly_synchronize/api/routers/jobs.py create mode 100644 skelly_synchronize/api/routers/videos.py create mode 100644 skelly_synchronize/api/schemas.py create mode 100644 skelly_synchronize/tests/api/test_health.py create mode 100644 skelly_synchronize/tests/api/test_jobs.py create mode 100644 skelly_synchronize/tests/api/test_videos.py diff --git a/docs/architecture/02-core-library.md b/docs/architecture/02-core-library.md index 7eb35c1..729dbee 100644 --- a/docs/architecture/02-core-library.md +++ b/docs/architecture/02-core-library.md @@ -9,21 +9,21 @@ ## Data models -All models are Pydantic `BaseModel`s, not plain dataclasses. This is a deliberate choice: `api` already requires Pydantic for FastAPI request/response validation, so sharing model definitions between `core` and `api` avoids a duplicate translation layer. The one exception is raw audio signal data (numpy arrays), which is kept **outside** any Pydantic model — large ndarrays don't serialize well and aren't meant to cross the API boundary — and instead passed alongside models as a plain `dict[str, np.ndarray]` keyed by camera name. +All models are Pydantic `BaseModel`s, not plain dataclasses. This is a deliberate choice: `api` already requires Pydantic for FastAPI request/response validation, so sharing model definitions between `core` and `api` avoids a duplicate translation layer. The one exception is raw audio signal data (numpy arrays), which is kept **outside** any Pydantic model — large ndarrays don't serialize well and aren't meant to cross the API boundary — and instead passed alongside models as a plain `dict[str, np.ndarray]` keyed by video name. ```python -CameraName = str # alias for clarity in signatures +VideoName = str # alias for clarity in signatures class VideoInfo(BaseModel): filepath: Path - camera_name: str + video_name: str duration_seconds: float fps: float frame_count: int | None = None class AudioInfo(BaseModel): filepath: Path - camera_name: str + video_name: str sample_rate: int duration_seconds: float # raw signal (np.ndarray) is intentionally NOT a field here @@ -33,7 +33,7 @@ class SyncMethod(str, Enum): BRIGHTNESS = "brightness" class LagResult(BaseModel): - camera_name: str + video_name: str lag_seconds: float confidence: float | None = None # resolves KI-14 @@ -56,7 +56,7 @@ class SyncResult(BaseModel): ### `LagResult` contract (resolves KI-02) -Every lag-producing algorithm (audio cross-correlation, brightness-change detection) must return `LagResult` values that share **one contract**: `lag_seconds` is the number of seconds to trim off the front of that specific video so that all videos align, normalized so the minimum lag across all cameras is `0`. The current codebase normalizes this way for the audio path only and returns raw un-normalized values for the brightness path — this only "works" today because downstream trimming code happens to treat both the same way. In the rewrite, normalization happens once, centrally, right before `ComputeLagsStage` returns — not duplicated per-algorithm, and not left as an implicit assumption. +Every lag-producing algorithm (audio cross-correlation, brightness-change detection) must return `LagResult` values that share **one contract**: `lag_seconds` is the number of seconds to trim off the front of that specific video so that all videos align, normalized so the minimum lag across all videos is `0`. The current codebase normalizes this way for the audio path only and returns raw un-normalized values for the brightness path — this only "works" today because downstream trimming code happens to treat both the same way. In the rewrite, normalization happens once, centrally, right before `ComputeLagsStage` returns — not duplicated per-algorithm, and not left as an implicit assumption. ## `VideoBackend` interface (resolves KI-03) @@ -111,8 +111,8 @@ Shared stage list, used by **both** sync methods (this is what eliminates the cu Replace `multiprocessing.Pool.starmap` with `concurrent.futures.ProcessPoolExecutor` + `as_completed`. This gives two things the current implementation lacks: -- **Per-task error isolation**: today, if one worker's `trim_single_video` raises, the whole `starmap` call surfaces a single aggregate failure with no partial-result visibility. `as_completed` lets the pipeline report exactly which camera failed and why, while still letting sibling trims finish. -- **A natural hook for progress reporting**: each completed future can immediately report per-camera progress instead of the pipeline blocking silently until every video is done. +- **Per-task error isolation**: today, if one worker's `trim_single_video` raises, the whole `starmap` call surfaces a single aggregate failure with no partial-result visibility. `as_completed` lets the pipeline report exactly which video failed and why, while still letting sibling trims finish. +- **A natural hook for progress reporting**: each completed future can immediately report per-video progress instead of the pipeline blocking silently until every video is done. Worker function signature takes only picklable arguments: `(video_info: VideoInfo, lag: LagResult, backend_kind: VideoBackendKind, output_dir: Path)`. @@ -126,7 +126,7 @@ The deffcode trim path's current `frame_number in frame_list` check (O(n) list s ## Audio subsystem -- **Reference-camera selection (resolves KI-13)**: today, `find_cross_correlation_lags` picks `next(iter(audio_signal_dict))`, an implicit dependency on alphabetical file-discovery order. The rewrite makes this an explicit, documented strategy — recommend "camera with the longest audio duration" as a tie-break-free deterministic choice (falls back sensibly even if all durations happen to match, since ties then resolve to sorted-camera-name order, which is still deterministic and documented). +- **Reference-video selection (resolves KI-13)**: today, `find_cross_correlation_lags` picks `next(iter(audio_signal_dict))`, an implicit dependency on alphabetical file-discovery order. The rewrite makes this an explicit, documented strategy — recommend "video with the longest audio duration" as a tie-break-free deterministic choice (falls back sensibly even if all durations happen to match, since ties then resolve to sorted-video-name order, which is still deterministic and documented). - **In-memory reuse (resolves KI-12)**: `trim_audio_files` reuses the signal already loaded during extraction instead of reloading each `.wav` from disk. - **Confidence score (resolves KI-14)**: `cross_correlate` additionally returns a confidence metric (e.g. ratio of the peak correlation value to the surrounding noise floor, or peak sharpness) that populates `LagResult.confidence`. - **In-memory-only loading (KI-11)**: accepted as a documented v1 limitation given the local single-user, minutes-not-hours use case. Not addressed by streaming in this pass. diff --git a/docs/architecture/03-api-design.md b/docs/architecture/03-api-design.md index 527ff7a..bd0db54 100644 --- a/docs/architecture/03-api-design.md +++ b/docs/architecture/03-api-design.md @@ -38,7 +38,7 @@ For each job, `api` creates a `multiprocessing.Manager().dict()` and passes a ca | Method & path | Purpose | |---|---| | `GET /health` | Liveness check, `{"status": "ok"}`. | -| `GET /cameras?folder_path=...` | Validates a folder path and returns the discovered video files/camera names, using `core`'s discovery stage standalone — lets the frontend show a preview before a job is started. | +| `GET /videos?folder_path=...` | Validates a folder path and returns the discovered video files/video names, using `core`'s discovery stage standalone — lets the frontend show a preview before a job is started. | | `POST /jobs` | Body: `SyncRequest`. Starts a job in a new process, returns `201 {job_id, status: "pending"}` immediately. | | `GET /jobs/{job_id}` | Returns the full `Job` — status, progress, progress_message, `result` if succeeded, `error` if failed. | | `GET /jobs` | Lists recent jobs (in-memory, capped at e.g. the last 20) — powers a simple job-history panel. | diff --git a/docs/architecture/04-frontend.md b/docs/architecture/04-frontend.md index f28ac6e..68635ee 100644 --- a/docs/architecture/04-frontend.md +++ b/docs/architecture/04-frontend.md @@ -35,7 +35,7 @@ Replaces today's GUI, which only exposes 2 of the several parameters `core` actu ### Result screen -- Per-camera lag summary table (`camera_name`, `lag_seconds`, `confidence`) from `SyncResult.lags`. +- Per-video lag summary table (`video_name`, `lag_seconds`, `confidence`) from `SyncResult.lags`. - Debug plot image, ``, shown only if debug artifacts were requested. - Output folder path, shown as selectable text (a browser page cannot open a native file-manager window — documented as a known limitation rather than attempted). - "Run another sync" button, returns to Setup. @@ -46,7 +46,7 @@ Lists recent jobs from `GET /jobs`; clicking one re-displays its Result screen. ## Error display (resolves part of KI-22) -Today, sync errors are only visible in logs/stdout — the GUI has no error dialog. The new frontend shows inline error banners on the relevant screen, reading either `Job.error` (job-level failures) or the HTTP response's `detail` field (request-level failures, e.g. an invalid folder path from `GET /cameras` or `POST /jobs`). +Today, sync errors are only visible in logs/stdout — the GUI has no error dialog. The new frontend shows inline error banners on the relevant screen, reading either `Job.error` (job-level failures) or the HTTP response's `detail` field (request-level failures, e.g. an invalid folder path from `GET /videos` or `POST /jobs`). ## Styling diff --git a/pyproject.toml b/pyproject.toml index 11c6afb..350c9ac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,9 +39,14 @@ requires-python = ">=3.10,<3.13" dynamic = ["version"] [project.optional-dependencies] +api = [ + "fastapi>=0.110,<1", + "uvicorn[standard]>=0.30,<1", +] dev = [ "pytest", "requests", + "httpx", "black", "bumpver", "isort", @@ -65,6 +70,7 @@ Github = "https://github.com/freemocap/skelly_synchronize" [project.scripts] skelly-synchronize = "skelly_synchronize.cli.main:main" +skelly-sync-api = "skelly_synchronize.api.main:run" [tool.bumpver] #bump the version by entering `bumpver update` in the terminal current_version = "v2025.04.1037" diff --git a/skelly_synchronize/api/__init__.py b/skelly_synchronize/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/skelly_synchronize/api/jobs.py b/skelly_synchronize/api/jobs.py new file mode 100644 index 0000000..1506b73 --- /dev/null +++ b/skelly_synchronize/api/jobs.py @@ -0,0 +1,185 @@ +"""In-memory job store + per-job process orchestration. + +Each job runs in its own OS process so a crashed/hung sync run can't take +down the API process, and `core`'s internal `ProcessPoolExecutor` (used for +parallel trimming) isn't nested inside a thread running an asyncio event +loop. Progress/result/error are bridged back via a `multiprocessing.Manager` +dict written to by the child process and read by the API process. +""" + +import logging +import multiprocessing +import threading +from datetime import datetime, timezone +from enum import Enum +from multiprocessing.managers import DictProxy +from uuid import UUID, uuid4 + +from pydantic import BaseModel + +from skelly_synchronize.core.discovery import get_video_file_list +from skelly_synchronize.core.exceptions import SkellySyncError +from skelly_synchronize.core.models import SyncRequest, SyncResult +from skelly_synchronize.core.pipeline.runner import run_pipeline + +logger = logging.getLogger(__name__) + +_MAX_LISTED_JOBS = 20 + + +class JobStatus(str, Enum): + PENDING = "pending" + RUNNING = "running" + SUCCEEDED = "succeeded" + FAILED = "failed" + CANCELLED = "cancelled" + + +class Job(BaseModel): + id: UUID + status: JobStatus + progress: float = 0.0 + progress_message: str | None = None + created_at: datetime + updated_at: datetime + request: SyncRequest + result: SyncResult | None = None + error: str | None = None + + +def _run_job(request: SyncRequest, shared: DictProxy) -> None: + """Child-process entry point: run the pipeline, write progress/outcome to `shared`. + + Must stay a top-level function (not a closure/method) so it can be + pickled by `multiprocessing`. + """ + shared["status"] = JobStatus.RUNNING.value + + total_videos = len(get_video_file_list(request.raw_video_folder_path)) or 1 + completed_videos: set[str] = set() + + def progress_callback(video_name: str, progress: float) -> None: + if progress >= 1.0: + completed_videos.add(video_name) + shared["progress"] = len(completed_videos) / total_videos + shared["progress_message"] = f"trimmed {video_name}" + + try: + result = run_pipeline(request, progress_callback) + except SkellySyncError as e: + shared["status"] = JobStatus.FAILED.value + shared["error"] = str(e) + return + + shared["status"] = JobStatus.SUCCEEDED.value + shared["progress"] = 1.0 + shared["result"] = result.model_dump(mode="json") + + +class JobStore: + """Guards job bookkeeping with a lock; job execution itself lives in child processes.""" + + def __init__(self) -> None: + self._jobs: dict[UUID, Job] = {} + self._handles: dict[UUID, tuple[multiprocessing.Process, DictProxy]] = {} + self._lock = threading.Lock() + self._manager: multiprocessing.managers.SyncManager | None = None + + def _get_manager(self) -> multiprocessing.managers.SyncManager: + # Created lazily rather than in __init__: `job_store` is a module-level + # singleton, so eagerly starting a Manager (itself a subprocess) as an + # import-time side effect makes every spawn-based child process that + # re-imports this module attempt to start its own Manager recursively. + if self._manager is None: + self._manager = multiprocessing.Manager() + return self._manager + + def create(self, request: SyncRequest) -> Job: + if not request.raw_video_folder_path.is_dir(): + raise FileNotFoundError( + f"raw video folder does not exist: {request.raw_video_folder_path}" + ) + + job_id = uuid4() + now = datetime.now(timezone.utc) + job = Job( + id=job_id, + status=JobStatus.PENDING, + created_at=now, + updated_at=now, + request=request, + ) + shared = self._get_manager().dict( + status=JobStatus.PENDING.value, + progress=0.0, + progress_message=None, + result=None, + error=None, + ) + process = multiprocessing.Process(target=_run_job, args=(request, shared)) + + with self._lock: + self._jobs[job_id] = job + self._handles[job_id] = (process, shared) + + process.start() + return job + + def _refresh(self, job_id: UUID) -> Job | None: + with self._lock: + job = self._jobs.get(job_id) + handle = self._handles.get(job_id) + if job is None or handle is None: + return None + + _, shared = handle + result_data = shared.get("result") + updated = job.model_copy( + update={ + "status": JobStatus(shared.get("status", job.status.value)), + "progress": shared.get("progress", job.progress), + "progress_message": shared.get("progress_message"), + "result": ( + SyncResult.model_validate(result_data) + if result_data is not None + else None + ), + "error": shared.get("error"), + "updated_at": datetime.now(timezone.utc), + } + ) + + with self._lock: + self._jobs[job_id] = updated + return updated + + def get(self, job_id: UUID) -> Job | None: + return self._refresh(job_id) + + def list(self, limit: int = _MAX_LISTED_JOBS) -> list[Job]: + with self._lock: + job_ids = list(self._jobs.keys()) + jobs = [self._refresh(job_id) for job_id in job_ids] + jobs = [job for job in jobs if job is not None] + jobs.sort(key=lambda job: job.created_at, reverse=True) + return jobs[:limit] + + def cancel(self, job_id: UUID) -> Job | None: + with self._lock: + handle = self._handles.get(job_id) + if handle is None: + return None + + process, shared = handle + if process.is_alive(): + process.terminate() + process.join(timeout=5) + shared["status"] = JobStatus.CANCELLED.value + return self._refresh(job_id) + + +job_store = JobStore() + + +def get_job_store() -> JobStore: + return job_store diff --git a/skelly_synchronize/api/main.py b/skelly_synchronize/api/main.py new file mode 100644 index 0000000..ec13286 --- /dev/null +++ b/skelly_synchronize/api/main.py @@ -0,0 +1,52 @@ +import logging + +from fastapi import FastAPI, Request +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import JSONResponse + +from skelly_synchronize.api.routers import health, jobs, videos +from skelly_synchronize.core.exceptions import ( + BackendSubprocessError, + SkellySyncError, + VideoProbeError, +) +from skelly_synchronize.core.logging_setup import configure_logging + +_LOCALHOST_ORIGIN_REGEX = r"^https?://(localhost|127\.0\.0\.1)(:\d+)?$" + +app = FastAPI(title="skelly_synchronize API") + +app.add_middleware( + CORSMiddleware, + allow_origin_regex=_LOCALHOST_ORIGIN_REGEX, + allow_methods=["*"], + allow_headers=["*"], +) + +app.include_router(health.router) +app.include_router(videos.router) +app.include_router(jobs.router) + + +@app.exception_handler(SkellySyncError) +def handle_skelly_sync_error(request: Request, exc: SkellySyncError) -> JSONResponse: + if isinstance(exc, VideoProbeError): + status_code = 422 + elif isinstance(exc, BackendSubprocessError): + return JSONResponse( + status_code=500, content={"detail": str(exc), "stderr": exc.stderr} + ) + else: + status_code = 500 + return JSONResponse(status_code=status_code, content={"detail": str(exc)}) + + +def run() -> None: + import uvicorn + + configure_logging(level=logging.INFO) + uvicorn.run(app, host="127.0.0.1", port=8000) + + +if __name__ == "__main__": + run() diff --git a/skelly_synchronize/api/routers/__init__.py b/skelly_synchronize/api/routers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/skelly_synchronize/api/routers/health.py b/skelly_synchronize/api/routers/health.py new file mode 100644 index 0000000..cccab52 --- /dev/null +++ b/skelly_synchronize/api/routers/health.py @@ -0,0 +1,8 @@ +from fastapi import APIRouter + +router = APIRouter() + + +@router.get("/health") +def health() -> dict[str, str]: + return {"status": "ok"} diff --git a/skelly_synchronize/api/routers/jobs.py b/skelly_synchronize/api/routers/jobs.py new file mode 100644 index 0000000..6a73966 --- /dev/null +++ b/skelly_synchronize/api/routers/jobs.py @@ -0,0 +1,64 @@ +from typing import Annotated +from uuid import UUID + +from fastapi import APIRouter, Depends, HTTPException, status +from fastapi.responses import FileResponse + +from skelly_synchronize.api.jobs import Job, JobStatus, JobStore, get_job_store +from skelly_synchronize.api.schemas import JobCreateResponse +from skelly_synchronize.core.config import DEBUG_PLOT_NAME +from skelly_synchronize.core.models import SyncRequest + +router = APIRouter() + +JobStoreDep = Annotated[JobStore, Depends(get_job_store)] + + +@router.post("/jobs", status_code=status.HTTP_201_CREATED) +def create_job(request: SyncRequest, job_store: JobStoreDep) -> JobCreateResponse: + try: + job = job_store.create(request) + except FileNotFoundError as e: + raise HTTPException(status_code=404, detail=str(e)) + return JobCreateResponse(job_id=job.id, status=job.status) + + +@router.get("/jobs/{job_id}") +def get_job(job_id: UUID, job_store: JobStoreDep) -> Job: + job = job_store.get(job_id) + if job is None: + raise HTTPException(status_code=404, detail=f"job not found: {job_id}") + return job + + +@router.get("/jobs") +def list_jobs(job_store: JobStoreDep) -> list[Job]: + return job_store.list() + + +@router.delete("/jobs/{job_id}") +def cancel_job(job_id: UUID, job_store: JobStoreDep) -> Job: + job = job_store.cancel(job_id) + if job is None: + raise HTTPException(status_code=404, detail=f"job not found: {job_id}") + return job + + +@router.get("/jobs/{job_id}/debug-plot") +def get_debug_plot(job_id: UUID, job_store: JobStoreDep) -> FileResponse: + job = job_store.get(job_id) + if job is None: + raise HTTPException(status_code=404, detail=f"job not found: {job_id}") + if job.status != JobStatus.SUCCEEDED or job.result is None: + raise HTTPException(status_code=404, detail=f"job has no debug plot: {job_id}") + + plot_path = next( + (p for p in job.result.debug_artifact_paths if p.name == DEBUG_PLOT_NAME), + None, + ) + if plot_path is None or not plot_path.is_file(): + raise HTTPException( + status_code=404, detail=f"debug plot not found for job: {job_id}" + ) + + return FileResponse(plot_path) diff --git a/skelly_synchronize/api/routers/videos.py b/skelly_synchronize/api/routers/videos.py new file mode 100644 index 0000000..5be20b1 --- /dev/null +++ b/skelly_synchronize/api/routers/videos.py @@ -0,0 +1,20 @@ +from pathlib import Path + +from fastapi import APIRouter, HTTPException + +from skelly_synchronize.api.schemas import VideoPreview, VideosResponse +from skelly_synchronize.core.discovery import get_video_file_list + +router = APIRouter() + + +@router.get("/videos") +def list_videos(folder_path: Path) -> VideosResponse: + if not folder_path.is_dir(): + raise HTTPException( + status_code=404, detail=f"folder does not exist: {folder_path}" + ) + + video_paths = get_video_file_list(folder_path) + videos = [VideoPreview(video_name=path.stem, filepath=path) for path in video_paths] + return VideosResponse(folder_path=folder_path, videos=videos) diff --git a/skelly_synchronize/api/schemas.py b/skelly_synchronize/api/schemas.py new file mode 100644 index 0000000..aca85f7 --- /dev/null +++ b/skelly_synchronize/api/schemas.py @@ -0,0 +1,28 @@ +"""API-only schema types with no equivalent in `core`. + +`SyncRequest`/`SyncResult`/`Job` are used directly as request/response +schemas elsewhere -- this module only adds wrapper types the API needs on +top of those. +""" + +from pathlib import Path +from uuid import UUID + +from pydantic import BaseModel + +from skelly_synchronize.api.jobs import JobStatus + + +class JobCreateResponse(BaseModel): + job_id: UUID + status: JobStatus + + +class VideoPreview(BaseModel): + video_name: str + filepath: Path + + +class VideosResponse(BaseModel): + folder_path: Path + videos: list[VideoPreview] diff --git a/skelly_synchronize/cli/main.py b/skelly_synchronize/cli/main.py index 53c5d9b..2d072ee 100644 --- a/skelly_synchronize/cli/main.py +++ b/skelly_synchronize/cli/main.py @@ -106,9 +106,9 @@ def main(argv: list[str] | None = None) -> int: create_debug_artifacts=args.create_debug_artifacts, ) - def progress_callback(camera_name: str, progress: float) -> None: + def progress_callback(video_name: str, progress: float) -> None: if progress >= 1.0: - print(f" {camera_name}: trimmed") + print(f" {video_name}: trimmed") try: result = run_pipeline(request, progress_callback) @@ -121,7 +121,7 @@ def progress_callback(camera_name: str, progress: float) -> None: print(f"Synchronized frame count: {result.synchronized_frame_count}") print("Lags:") for lag in result.lags: - print(f" {lag.camera_name}: {lag.lag_seconds:.4f}s") + print(f" {lag.video_name}: {lag.lag_seconds:.4f}s") print(f"Final video length: {result.synchronized_frame_count} frames") if result.debug_artifact_paths: print("Debug artifacts:") diff --git a/skelly_synchronize/core/audio.py b/skelly_synchronize/core/audio.py index d541404..0e46131 100644 --- a/skelly_synchronize/core/audio.py +++ b/skelly_synchronize/core/audio.py @@ -10,16 +10,16 @@ logger = logging.getLogger(__name__) -def get_reference_camera_name(videos: list[VideoInfo]) -> str: - """Pick a deterministic reference camera for cross-correlation (resolves KI-13). +def get_reference_video_name(videos: list[VideoInfo]) -> str: + """Pick a deterministic reference video for cross-correlation (resolves KI-13). - Strategy: the camera with the longest recorded duration; ties (including - the degenerate all-equal case) resolve to sorted-camera-name order, which + Strategy: the video with the longest recorded duration; ties (including + the degenerate all-equal case) resolve to sorted-video-name order, which stays deterministic either way. """ - return sorted(videos, key=lambda v: (-v.duration_seconds, v.camera_name))[ + return sorted(videos, key=lambda v: (-v.duration_seconds, v.video_name))[ 0 - ].camera_name + ].video_name def cross_correlate( @@ -53,43 +53,43 @@ def find_cross_correlation_lags( videos: list[VideoInfo], sample_rate: int, ) -> list[LagResult]: - """Cross correlate every camera's audio against a deterministic reference camera. + """Cross correlate every video's audio against a deterministic reference video. Returns `LagResult`s satisfying the shared contract: `lag_seconds` is the number of seconds to trim off the front of that video so all videos align, normalized so the minimum lag is 0 (resolves KI-02 -- normalized once, here, rather than left as an implicit downstream assumption). """ - reference_camera_name = get_reference_camera_name(videos) - reference_signal = audio_signals[reference_camera_name] + reference_video_name = get_reference_video_name(videos) + reference_signal = audio_signals[reference_video_name] logger.info( - f"Using {reference_camera_name} as the cross-correlation reference camera" + f"Using {reference_video_name} as the cross-correlation reference video" ) raw_lags_seconds: dict[str, float] = {} confidences: dict[str, float] = {} - for camera_name, camera_signal in audio_signals.items(): - lag_samples, confidence = cross_correlate(reference_signal, camera_signal) - raw_lags_seconds[camera_name] = lag_samples / sample_rate - confidences[camera_name] = confidence + for video_name, video_signal in audio_signals.items(): + lag_samples, confidence = cross_correlate(reference_signal, video_signal) + raw_lags_seconds[video_name] = lag_samples / sample_rate + confidences[video_name] = confidence max_lag = max(raw_lags_seconds.values()) return [ LagResult( - camera_name=camera_name, + video_name=video_name, lag_seconds=max_lag - raw_lag, - confidence=confidences[camera_name], + confidence=confidences[video_name], ) - for camera_name, raw_lag in raw_lags_seconds.items() + for video_name, raw_lag in raw_lags_seconds.items() ] def trim_audio_in_memory( audio_signals: dict[str, np.ndarray], sample_rate: int, - lags_by_camera: dict[str, LagResult], + lags_by_video: dict[str, LagResult], synced_length_seconds: float, output_folder: Path, ) -> dict[str, Path]: @@ -104,12 +104,12 @@ def trim_audio_in_memory( length_in_samples = int(synced_length_seconds * sample_rate) output_paths: dict[str, Path] = {} - for camera_name, camera_signal in audio_signals.items(): - lag_in_samples = int(lags_by_camera[camera_name].lag_seconds * sample_rate) - trimmed_signal = camera_signal[lag_in_samples:][:length_in_samples] + for video_name, video_signal in audio_signals.items(): + lag_in_samples = int(lags_by_video[video_name].lag_seconds * sample_rate) + trimmed_signal = video_signal[lag_in_samples:][:length_in_samples] - output_path = output_folder / f"{camera_name}.wav" + output_path = output_folder / f"{video_name}.wav" sf.write(output_path, trimmed_signal, sample_rate, subtype="PCM_24") - output_paths[camera_name] = output_path + output_paths[video_name] = output_path return output_paths diff --git a/skelly_synchronize/core/backends/ffmpeg.py b/skelly_synchronize/core/backends/ffmpeg.py index a370db9..e298a2f 100644 --- a/skelly_synchronize/core/backends/ffmpeg.py +++ b/skelly_synchronize/core/backends/ffmpeg.py @@ -136,7 +136,7 @@ def probe(self, filepath: Path) -> VideoInfo: fps = self._extract_video_fps(filepath) return VideoInfo( filepath=filepath, - camera_name=filepath.stem, + video_name=filepath.stem, duration_seconds=duration_seconds, fps=fps, ) diff --git a/skelly_synchronize/core/debug.py b/skelly_synchronize/core/debug.py index f14c63d..fcb0443 100644 --- a/skelly_synchronize/core/debug.py +++ b/skelly_synchronize/core/debug.py @@ -32,17 +32,17 @@ def save_debug_toml( count shared by every synchronized video -- surfaced explicitly here (rather than requiring a reader to cross-check every entry in `synchronized_video_information`) since an exact frame-count match across - cameras is the core correctness guarantee synchronization is supposed to + videos is the core correctness guarantee synchronization is supposed to provide. """ data = { "raw_video_information": { - video.camera_name: video.model_dump(mode="json") for video in videos_before + video.video_name: video.model_dump(mode="json") for video in videos_before }, "synchronized_video_information": { - video.camera_name: video.model_dump(mode="json") for video in videos_after + video.video_name: video.model_dump(mode="json") for video in videos_after }, - "lag_results": {lag.camera_name: lag.model_dump() for lag in lags}, + "lag_results": {lag.video_name: lag.model_dump() for lag in lags}, } # toml has no null type, so an unknown value is an absent key, not a null value. if synchronized_fps is not None: @@ -110,15 +110,15 @@ def plot_brightness_series( axs[0].set_title("Before Trimming") axs[1].set_title("After Trimming") - for camera_name, brightness_array in before_series.items(): - fps = before_fps[camera_name] + for video_name, brightness_array in before_series.items(): + fps = before_fps[video_name] time = np.arange(len(brightness_array)) / fps - axs[0].plot(time, brightness_array, alpha=0.5, label=camera_name) + axs[0].plot(time, brightness_array, alpha=0.5, label=video_name) - for camera_name, brightness_array in after_series.items(): - fps = after_fps[camera_name] + for video_name, brightness_array in after_series.items(): + fps = after_fps[video_name] time = np.arange(len(brightness_array)) / fps - axs[1].plot(time, brightness_array, alpha=0.5, label=camera_name) + axs[1].plot(time, brightness_array, alpha=0.5, label=video_name) output_path = Path(output_path) logger.info(f"Saving debug plots to: {output_path}") diff --git a/skelly_synchronize/core/models.py b/skelly_synchronize/core/models.py index 2d888ef..f50249b 100644 --- a/skelly_synchronize/core/models.py +++ b/skelly_synchronize/core/models.py @@ -3,7 +3,7 @@ from pydantic import BaseModel -CameraName = str # alias for clarity in signatures +VideoName = str # alias for clarity in signatures class VideoBackendKind(str, Enum): @@ -18,7 +18,7 @@ class SyncMethod(str, Enum): class VideoInfo(BaseModel): filepath: Path - camera_name: str + video_name: str duration_seconds: float fps: float frame_count: int | None = None @@ -26,14 +26,14 @@ class VideoInfo(BaseModel): class AudioInfo(BaseModel): filepath: Path - camera_name: str + video_name: str sample_rate: int duration_seconds: float # raw signal (np.ndarray) is intentionally NOT a field here class LagResult(BaseModel): - camera_name: str + video_name: str lag_seconds: float confidence: float | None = None @@ -55,5 +55,5 @@ class SyncResult(BaseModel): debug_artifact_paths: list[Path] elapsed_seconds: float # The single frame count shared by every synchronized video -- verified - # identical across cameras by VerifySynchronizedFrameCountStage + # identical across videos by VerifySynchronizedFrameCountStage synchronized_frame_count: int | None = None diff --git a/skelly_synchronize/core/pipeline/stages.py b/skelly_synchronize/core/pipeline/stages.py index 24d2fc2..56ca8ae 100644 --- a/skelly_synchronize/core/pipeline/stages.py +++ b/skelly_synchronize/core/pipeline/stages.py @@ -185,7 +185,7 @@ def run( for video in videos: output_path = ( - normalized_folder / f"{video.camera_name}.{VideoExtension.MP4.value}" + normalized_folder / f"{video.video_name}.{VideoExtension.MP4.value}" ) normalize_framerate_and_sample_rate( video.filepath, output_path, target_fps, target_sample_rate @@ -211,12 +211,10 @@ def run( audio_signals = {} sample_rate = None for video in context.videos: - audio_path = ( - audio_folder / f"{video.camera_name}.{AudioExtension.WAV.value}" - ) + audio_path = audio_folder / f"{video.video_name}.{AudioExtension.WAV.value}" extract_audio(video.filepath, audio_path) - camera_signal, sample_rate = librosa.load(path=audio_path, sr=None) - audio_signals[video.camera_name] = camera_signal + video_signal, sample_rate = librosa.load(path=audio_path, sr=None) + audio_signals[video.video_name] = video_signal context.pre_trim_videos = list(context.videos) context.audio_folder_path = audio_folder @@ -241,14 +239,14 @@ def run( event = find_first_brightness_change(series, video.fps, threshold) if event is None: raise SkellySyncError( - f"No brightness change detected for camera {video.camera_name}" + f"No brightness change detected for video {video.video_name}" ) - raw_lags[video.camera_name] = event.lag_seconds + raw_lags[video.video_name] = event.lag_seconds max_lag = max(raw_lags.values()) context.lags = [ - LagResult(camera_name=camera_name, lag_seconds=max_lag - raw_lag) - for camera_name, raw_lag in raw_lags.items() + LagResult(video_name=video_name, lag_seconds=max_lag - raw_lag) + for video_name, raw_lag in raw_lags.items() ] return context @@ -266,7 +264,7 @@ def _trim_video_worker( instance, and probing the result always goes through ffmpeg (KI-03). """ backend = get_backend(backend_kind) - output_path = Path(output_dir) / synced_video_filename(video_info.camera_name) + output_path = Path(output_dir) / synced_video_filename(video_info.video_name) start_seconds = lag.lag_seconds end_seconds = start_seconds + minimum_duration @@ -279,19 +277,19 @@ class TrimStage: """Trims every video to the shared window in parallel. Uses `ProcessPoolExecutor` + `as_completed` instead of - `multiprocessing.Pool.starmap` so a single camera's trim failure doesn't - hide which camera failed or block progress reporting for the others. + `multiprocessing.Pool.starmap` so a single video's trim failure doesn't + hide which video failed or block progress reporting for the others. """ def run( self, context: PipelineContext, progress_callback: ProgressCallback | None ) -> PipelineContext: videos = context.videos - lags_by_camera = {lag.camera_name: lag for lag in context.lags} + lags_by_video = {lag.video_name: lag for lag in context.lags} output_dir = context.synchronized_folder_path minimum_duration = min( - video.duration_seconds - lags_by_camera[video.camera_name].lag_seconds + video.duration_seconds - lags_by_video[video.video_name].lag_seconds for video in videos ) @@ -300,36 +298,36 @@ def run( results: list[VideoInfo] = [] errors: list[tuple[str, BaseException]] = [] with ProcessPoolExecutor(max_workers=max_workers) as executor: - future_to_camera = { + future_to_video = { executor.submit( _trim_video_worker, video, - lags_by_camera[video.camera_name], + lags_by_video[video.video_name], context.request.video_handler, output_dir, minimum_duration, - ): video.camera_name + ): video.video_name for video in videos } - for future in as_completed(future_to_camera): - camera_name = future_to_camera[future] + for future in as_completed(future_to_video): + video_name = future_to_video[future] try: results.append(future.result()) if progress_callback is not None: - progress_callback(camera_name, 1.0) - except Exception as e: # noqa: BLE001 - isolate per-camera failures + progress_callback(video_name, 1.0) + except Exception as e: # noqa: BLE001 - isolate per-video failures logger.error( - f"Error trimming video {camera_name}: {e}", exc_info=True + f"Error trimming video {video_name}: {e}", exc_info=True ) - errors.append((camera_name, e)) + errors.append((video_name, e)) if errors: - failed_cameras = ", ".join(camera_name for camera_name, _ in errors) + failed_videos = ", ".join(video_name for video_name, _ in errors) raise SkellySyncError( - f"Trimming failed for camera(s): {failed_cameras}" + f"Trimming failed for video(s): {failed_videos}" ) from errors[0][1] - context.videos = sorted(results, key=lambda video: video.camera_name) + context.videos = sorted(results, key=lambda video: video.video_name) return context @@ -337,7 +335,7 @@ class VerifySynchronizedFramerateStage: """Hard-fail if the trimmed videos don't all share one exact framerate. Synchronized videos are only actually synchronized if every frame index - maps to the same wall-clock time across cameras -- a framerate mismatch + maps to the same wall-clock time across videos -- a framerate mismatch (even a tiny one introduced by a backend's re-encode) silently breaks that guarantee, so this is checked explicitly rather than assumed. """ @@ -345,13 +343,13 @@ class VerifySynchronizedFramerateStage: def run( self, context: PipelineContext, progress_callback: ProgressCallback | None ) -> PipelineContext: - fps_by_camera = {video.camera_name: video.fps for video in context.videos} - unique_fps_values = set(fps_by_camera.values()) + fps_by_video = {video.video_name: video.fps for video in context.videos} + unique_fps_values = set(fps_by_video.values()) if len(unique_fps_values) > 1: raise SkellySyncError( "Synchronized videos do not share an identical framerate: " - f"{fps_by_camera}" + f"{fps_by_video}" ) context.synchronized_fps = ( @@ -364,8 +362,8 @@ class VerifySynchronizedFrameCountStage: """Hard-fail if the trimmed videos don't all have identical frame counts. This is the actual correctness guarantee of synchronization: if any - camera's output has even one more or fewer frames than the others, frame - N no longer corresponds to the same instant across cameras. Each video's + video's output has even one more or fewer frames than the others, frame + N no longer corresponds to the same instant across videos. Each video's `frame_count` was already read directly from its file during probing (`_probe_with_frame_count`), so this stage just compares those real, already-measured counts rather than re-deriving anything from duration/fps @@ -375,15 +373,15 @@ class VerifySynchronizedFrameCountStage: def run( self, context: PipelineContext, progress_callback: ProgressCallback | None ) -> PipelineContext: - frame_counts_by_camera = { - video.camera_name: video.frame_count for video in context.videos + frame_counts_by_video = { + video.video_name: video.frame_count for video in context.videos } - unique_frame_counts = set(frame_counts_by_camera.values()) + unique_frame_counts = set(frame_counts_by_video.values()) if len(unique_frame_counts) > 1: raise SkellySyncError( "Synchronized videos do not have identical frame counts: " - f"{frame_counts_by_camera}" + f"{frame_counts_by_video}" ) context.synchronized_frame_count = ( @@ -396,7 +394,7 @@ class ReattachAudioStage: def run( self, context: PipelineContext, progress_callback: ProgressCallback | None ) -> PipelineContext: - lags_by_camera = {lag.camera_name: lag for lag in context.lags} + lags_by_video = {lag.video_name: lag for lag in context.lags} synced_length_seconds = ( context.videos[0].duration_seconds if context.videos else 0.0 ) @@ -405,16 +403,16 @@ def run( trimmed_audio_paths = trim_audio_in_memory( context.audio_signals, context.audio_sample_rate, - lags_by_camera, + lags_by_video, synced_length_seconds, trimmed_audio_folder, ) for lag in context.lags: video_path = context.synchronized_folder_path / synced_video_filename( - lag.camera_name + lag.video_name ) - audio_path = trimmed_audio_paths[lag.camera_name] + audio_path = trimmed_audio_paths[lag.video_name] with tempfile.TemporaryDirectory( dir=str(context.synchronized_folder_path) @@ -467,22 +465,22 @@ def run( ) before_series = { - video.camera_name: compute_brightness_series(video) + video.video_name: compute_brightness_series(video) for video in context.pre_trim_videos } after_series = { - video.camera_name: compute_brightness_series(video) + video.video_name: compute_brightness_series(video) for video in context.videos } - for camera_name, series in before_series.items(): + for video_name, series in before_series.items(): save_brightness_series( series, source_folder - / f"{camera_name}{BRIGHTNESS_SUFFIX}.{NUMPY_EXTENSION}", + / f"{video_name}{BRIGHTNESS_SUFFIX}.{NUMPY_EXTENSION}", ) - before_fps = {v.camera_name: v.fps for v in context.pre_trim_videos} - after_fps = {v.camera_name: v.fps for v in context.videos} + before_fps = {v.video_name: v.fps for v in context.pre_trim_videos} + after_fps = {v.video_name: v.fps for v in context.videos} plot_brightness_series( before_series, before_fps, after_series, after_fps, plot_path ) diff --git a/skelly_synchronize/tests/api/test_health.py b/skelly_synchronize/tests/api/test_health.py new file mode 100644 index 0000000..fc4c2b5 --- /dev/null +++ b/skelly_synchronize/tests/api/test_health.py @@ -0,0 +1,11 @@ +from fastapi.testclient import TestClient + +from skelly_synchronize.api.main import app + + +def test_health(): + client = TestClient(app) + response = client.get("/health") + + assert response.status_code == 200 + assert response.json() == {"status": "ok"} diff --git a/skelly_synchronize/tests/api/test_jobs.py b/skelly_synchronize/tests/api/test_jobs.py new file mode 100644 index 0000000..397b6bd --- /dev/null +++ b/skelly_synchronize/tests/api/test_jobs.py @@ -0,0 +1,163 @@ +from fastapi.testclient import TestClient + +import skelly_synchronize.api.jobs as jobs_module +from skelly_synchronize.api.jobs import JobStore, get_job_store +from skelly_synchronize.api.main import app +from skelly_synchronize.core.models import SyncMethod, SyncResult + + +class FakeProcessRunsImmediately: + """Runs `target` synchronously on `start()` -- avoids real multiprocessing + so tests stay fast and so monkeypatched `run_pipeline` (patched in this + process) is actually the code that executes.""" + + def __init__(self, target, args): + self._target = target + self._args = args + self._alive = False + + def start(self): + self._alive = True + self._target(*self._args) + self._alive = False + + def is_alive(self): + return self._alive + + def terminate(self): + self._alive = False + + def join(self, timeout=None): + pass + + +class FakeProcessStaysAlive(FakeProcessRunsImmediately): + """Never runs `target` -- simulates a still-running job for cancel tests.""" + + def start(self): + self._alive = True + + +def _sync_request(tmp_path): + (tmp_path / "cam_a.mp4").touch() + return { + "raw_video_folder_path": str(tmp_path), + "method": SyncMethod.AUDIO.value, + "create_debug_artifacts": False, + } + + +def _fake_result(tmp_path): + return SyncResult( + synchronized_video_folder_path=tmp_path / "synchronized_videos", + videos_before=[], + videos_after=[], + lags=[], + debug_artifact_paths=[], + elapsed_seconds=0.1, + synchronized_frame_count=10, + ) + + +def _override_job_store(monkeypatch, process_class): + monkeypatch.setattr(jobs_module.multiprocessing, "Process", process_class) + store = JobStore() + app.dependency_overrides[get_job_store] = lambda: store + return store + + +def test_create_and_get_job_succeeds(tmp_path, monkeypatch): + def fake_run_pipeline(request, progress_callback=None): + if progress_callback is not None: + progress_callback("cam_a", 1.0) + return _fake_result(tmp_path) + + monkeypatch.setattr(jobs_module, "run_pipeline", fake_run_pipeline) + _override_job_store(monkeypatch, FakeProcessRunsImmediately) + client = TestClient(app) + try: + create_response = client.post("/jobs", json=_sync_request(tmp_path)) + assert create_response.status_code == 201 + job_id = create_response.json()["job_id"] + + get_response = client.get(f"/jobs/{job_id}") + assert get_response.status_code == 200 + body = get_response.json() + assert body["status"] == "succeeded" + assert body["progress"] == 1.0 + assert body["result"]["synchronized_frame_count"] == 10 + finally: + app.dependency_overrides.clear() + + +def test_create_job_missing_folder_returns_404(tmp_path, monkeypatch): + _override_job_store(monkeypatch, FakeProcessRunsImmediately) + client = TestClient(app) + try: + request = _sync_request(tmp_path) + request["raw_video_folder_path"] = str(tmp_path / "does_not_exist") + + response = client.post("/jobs", json=request) + + assert response.status_code == 404 + finally: + app.dependency_overrides.clear() + + +def test_list_jobs_includes_created_job(tmp_path, monkeypatch): + monkeypatch.setattr( + jobs_module, + "run_pipeline", + lambda request, progress_callback=None: _fake_result(tmp_path), + ) + _override_job_store(monkeypatch, FakeProcessRunsImmediately) + client = TestClient(app) + try: + create_response = client.post("/jobs", json=_sync_request(tmp_path)) + job_id = create_response.json()["job_id"] + + list_response = client.get("/jobs") + + assert list_response.status_code == 200 + assert any(job["id"] == job_id for job in list_response.json()) + finally: + app.dependency_overrides.clear() + + +def test_cancel_job_marks_cancelled(tmp_path, monkeypatch): + _override_job_store(monkeypatch, FakeProcessStaysAlive) + client = TestClient(app) + try: + create_response = client.post("/jobs", json=_sync_request(tmp_path)) + job_id = create_response.json()["job_id"] + + cancel_response = client.delete(f"/jobs/{job_id}") + + assert cancel_response.status_code == 200 + assert cancel_response.json()["status"] == "cancelled" + finally: + app.dependency_overrides.clear() + + +def test_get_unknown_job_returns_404(monkeypatch): + _override_job_store(monkeypatch, FakeProcessRunsImmediately) + client = TestClient(app) + try: + response = client.get("/jobs/00000000-0000-0000-0000-000000000000") + assert response.status_code == 404 + finally: + app.dependency_overrides.clear() + + +def test_debug_plot_missing_before_success_returns_404(tmp_path, monkeypatch): + _override_job_store(monkeypatch, FakeProcessStaysAlive) + client = TestClient(app) + try: + create_response = client.post("/jobs", json=_sync_request(tmp_path)) + job_id = create_response.json()["job_id"] + + response = client.get(f"/jobs/{job_id}/debug-plot") + + assert response.status_code == 404 + finally: + app.dependency_overrides.clear() diff --git a/skelly_synchronize/tests/api/test_videos.py b/skelly_synchronize/tests/api/test_videos.py new file mode 100644 index 0000000..4e7e5d8 --- /dev/null +++ b/skelly_synchronize/tests/api/test_videos.py @@ -0,0 +1,27 @@ +from fastapi.testclient import TestClient + +from skelly_synchronize.api.main import app + +client = TestClient(app) + + +def test_list_videos_returns_discovered_video_stems(tmp_path): + (tmp_path / "cam_a.mp4").touch() + (tmp_path / "cam_b.mov").touch() + (tmp_path / "notes.txt").touch() + + response = client.get("/videos", params={"folder_path": str(tmp_path)}) + + assert response.status_code == 200 + body = response.json() + assert body["folder_path"] == str(tmp_path) + video_names = sorted(video["video_name"] for video in body["videos"]) + assert video_names == ["cam_a", "cam_b"] + + +def test_list_videos_missing_folder_returns_404(tmp_path): + missing_folder = tmp_path / "does_not_exist" + + response = client.get("/videos", params={"folder_path": str(missing_folder)}) + + assert response.status_code == 404 diff --git a/skelly_synchronize/tests/core/backends/test_ffmpeg.py b/skelly_synchronize/tests/core/backends/test_ffmpeg.py index d9c3efa..c88181a 100644 --- a/skelly_synchronize/tests/core/backends/test_ffmpeg.py +++ b/skelly_synchronize/tests/core/backends/test_ffmpeg.py @@ -37,7 +37,7 @@ def test_probe_parses_duration_and_fps(monkeypatch, backend): video_info = backend.probe(Path("some_video.mp4")) - assert video_info.camera_name == "some_video" + assert video_info.video_name == "some_video" assert video_info.duration_seconds == 12.5 assert video_info.fps == 30.0 diff --git a/skelly_synchronize/tests/core/pipeline/test_stages.py b/skelly_synchronize/tests/core/pipeline/test_stages.py index 9926e61..68a72af 100644 --- a/skelly_synchronize/tests/core/pipeline/test_stages.py +++ b/skelly_synchronize/tests/core/pipeline/test_stages.py @@ -27,14 +27,14 @@ def _make_video_info( - camera_name: str, + video_name: str, fps: float = 30.0, duration_seconds: float = 10.0, frame_count: int | None = None, ): return VideoInfo( - filepath=Path(f"{camera_name}.mp4"), - camera_name=camera_name, + filepath=Path(f"{video_name}.mp4"), + video_name=video_name, duration_seconds=duration_seconds, fps=fps, frame_count=frame_count, @@ -85,12 +85,12 @@ def test_brightness_lag_stage_normalizes_lags(monkeypatch, tmp_path): raw_lag_seconds = {"cam_a": 1.0, "cam_b": 3.0} - # encode the camera's raw lag in the "series" so the fake event-finder + # encode the video's raw lag in the "series" so the fake event-finder # can decode it back out, without needing to track call order. monkeypatch.setattr( stages_module, "compute_brightness_series", - lambda video: np.array([raw_lag_seconds[video.camera_name]]), + lambda video: np.array([raw_lag_seconds[video.video_name]]), ) monkeypatch.setattr( stages_module, @@ -101,12 +101,12 @@ def test_brightness_lag_stage_normalizes_lags(monkeypatch, tmp_path): ) lags = BrightnessLagStage().run(context, None).lags - lag_by_camera = {lag.camera_name: lag.lag_seconds for lag in lags} + lag_by_video = {lag.video_name: lag.lag_seconds for lag in lags} max_lag = max(raw_lag_seconds.values()) - assert min(lag_by_camera.values()) == 0.0 - assert lag_by_camera["cam_a"] == max_lag - raw_lag_seconds["cam_a"] - assert lag_by_camera["cam_b"] == max_lag - raw_lag_seconds["cam_b"] + assert min(lag_by_video.values()) == 0.0 + assert lag_by_video["cam_a"] == max_lag - raw_lag_seconds["cam_a"] + assert lag_by_video["cam_b"] == max_lag - raw_lag_seconds["cam_b"] def test_brightness_lag_stage_raises_when_no_event_detected(monkeypatch, tmp_path): @@ -132,9 +132,9 @@ def __init__(self, should_fail_for: set[str] | None = None): self.should_fail_for = should_fail_for or set() def trim(self, filepath, start_seconds, end_seconds, output_path): - camera_name = Path(filepath).stem - if camera_name in self.should_fail_for: - raise RuntimeError(f"boom for {camera_name}") + video_name = Path(filepath).stem + if video_name in self.should_fail_for: + raise RuntimeError(f"boom for {video_name}") Path(output_path).touch() def probe(self, filepath): @@ -146,16 +146,16 @@ def test_trim_video_worker_uses_ffmpeg_backend_for_final_probe(monkeypatch, tmp_ monkeypatch.setattr(stages_module, "get_backend", lambda kind: fake_backend) video_info = _make_video_info("raw_cam_a", duration_seconds=10.0) - lag = LagResult(camera_name="raw_cam_a", lag_seconds=1.0) + lag = LagResult(video_name="raw_cam_a", lag_seconds=1.0) result = _trim_video_worker( video_info, lag, VideoBackendKind.FFMPEG, tmp_path, minimum_duration=5.0 ) - assert result.camera_name == "synced_cam_a" + assert result.video_name == "synced_cam_a" -def test_trim_stage_isolates_per_camera_errors(monkeypatch, tmp_path): +def test_trim_stage_isolates_per_video_errors(monkeypatch, tmp_path): # Run the pool in-thread (not in a subprocess) so the monkeypatched # backend factory is visible to "worker" code during the test. monkeypatch.setattr( @@ -174,8 +174,8 @@ def test_trim_stage_isolates_per_camera_errors(monkeypatch, tmp_path): _make_video_info("cam_b", duration_seconds=10.0), ] lags = [ - LagResult(camera_name="cam_a", lag_seconds=0.0), - LagResult(camera_name="cam_b", lag_seconds=0.0), + LagResult(video_name="cam_a", lag_seconds=0.0), + LagResult(video_name="cam_b", lag_seconds=0.0), ] context = PipelineContext( request=request, videos=videos, lags=lags, synchronized_folder_path=output_dir @@ -185,7 +185,7 @@ def test_trim_stage_isolates_per_camera_errors(monkeypatch, tmp_path): TrimStage().run(context, None) -def test_trim_stage_succeeds_when_all_cameras_trim_cleanly(monkeypatch, tmp_path): +def test_trim_stage_succeeds_when_all_videos_trim_cleanly(monkeypatch, tmp_path): monkeypatch.setattr( stages_module, "ProcessPoolExecutor", concurrent.futures.ThreadPoolExecutor ) @@ -202,8 +202,8 @@ def test_trim_stage_succeeds_when_all_cameras_trim_cleanly(monkeypatch, tmp_path _make_video_info("cam_b", duration_seconds=10.0), ] lags = [ - LagResult(camera_name="cam_a", lag_seconds=0.0), - LagResult(camera_name="cam_b", lag_seconds=1.0), + LagResult(video_name="cam_a", lag_seconds=0.0), + LagResult(video_name="cam_b", lag_seconds=1.0), ] context = PipelineContext( request=request, videos=videos, lags=lags, synchronized_folder_path=output_dir @@ -211,7 +211,7 @@ def test_trim_stage_succeeds_when_all_cameras_trim_cleanly(monkeypatch, tmp_path result = TrimStage().run(context, None) - assert {video.camera_name for video in result.videos} == { + assert {video.video_name for video in result.videos} == { "synced_cam_a", "synced_cam_b", } @@ -232,7 +232,7 @@ def test_verify_synchronized_framerate_stage_passes_when_fps_matches(tmp_path): def test_verify_synchronized_framerate_stage_raises_on_any_mismatch(tmp_path): # this is the core correctness guarantee of synchronization: even a tiny - # fps discrepancy between cameras (e.g. from a lossy backend re-encode) + # fps discrepancy between videos (e.g. from a lossy backend re-encode) # must fail loudly instead of silently producing misaligned output. videos = [ _make_video_info("synced_cam_a", fps=29.97), @@ -274,7 +274,7 @@ def test_verify_synchronized_frame_count_stage_passes_when_counts_match(tmp_path def test_verify_synchronized_frame_count_stage_raises_on_any_mismatch(tmp_path): # this is the actual correctness guarantee of synchronization: matching - # fps doesn't help if one camera's output is a frame longer or shorter. + # fps doesn't help if one video's output is a frame longer or shorter. request = _make_request(tmp_path) videos = [ _make_video_info("synced_cam_a", frame_count=300), diff --git a/skelly_synchronize/tests/core/test_audio.py b/skelly_synchronize/tests/core/test_audio.py index 6544a21..01280c2 100644 --- a/skelly_synchronize/tests/core/test_audio.py +++ b/skelly_synchronize/tests/core/test_audio.py @@ -5,36 +5,36 @@ from skelly_synchronize.core.audio import ( cross_correlate, find_cross_correlation_lags, - get_reference_camera_name, + get_reference_video_name, trim_audio_in_memory, ) from skelly_synchronize.core.models import LagResult, VideoInfo -def _make_video_info(camera_name: str, duration_seconds: float) -> VideoInfo: +def _make_video_info(video_name: str, duration_seconds: float) -> VideoInfo: return VideoInfo( - filepath=Path(f"{camera_name}.mp4"), - camera_name=camera_name, + filepath=Path(f"{video_name}.mp4"), + video_name=video_name, duration_seconds=duration_seconds, fps=30.0, ) -def test_get_reference_camera_name_picks_longest_duration(): +def test_get_reference_video_name_picks_longest_duration(): videos = [ _make_video_info("cam_a", 10.0), _make_video_info("cam_b", 12.0), _make_video_info("cam_c", 11.0), ] - assert get_reference_camera_name(videos) == "cam_b" + assert get_reference_video_name(videos) == "cam_b" -def test_get_reference_camera_name_ties_break_by_name(): +def test_get_reference_video_name_ties_break_by_name(): videos = [ _make_video_info("cam_b", 10.0), _make_video_info("cam_a", 10.0), ] - assert get_reference_camera_name(videos) == "cam_a" + assert get_reference_video_name(videos) == "cam_a" def test_cross_correlate_detects_known_shift(): @@ -63,9 +63,9 @@ def test_find_cross_correlation_lags_normalizes_to_zero_minimum(): videos = [_make_video_info("cam_a", 1.0), _make_video_info("cam_b", 1.0)] lags = find_cross_correlation_lags(audio_signals, videos, sample_rate) - lag_by_camera = {lag.camera_name: lag.lag_seconds for lag in lags} + lag_by_video = {lag.video_name: lag.lag_seconds for lag in lags} - assert min(lag_by_camera.values()) == 0.0 + assert min(lag_by_video.values()) == 0.0 assert all(confidence.confidence is not None for confidence in lags) @@ -76,8 +76,8 @@ def test_trim_audio_in_memory_reuses_loaded_signals(tmp_path): "cam_b": np.arange(0, 100, dtype=float), } lags = { - "cam_a": LagResult(camera_name="cam_a", lag_seconds=0.0), - "cam_b": LagResult(camera_name="cam_b", lag_seconds=0.1), + "cam_a": LagResult(video_name="cam_a", lag_seconds=0.0), + "cam_b": LagResult(video_name="cam_b", lag_seconds=0.1), } output_paths = trim_audio_in_memory( diff --git a/skelly_synchronize/tests/core/test_debug.py b/skelly_synchronize/tests/core/test_debug.py index 16585ce..3946566 100644 --- a/skelly_synchronize/tests/core/test_debug.py +++ b/skelly_synchronize/tests/core/test_debug.py @@ -6,10 +6,10 @@ from skelly_synchronize.core.models import LagResult, VideoInfo -def _make_video_info(camera_name: str, fps: float = 29.97) -> VideoInfo: +def _make_video_info(video_name: str, fps: float = 29.97) -> VideoInfo: return VideoInfo( - filepath=Path(f"{camera_name}.mp4"), - camera_name=camera_name, + filepath=Path(f"{video_name}.mp4"), + video_name=video_name, duration_seconds=10.0, fps=fps, ) @@ -28,7 +28,7 @@ def test_save_debug_toml_surfaces_synchronized_fps(tmp_path): _make_video_info("synced_cam_a"), _make_video_info("synced_cam_b"), ], - lags=[LagResult(camera_name="cam_a", lag_seconds=0.0)], + lags=[LagResult(video_name="cam_a", lag_seconds=0.0)], synchronized_fps=29.97, synchronized_frame_count=872, ) diff --git a/skelly_synchronize/tests/core/test_models.py b/skelly_synchronize/tests/core/test_models.py index 52e9d4e..0ce3673 100644 --- a/skelly_synchronize/tests/core/test_models.py +++ b/skelly_synchronize/tests/core/test_models.py @@ -14,11 +14,11 @@ def test_video_info_requires_all_fields(): with pytest.raises(ValidationError): - VideoInfo(filepath=Path("video.mp4"), camera_name="cam_1") + VideoInfo(filepath=Path("video.mp4"), video_name="cam_1") video_info = VideoInfo( filepath=Path("video.mp4"), - camera_name="cam_1", + video_name="cam_1", duration_seconds=10.0, fps=30.0, ) @@ -26,7 +26,7 @@ def test_video_info_requires_all_fields(): def test_lag_result_confidence_is_optional(): - lag_result = LagResult(camera_name="cam_1", lag_seconds=0.5) + lag_result = LagResult(video_name="cam_1", lag_seconds=0.5) assert lag_result.confidence is None diff --git a/skelly_synchronize/tests/integration/test_audio_sync_pipeline.py b/skelly_synchronize/tests/integration/test_audio_sync_pipeline.py index 3d72f66..c288718 100644 --- a/skelly_synchronize/tests/integration/test_audio_sync_pipeline.py +++ b/skelly_synchronize/tests/integration/test_audio_sync_pipeline.py @@ -38,9 +38,9 @@ def test_audio_sync_pipeline_end_to_end( assert result.synchronized_video_folder_path.exists() - lags_by_camera = {lag.camera_name: lag.lag_seconds for lag in result.lags} - for camera_name, expected_lag in EXPECTED_LAG_SECONDS.items(): - assert lags_by_camera[camera_name] == pytest.approx( + lags_by_video = {lag.video_name: lag.lag_seconds for lag in result.lags} + for video_name, expected_lag in EXPECTED_LAG_SECONDS.items(): + assert lags_by_video[video_name] == pytest.approx( expected_lag, abs=LAG_TOLERANCE_SECONDS ) diff --git a/skelly_synchronize/tests/integration/test_brightness_sync_pipeline.py b/skelly_synchronize/tests/integration/test_brightness_sync_pipeline.py index d278ba8..57b62dc 100644 --- a/skelly_synchronize/tests/integration/test_brightness_sync_pipeline.py +++ b/skelly_synchronize/tests/integration/test_brightness_sync_pipeline.py @@ -33,9 +33,9 @@ def test_brightness_sync_pipeline_end_to_end( assert result.synchronized_video_folder_path.exists() - lags_by_camera = {lag.camera_name: lag.lag_seconds for lag in result.lags} - for camera_name, expected_lag in EXPECTED_LAG_SECONDS.items(): - assert lags_by_camera[camera_name] == pytest.approx( + lags_by_video = {lag.video_name: lag.lag_seconds for lag in result.lags} + for video_name, expected_lag in EXPECTED_LAG_SECONDS.items(): + assert lags_by_video[video_name] == pytest.approx( expected_lag, abs=LAG_TOLERANCE_SECONDS ) From a4f3ddb2be0c32a06a382f7aa4b98df9ca41fa0d Mon Sep 17 00:00:00 2001 From: philipqueen Date: Mon, 10 Aug 2026 13:26:25 -0600 Subject: [PATCH 07/16] remove old code --- skelly_synchronize_old/__init__.py | 38 --- skelly_synchronize_old/__main__.py | 25 -- .../core_processes/audio_utilities.py | 98 ------ .../core_processes/correlation_functions.py | 137 --------- .../core_processes/debugging/debug_output.py | 16 - .../core_processes/debugging/debug_plots.py | 130 -------- .../core_processes/normalize_framerates.py | 46 --- .../video_functions/deffcode_functions.py | 83 ------ .../video_functions/ffmpeg_functions.py | 282 ------------------ .../video_functions/video_utilities.py | 210 ------------- .../gui/skelly_synchronize_gui.py | 98 ------ .../gui/widgets/run_button_widget.py | 16 - skelly_synchronize_old/skelly_synchronize.py | 278 ----------------- skelly_synchronize_old/system/__init__.py | 0 .../system/default_paths.py | 48 --- .../system/file_extensions.py | 18 -- .../system/logging_configuration.py | 42 --- .../system/paths_and_file_names.py | 24 -- skelly_synchronize_old/tests/conftest.py | 50 ---- .../tests/test_all_files_created.py | 45 --- .../tests/test_normalize_lag_dict.py | 26 -- .../test_number_of_videos_is_preserved.py | 15 - .../tests/test_trim_single_video_deffcode.py | 33 -- .../tests/test_videos_are_same_length.py | 20 -- .../utilities/check_list_values_are_equal.py | 19 -- .../utilities/find_frame_count_of_video.py | 9 - ..._number_of_frames_of_videos_in_a_folder.py | 43 --- .../tests/utilities/load_sample_data.py | 38 --- .../utils/get_video_files.py | 33 -- .../utils/path_handling_utilities.py | 35 --- 30 files changed, 1955 deletions(-) delete mode 100644 skelly_synchronize_old/__init__.py delete mode 100644 skelly_synchronize_old/__main__.py delete mode 100644 skelly_synchronize_old/core_processes/audio_utilities.py delete mode 100644 skelly_synchronize_old/core_processes/correlation_functions.py delete mode 100644 skelly_synchronize_old/core_processes/debugging/debug_output.py delete mode 100644 skelly_synchronize_old/core_processes/debugging/debug_plots.py delete mode 100644 skelly_synchronize_old/core_processes/normalize_framerates.py delete mode 100644 skelly_synchronize_old/core_processes/video_functions/deffcode_functions.py delete mode 100644 skelly_synchronize_old/core_processes/video_functions/ffmpeg_functions.py delete mode 100644 skelly_synchronize_old/core_processes/video_functions/video_utilities.py delete mode 100644 skelly_synchronize_old/gui/skelly_synchronize_gui.py delete mode 100644 skelly_synchronize_old/gui/widgets/run_button_widget.py delete mode 100644 skelly_synchronize_old/skelly_synchronize.py delete mode 100644 skelly_synchronize_old/system/__init__.py delete mode 100644 skelly_synchronize_old/system/default_paths.py delete mode 100644 skelly_synchronize_old/system/file_extensions.py delete mode 100644 skelly_synchronize_old/system/logging_configuration.py delete mode 100644 skelly_synchronize_old/system/paths_and_file_names.py delete mode 100644 skelly_synchronize_old/tests/conftest.py delete mode 100644 skelly_synchronize_old/tests/test_all_files_created.py delete mode 100644 skelly_synchronize_old/tests/test_normalize_lag_dict.py delete mode 100644 skelly_synchronize_old/tests/test_number_of_videos_is_preserved.py delete mode 100644 skelly_synchronize_old/tests/test_trim_single_video_deffcode.py delete mode 100644 skelly_synchronize_old/tests/test_videos_are_same_length.py delete mode 100644 skelly_synchronize_old/tests/utilities/check_list_values_are_equal.py delete mode 100644 skelly_synchronize_old/tests/utilities/find_frame_count_of_video.py delete mode 100644 skelly_synchronize_old/tests/utilities/get_number_of_frames_of_videos_in_a_folder.py delete mode 100644 skelly_synchronize_old/tests/utilities/load_sample_data.py delete mode 100644 skelly_synchronize_old/utils/get_video_files.py delete mode 100644 skelly_synchronize_old/utils/path_handling_utilities.py diff --git a/skelly_synchronize_old/__init__.py b/skelly_synchronize_old/__init__.py deleted file mode 100644 index 905a4d1..0000000 --- a/skelly_synchronize_old/__init__.py +++ /dev/null @@ -1,38 +0,0 @@ -"""Top-level package for basic_template_repo.""" - -__package_name__ = "skelly_synchronize" -__version__ = "v2025.04.1037" - -__author__ = """Philip Queen""" -__email__ = "info@freemocap.org" -__repo_owner_github_user_name__ = "freemocap" -__repo_url__ = ( - f"https://github.com/{__repo_owner_github_user_name__}/{__package_name__}/" -) -__repo_issues_url__ = f"{__repo_url__}issues" - -import sys -from pathlib import Path - - -# print(f"Thank you for using {__package_name__}!") -# print(f"This is printing from: {__file__}") -# print(f"Source code for this package is available at: {__repo_url__}") - -base_package_path = Path(__file__).parent -# print(f"adding base_package_path: {base_package_path} : to sys.path") -sys.path.insert(0, str(base_package_path)) # add parent directory to sys.path - -from skelly_synchronize.system.default_paths import get_log_file_path -from skelly_synchronize.system.logging_configuration import configure_logging -from skelly_synchronize.skelly_synchronize import ( - synchronize_videos_from_audio, - synchronize_videos_from_brightness, -) -from skelly_synchronize.core_processes.debugging.debug_plots import ( - create_audio_debug_plots, - create_brightness_debug_plots, -) - - -configure_logging(log_file_path=str(get_log_file_path())) diff --git a/skelly_synchronize_old/__main__.py b/skelly_synchronize_old/__main__.py deleted file mode 100644 index 762e900..0000000 --- a/skelly_synchronize_old/__main__.py +++ /dev/null @@ -1,25 +0,0 @@ -# __main__.py -import sys -from pathlib import Path -import argparse - -base_package_path = Path(__file__).parent.parent -print(f"adding base_package_path: {base_package_path} : to sys.path") -sys.path.insert(0, str(base_package_path)) # add parent directory to sys.path - - -def parse_args(): - parser = argparse.ArgumentParser(description="Skelly Synchronize") - return parser.parse_args() - - -def run(): - parse_args() - - from gui.skelly_synchronize_gui import main - - main() - - -if __name__ == "__main__": - run() diff --git a/skelly_synchronize_old/core_processes/audio_utilities.py b/skelly_synchronize_old/core_processes/audio_utilities.py deleted file mode 100644 index 9a3fc16..0000000 --- a/skelly_synchronize_old/core_processes/audio_utilities.py +++ /dev/null @@ -1,98 +0,0 @@ -import logging -import librosa -import soundfile as sf -from pathlib import Path -import numpy as np -from typing import Dict - -from skelly_synchronize_old.core_processes.video_functions.ffmpeg_functions import ( - extract_audio_from_video_ffmpeg, - extract_audio_sample_rate_ffmpeg, -) -from skelly_synchronize_old.system.file_extensions import AudioExtension -from skelly_synchronize_old.system.paths_and_file_names import TRIMMED_AUDIO_FOLDER_NAME - -logger = logging.getLogger(__name__) - - -def get_audio_sample_rates(video_info_dict: Dict[str, dict]) -> list: - """Get the sample rates of each audio file and return them in a list""" - audio_sample_rate_list = [ - extract_audio_sample_rate_ffmpeg(file_pathstring=video_dict["video pathstring"]) - for video_dict in video_info_dict.values() - ] - - return audio_sample_rate_list - - -def normalize_audio(audio_file: np.ndarray): - """Perform z-score normalization on an audio file and return the normalized audio file - this is best practice for correlating.""" - return (audio_file - np.mean(audio_file)) / np.std(audio_file - np.mean(audio_file)) - - -def extract_audio_files( - video_info_dict: Dict[str, dict], - audio_extension: AudioExtension, - audio_folder_path: Path, -) -> dict: - """Get a dictionary with audio files and information from the given video file paths.""" - audio_signal_dict = dict() - for video_dict in video_info_dict.values(): - audio_name = f"{video_dict['camera name']}.{audio_extension.value}" - audio_file_path = audio_folder_path / audio_name - - extract_audio_from_video_ffmpeg( - file_pathstring=video_dict["video pathstring"], - output_file_path=audio_file_path, - ) - - if not audio_file_path.is_file(): - logging.error("Error loading audio file, verify video has audio track") - raise FileNotFoundError( - f"Audio file not found: {audio_file_path}, ensure input video has audio" - ) - - audio_signal, sample_rate = librosa.load(path=audio_file_path, sr=None) - - audio_duration = librosa.get_duration(y=audio_signal, sr=sample_rate) - logger.info(f"audio file {audio_name} is {audio_duration} seconds long") - audio_signal_dict[audio_name] = { - "audio file": audio_signal, - "sample rate": sample_rate, - "camera name": video_dict["camera name"], - "audio duration": audio_duration, - } - - return audio_signal_dict - - -def trim_audio_files( - audio_folder_path: Path, - lag_dictionary: dict, - synced_video_length: float, - audio_extension: AudioExtension = AudioExtension.WAV, -): - logger.info("Trimming audio files to match synchronized video length") - - trimmed_audio_folder_path = Path(audio_folder_path) / TRIMMED_AUDIO_FOLDER_NAME - trimmed_audio_folder_path.mkdir(parents=True, exist_ok=True) - - for audio_filepath in audio_folder_path.glob(f"*.{audio_extension.value}"): - audio_signal, sr = librosa.load(path=audio_filepath, sr=None) - lag = lag_dictionary[audio_filepath.stem] - - lag_in_samples = int(float(lag) * sr) - synched_video_length_in_samples = int(synced_video_length * sr) - - shortened_audio_signal = audio_signal[lag_in_samples:] - shortened_audio_signal = shortened_audio_signal[ - :synched_video_length_in_samples - ] - - audio_filename = f"{audio_filepath.stem}.{AudioExtension.WAV.value}" - - logger.info(f"Saving audio {audio_filename}") - output_path = trimmed_audio_folder_path / audio_filename - sf.write(output_path, shortened_audio_signal, sr, subtype="PCM_24") - - return trimmed_audio_folder_path diff --git a/skelly_synchronize_old/core_processes/correlation_functions.py b/skelly_synchronize_old/core_processes/correlation_functions.py deleted file mode 100644 index ccf4e4f..0000000 --- a/skelly_synchronize_old/core_processes/correlation_functions.py +++ /dev/null @@ -1,137 +0,0 @@ -import logging -from pathlib import Path -import cv2 -import numpy as np -from typing import Dict -from scipy import signal - -from skelly_synchronize_old.system.file_extensions import NUMPY_EXTENSION -from skelly_synchronize_old.system.paths_and_file_names import BRIGHTNESS_SUFFIX - -logger = logging.getLogger(__name__) - - -def cross_correlate(audio1: np.ndarray, audio2: np.ndarray): - """Take two audio files, synchronize them using cross correlation, and trim them to the same length. - Inputs are two audio arrays to be synchronized. Return the lag expressed in terms of the audio sample rate of the clips. - """ - - # compute cross correlation with scipy correlate function, which gives the correlation of every different lag value - # mode='full' makes sure every lag value possible between the two signals is used, and method='fft' uses the fast fourier transform to speed the process up - correlation = signal.correlate(audio1, audio2, mode="full", method="fft") - # lags gives the amount of time shift used at each index, corresponding to the index of the correlate output list - lags = signal.correlation_lags(audio1.size, audio2.size, mode="full") - # lag is the time shift used at the point of maximum correlation - this is the key value used for shifting our audio/video - lag = lags[np.argmax(correlation)] - - return lag - - -def find_first_brightness_change( - video_pathstring: str, brightness_ratio_threshold: float = 1000 -) -> int: - logger.info(f"Detecting first brightness change in {video_pathstring}") - brightness_array = find_brightness_across_frames(video_pathstring) - brightness_difference = np.diff(brightness_array, prepend=brightness_array[0]) - brightness_double_difference = np.diff( - brightness_difference, prepend=brightness_difference[0] - ) - - combined_brightness_metric = brightness_difference * brightness_double_difference - - first_brightness_change = np.argmax( - combined_brightness_metric >= brightness_ratio_threshold - ) - - if first_brightness_change == 0: - logger.info( - "No brightness change exceeded threshold, defaulting to frame with fastest detected brightness change" - ) - first_brightness_change = np.argmax(brightness_double_difference) - else: - logger.info( - f"First brightness change detected at frame number {first_brightness_change}" - ) - - return int(first_brightness_change) - - -def find_brightness_across_frames(video_pathstring: str) -> np.ndarray: - video_capture_object = cv2.VideoCapture(video_pathstring) - - video_framecount = int(video_capture_object.get(cv2.CAP_PROP_FRAME_COUNT)) - brightness_array = np.zeros(video_framecount) - - frame_number = 0 - - while frame_number < video_framecount: - ret, frame = video_capture_object.read() - gray_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) - brightness_array[frame_number] = np.mean(gray_frame) - frame_number += 1 - - video_path = Path(video_pathstring) - brightness_array_pathstring = f"{str(video_path.parent / video_path.stem)}{BRIGHTNESS_SUFFIX}.{NUMPY_EXTENSION}" - np.save(file=brightness_array_pathstring, arr=brightness_array) - - return brightness_array - - -def normalize_lag_dictionary(lag_dictionary: Dict[str, float]) -> Dict[str, float]: - """Subtract every value in the dict from the max value. - This creates a normalized lag dict where the latest video has lag of 0. - The max value lag represents the latest starting video.""" - - normalized_lag_dictionary = { - camera_name: (max(lag_dictionary.values()) - value) - for camera_name, value in lag_dictionary.items() - } - - return normalized_lag_dictionary - - -def find_cross_correlation_lags( - audio_signal_dict: dict, sample_rate: int -) -> Dict[str, float]: - """Take a dictionary of audio signals, as well as the sample rate of the audio, cross correlate the audio files, and output a lag dictionary. - The lag dict is normalized so that the lag of the latest video to start in time is 0, and all other lags are positive. - """ - comparison_file_key = next(iter(audio_signal_dict)) - logger.info( - f"comparison file is: {comparison_file_key}, sample rate is: {sample_rate}" - ) - - lag_dict = { - single_audio_dict["camera name"]: cross_correlate( - audio1=audio_signal_dict[comparison_file_key]["audio file"], - audio2=single_audio_dict["audio file"], - ) - / sample_rate - for single_audio_dict in audio_signal_dict.values() - } # cross correlates all audio to the first audio file in the dict, and divides by the audio sample rate in order to get the lag in seconds - - normalized_lag_dict = normalize_lag_dictionary(lag_dictionary=lag_dict) - - logger.info( - f"original lag dict: {lag_dict} normalized lag dict: {normalized_lag_dict}" - ) - - return normalized_lag_dict - - -def find_brightest_point_lags( - video_info_dict: dict, frame_rate: float, brightness_ratio_threshold: float = 1000 -) -> Dict[str, float]: - """Take a video info dictionary, find the first significant contrast change in the video, and return its time in second as the lag. - The lag dict is normalized so that the lag of the latest video to start in time is 0, and all other lags are positive. - """ - lag_dict = { - video_dict["camera name"]: find_first_brightness_change( - video_pathstring=str(video_dict["video pathstring"]), - brightness_ratio_threshold=brightness_ratio_threshold, - ) - / frame_rate - for video_dict in video_info_dict.values() - } - - return lag_dict diff --git a/skelly_synchronize_old/core_processes/debugging/debug_output.py b/skelly_synchronize_old/core_processes/debugging/debug_output.py deleted file mode 100644 index b28cd21..0000000 --- a/skelly_synchronize_old/core_processes/debugging/debug_output.py +++ /dev/null @@ -1,16 +0,0 @@ -import toml -from pathlib import Path - - -def save_dictionaries_to_toml(input_dictionaries: dict, output_file_path: Path): - """Saves informative dictionaries to a TOML file for debugging""" - with open(output_file_path, "w") as toml_file: - toml_file.write(toml.dumps(input_dictionaries)) - - -def remove_audio_files_from_audio_signal_dict(audio_signal_dictionary: dict) -> dict: - """Remove audio files from audio signal dict to reduce unnessary storage""" - for audio_info_dictionary in audio_signal_dictionary.values(): - audio_info_dictionary.pop("audio file") - - return audio_signal_dictionary diff --git a/skelly_synchronize_old/core_processes/debugging/debug_plots.py b/skelly_synchronize_old/core_processes/debugging/debug_plots.py deleted file mode 100644 index 18abf2c..0000000 --- a/skelly_synchronize_old/core_processes/debugging/debug_plots.py +++ /dev/null @@ -1,130 +0,0 @@ -import logging -import librosa -from matplotlib import pyplot as plt -import numpy as np -from pathlib import Path -from typing import List - -from skelly_synchronize_old.system.file_extensions import NUMPY_EXTENSION, AudioExtension -from skelly_synchronize_old.system.paths_and_file_names import ( - BRIGHTNESS_SUFFIX, - DEBUG_PLOT_NAME, - AUDIO_FILES_FOLDER_NAME, - TRIMMED_AUDIO_FOLDER_NAME, -) - -logger = logging.getLogger(__name__) - - -def create_brightness_debug_plots( - raw_video_folder_path: Path, synchronized_video_folder_path: Path -): - output_filepath = synchronized_video_folder_path / DEBUG_PLOT_NAME - - list_of_raw_brightness_paths = get_brightness_npys_from_folder( - folder_path=raw_video_folder_path - ) - list_of_trimmed_brightness_paths = get_brightness_npys_from_folder( - folder_path=synchronized_video_folder_path - ) - - logger.info("Creating debug plots") - plot_brightness_across_frames( - raw_brightness_npys=list_of_raw_brightness_paths, - trimmed_brightness_npys=list_of_trimmed_brightness_paths, - output_filepath=output_filepath, - ) - - -def create_audio_debug_plots(synchronized_video_folder_path: Path): - output_filepath = synchronized_video_folder_path / DEBUG_PLOT_NAME - raw_audio_folder_path = synchronized_video_folder_path / AUDIO_FILES_FOLDER_NAME - trimmed_audio_folder_path = raw_audio_folder_path / TRIMMED_AUDIO_FOLDER_NAME - - list_of_raw_audio_paths = get_audio_paths_from_folder(raw_audio_folder_path) - list_of_trimmed_audio_paths = get_audio_paths_from_folder(trimmed_audio_folder_path) - - logger.info("Creating debug plots") - plot_audio_waveforms( - raw_audio_filepath_list=list_of_raw_audio_paths, - trimmed_audio_filepath_list=list_of_trimmed_audio_paths, - output_filepath=output_filepath, - ) - - -def get_brightness_npys_from_folder(folder_path: Path) -> List[Path]: - search_extension = f"*{BRIGHTNESS_SUFFIX}.{NUMPY_EXTENSION}" - return list(Path(folder_path).glob(search_extension)) - - -def get_audio_paths_from_folder( - folder_path: Path, audio_extension: AudioExtension = AudioExtension.WAV -) -> List[Path]: - search_extension = f"*.{audio_extension.value}" - return list(Path(folder_path).glob(search_extension)) - - -def plot_brightness_across_frames( - raw_brightness_npys: List[Path], - trimmed_brightness_npys: List[Path], - output_filepath: Path, -): - fig, axs = plt.subplots(2, 1, sharex=True, sharey=True) - fig.suptitle("Brightness Across Frames") - - axs[0].set_ylabel("Brightness") - axs[1].set_ylabel("Brightness") - axs[1].set_xlabel("Time (s)") - - axs[0].set_title("Before Cross Correlation") - axs[1].set_title("After Cross Correlation") - - for brightness_npys in raw_brightness_npys: - brightness_array = np.load(brightness_npys) - - time = np.linspace(0, len(brightness_array), num=len(brightness_array)) - - axs[0].plot(time, brightness_array, alpha=0.5) - - for brightness_npys in trimmed_brightness_npys: - brightness_array = np.load(brightness_npys) - - time = np.linspace(0, len(brightness_array), num=len(brightness_array)) - - axs[1].plot(time, brightness_array, alpha=0.5) - - logger.info(f"Saving debug plots to: {output_filepath}") - plt.savefig(output_filepath) - - -def plot_audio_waveforms( - raw_audio_filepath_list: List[Path], - trimmed_audio_filepath_list: List[Path], - output_filepath: Path, -): - fig, axs = plt.subplots(2, 1, sharex=True, sharey=True) - fig.suptitle("Audio Cross Correlation Debug") - - axs[0].set_ylabel("Amplitude") - axs[1].set_ylabel("Amplitude") - axs[1].set_xlabel("Time (s)") - - axs[0].set_title("Before Cross Correlation") - axs[1].set_title("After Cross Correlation") - - for audio_filepath in raw_audio_filepath_list: - audio_signal, sr = librosa.load(path=audio_filepath, sr=None) - - time = np.linspace(0, len(audio_signal) / sr, num=len(audio_signal)) - - axs[0].plot(time, audio_signal, alpha=0.4) - - for audio_filepath in trimmed_audio_filepath_list: - audio_signal, sr = librosa.load(path=audio_filepath, sr=None) - - time = np.linspace(0, len(audio_signal) / sr, num=len(audio_signal)) - - axs[1].plot(time, audio_signal, alpha=0.4) - - logger.info(f"Saving debug plots to: {output_filepath}") - plt.savefig(output_filepath) diff --git a/skelly_synchronize_old/core_processes/normalize_framerates.py b/skelly_synchronize_old/core_processes/normalize_framerates.py deleted file mode 100644 index 97ae8bb..0000000 --- a/skelly_synchronize_old/core_processes/normalize_framerates.py +++ /dev/null @@ -1,46 +0,0 @@ -from pathlib import Path -from typing import Dict, List, Optional -from skelly_synchronize_old.core_processes.video_functions.ffmpeg_functions import ( - normalize_framerates_in_video_ffmpeg, -) -from skelly_synchronize_old.system.file_extensions import VideoExtension -from skelly_synchronize_old.system.paths_and_file_names import NORMALIZED_VIDEOS_FOLDER_NAME - -from skelly_synchronize_old.utils.path_handling_utilities import create_directory - -standard_audio_sample_rate = 44100 - - -def normalize_framerates( - raw_video_folder_path: Path, - video_info_dict: Dict[str, dict], - fps_list: List[float], - audio_samplerate_list: Optional[List[float]] = None, -) -> Path: - """Normalize the frame rates of a list of videos. Also normalize audio sample rates, if given""" - normalized_videos_folder_path = create_directory( - parent_directory=raw_video_folder_path, - directory_name=NORMALIZED_VIDEOS_FOLDER_NAME, - ) - - fps_list.sort() - desired_fps = min(fps_list) - - if audio_samplerate_list: - audio_samplerate_list.sort() - desired_audio_sample_rate = min(audio_samplerate_list) - else: - desired_audio_sample_rate = standard_audio_sample_rate - - for video_dict in video_info_dict.values(): - normalize_framerates_in_video_ffmpeg( - input_video_pathstring=str(video_dict["video pathstring"]), - output_video_pathstring=str( - normalized_videos_folder_path - / f"{video_dict['camera name']}.{VideoExtension.MP4.value}" - ), - desired_fps=desired_fps, - desired_sample_rate=int(desired_audio_sample_rate), - ) - - return normalized_videos_folder_path diff --git a/skelly_synchronize_old/core_processes/video_functions/deffcode_functions.py b/skelly_synchronize_old/core_processes/video_functions/deffcode_functions.py deleted file mode 100644 index 2a88c63..0000000 --- a/skelly_synchronize_old/core_processes/video_functions/deffcode_functions.py +++ /dev/null @@ -1,83 +0,0 @@ -import json -import logging -import cv2 -from deffcode import FFdecoder, Sourcer - -from skelly_synchronize_old.core_processes.video_functions.ffmpeg_functions import ( - check_for_ffmpeg, -) - -tranposition_dictionary = { - 90.0: "transpose=cclock", - -270.0: "transpose=cclock", - -90.0: "transpose=clock", - 270.0: "transpose=clock", - 180.0: "transpose=cclock,transpose=cclock", - -180.0: "transpose=clock,transpose=clock", -} - -logger = logging.getLogger(__name__) - - -def trim_single_video_deffcode( - input_video_pathstring: str, - frame_list: list, - output_video_pathstring: str, -): - try: - ffmpeg_location = check_for_ffmpeg() - except FileNotFoundError: - ffmpeg_location = "" - - sourcer = Sourcer( - source=input_video_pathstring, custom_ffmpeg=ffmpeg_location - ).probe_stream() - metadata_dictionary = sourcer.retrieve_metadata() - - if metadata_dictionary["source_video_orientation"] != 0: - logging.info("Video has reversed metadata, changing FFmpeg transpose argument") - ffparams = { - "-ffprefixes": ["-noautorotate"], - "-vf": tranposition_dictionary[ - metadata_dictionary["source_video_orientation"] - ], - } - else: - ffparams = {} - - decoder = FFdecoder( - str(input_video_pathstring), - frame_format="bgr24", - custom_ffmpeg=ffmpeg_location, - verbose=False, - **ffparams, - ).formulate() - - metadata_dictionary = json.loads(decoder.metadata) - - fourcc = cv2.VideoWriter.fourcc(*"mp4v") - framerate = metadata_dictionary["output_framerate"] - framesize = tuple(metadata_dictionary["output_frames_resolution"]) - - video_writer_object = cv2.VideoWriter( - output_video_pathstring, fourcc, framerate, framesize - ) - - current_frame = 0 - written_frames = 0 - - for frame in decoder.generateFrame(): - if frame is None: - break - - if current_frame in frame_list: - video_writer_object.write(frame) - written_frames += 1 - - if written_frames == len(frame_list): - break - - current_frame += 1 - - decoder.terminate() - video_writer_object.release() diff --git a/skelly_synchronize_old/core_processes/video_functions/ffmpeg_functions.py b/skelly_synchronize_old/core_processes/video_functions/ffmpeg_functions.py deleted file mode 100644 index 68b3146..0000000 --- a/skelly_synchronize_old/core_processes/video_functions/ffmpeg_functions.py +++ /dev/null @@ -1,282 +0,0 @@ -import logging -import subprocess -import shutil -from pathlib import Path -from typing import Union - -from skelly_synchronize_old.system.file_extensions import AudioExtension - -logger = logging.getLogger(__name__) - -ffmpeg_string = "ffmpeg" -ffprobe_string = "ffprobe" - - -def check_for_ffmpeg() -> str: - ffmpeg_pathstring = shutil.which(ffmpeg_string) - if ffmpeg_pathstring is None: - raise FileNotFoundError( - "ffmpeg not found, please install ffmpeg and add it to your PATH" - ) - - return ffmpeg_pathstring - - -def check_for_ffprobe(): - if shutil.which(ffprobe_string) is None: - raise FileNotFoundError( - "ffprobe not found, please install ffmpeg and add it to your PATH" - ) - - -def parse_ffmpeg_output(output: str, file_pathstring: str) -> float: - cleaned_out = ( - str(output) - .replace("b'", "") - .replace("'", "") - .replace("\\n", "") - .replace("\\r", "") - .replace("\\t", "") - .replace("\\", "") - ) - - try: - output_as_float = float(cleaned_out) - except (ValueError, RuntimeError): - split_str = str(cleaned_out).split("/") - if len(split_str) == 2: - output_as_float = float(int(split_str[0])) / float((split_str[1])) - else: - raise RuntimeError( - f"Unable to parse duration {output} from video {file_pathstring}" - ) - - return output_as_float - - -def extract_audio_from_video_ffmpeg( - file_pathstring: str, output_file_path: Union[Path, str] -): - """Run a subprocess call to extract the audio from a video file using ffmpeg""" - check_for_ffmpeg() - if str(Path(output_file_path).suffix).strip(".") not in { - extension.value for extension in AudioExtension - }: - raise ValueError( - f"output path {Path(output_file_path).suffix} is not a valid audio extension, extracting audio requires a valid audio extension" - ) - - extract_audio_subprocess = subprocess.run( - [ - ffmpeg_string, - "-y", - "-i", - file_pathstring, - str(output_file_path), - ], - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - ) - - if extract_audio_subprocess.returncode != 0: - raise RuntimeError( - f"Unable to extract audio from video file {file_pathstring}, check that video has audio" - ) - - -def extract_video_duration_ffmpeg(file_pathstring: str): - """Run a subprocess call to get the duration from a video file using ffmpeg""" - extract_duration_subprocess = subprocess.run( - [ - ffprobe_string, - "-v", - "error", - "-select_streams", - "v:0", - "-show_entries", - "format=duration", - "-of", - "default=noprint_wrappers=1:nokey=1", - file_pathstring, - ], - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - ) - - if extract_duration_subprocess.returncode != 0: - raise RuntimeError( - f"extract duration subprocess failed for video {file_pathstring} with return code {extract_duration_subprocess.returncode}" - ) - - video_duration = parse_ffmpeg_output( - str(extract_duration_subprocess.stdout), file_pathstring - ) - - return video_duration - - -def extract_video_fps_ffmpeg(file_pathstring: str): - """Run a subprocess call to get the fps of a video file using ffmpeg""" - - check_for_ffprobe() - extract_fps_subprocess = subprocess.run( - [ - ffprobe_string, - "-v", - "error", - "-select_streams", - "v:0", - "-show_entries", - "stream=r_frame_rate", - "-of", - "default=noprint_wrappers=1:nokey=1", - file_pathstring, - ], - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - ) - if extract_fps_subprocess.returncode != 0: - raise RuntimeError( - f"extract fps subprocess failed for video {file_pathstring} with return code {extract_fps_subprocess.returncode}" - ) - - video_fps = parse_ffmpeg_output(str(extract_fps_subprocess.stdout), file_pathstring) - - return video_fps - - -def extract_audio_sample_rate_ffmpeg(file_pathstring: str): - """Run a subprocess call to get the audio sample rate of a video file using ffmpeg""" - - check_for_ffprobe() - extract_sample_rate_subprocess = subprocess.run( - [ - ffprobe_string, - "-v", - "error", - "-select_streams", - "a:0", - "-show_entries", - "stream=sample_rate", - "-of", - "default=noprint_wrappers=1:nokey=1", - file_pathstring, - ], - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - ) - if extract_sample_rate_subprocess.returncode != 0: - raise RuntimeError( - f"extract sample rate subprocess failed for video {file_pathstring} with return code {extract_sample_rate_subprocess.returncode}" - ) - - if str(extract_sample_rate_subprocess.stdout) == "": - raise ValueError( - f"No audio file found for video {file_pathstring}, check that video has audio" - ) - - audio_sample_rate = parse_ffmpeg_output( - str(extract_sample_rate_subprocess.stdout), file_pathstring - ) - - return audio_sample_rate - - -def normalize_framerates_in_video_ffmpeg( - input_video_pathstring: str, - output_video_pathstring: str, - desired_fps: float = 30, - desired_sample_rate: int = 44100, -): - """Run a subprocess call to normalize the framerate and audio sample rate of a video file using ffmpeg""" - - check_for_ffmpeg() - normalize_framerates_subprocess = subprocess.run( - [ - ffmpeg_string, - "-i", - f"{input_video_pathstring}", - "-r", - f"{desired_fps}", - "-ar", - f"{desired_sample_rate}", - "-y", - f"{output_video_pathstring}", - ] - ) - - if normalize_framerates_subprocess.returncode != 0: - raise RuntimeError( - f"Error normalizing video framerate in {input_video_pathstring} with target fps {desired_fps} and target sample rate {desired_sample_rate}, ffmpeg returned code {normalize_framerates_subprocess.returncode}" - ) - - -def trim_single_video_ffmpeg( - input_video_pathstring: str, - start_time: float, - desired_duration: float, - output_video_pathstring: str, -): - """Run a subprocess call to trim a video from start time to last as long as the desired duration""" - check_for_ffmpeg() - trim_video_subprocess = subprocess.run( - [ - ffmpeg_string, - "-i", - f"{input_video_pathstring}", - "-ss", - f"{start_time}", - "-t", - f"{desired_duration}", - "-y", - f"{output_video_pathstring}", - ], - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - ) - - if trim_video_subprocess.returncode != 0: - raise RuntimeError( - f"trim video subprocess failed for video {input_video_pathstring} with return code {trim_video_subprocess.returncode}" - ) - - -def attach_audio_to_video_ffmpeg( - input_video_pathstring: str, - audio_file_pathstring: str, - output_video_pathstring: str, -): - """Run a subprocess call to attach audio file back to the video""" - - check_for_ffmpeg() - attach_audio_subprocess = subprocess.run( - [ - ffmpeg_string, - "-i", - f"{input_video_pathstring}", - "-i", - f"{audio_file_pathstring}", - "-c:v", - "copy", - "-c:a", - "aac", - f"{output_video_pathstring}", - ], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - - if attach_audio_subprocess.returncode != 0: - raise RuntimeError( - f"Error occurred attaching audio to video {input_video_pathstring} with return code {attach_audio_subprocess.returncode}" - ) - - -if __name__ == "__main__": - video_path = "" - - print(f"video duration: {extract_video_duration_ffmpeg(video_path)}") - - print(f"video fps: {extract_video_fps_ffmpeg(video_path)}") - - print(f"audio sample rate: {extract_audio_sample_rate_ffmpeg(video_path)}") diff --git a/skelly_synchronize_old/core_processes/video_functions/video_utilities.py b/skelly_synchronize_old/core_processes/video_functions/video_utilities.py deleted file mode 100644 index cddd5ea..0000000 --- a/skelly_synchronize_old/core_processes/video_functions/video_utilities.py +++ /dev/null @@ -1,210 +0,0 @@ -import logging -import multiprocessing -import tempfile -import shutil -from pathlib import Path -from typing import Dict - -from skelly_synchronize_old.core_processes.audio_utilities import trim_audio_files -from skelly_synchronize_old.core_processes.video_functions.deffcode_functions import ( - trim_single_video_deffcode, -) -from skelly_synchronize_old.core_processes.video_functions.ffmpeg_functions import ( - attach_audio_to_video_ffmpeg, - extract_video_duration_ffmpeg, - extract_video_fps_ffmpeg, - trim_single_video_ffmpeg, -) -from skelly_synchronize_old.system.file_extensions import AudioExtension, VideoExtension -from skelly_synchronize_old.utils.get_video_files import get_video_file_list -from skelly_synchronize_old.utils.path_handling_utilities import ( - name_synced_video, -) - -logger = logging.getLogger(__name__) - - -def create_video_info_dict( - video_filepath_list: list, video_handler: str = "ffmpeg" -) -> Dict[str, dict]: - """Get a dictionary with video information from the given video file paths.""" - video_info_dict = dict() - for video_filepath in video_filepath_list: - video_dict = dict() - video_dict["video filepath"] = Path(video_filepath) - video_dict["video pathstring"] = str(video_filepath) - video_name = Path(video_filepath).stem - video_dict["camera name"] = video_name - - if video_handler == "ffmpeg": - video_dict["video duration"] = extract_video_duration_ffmpeg( - file_pathstring=str(video_filepath) - ) - video_dict["video fps"] = extract_video_fps_ffmpeg( - file_pathstring=str(video_filepath) - ) - - video_info_dict[video_name] = video_dict - - return video_info_dict - - -def trim_videos( - video_info_dict: Dict[str, dict], - synchronized_folder_path: Path, - lag_dict: Dict[str, float], - fps: float, - video_handler: str = "deffcode", -) -> None: - """Take a list of video files and a list of lags, and make all videos start and end at the same time.""" - - if video_handler not in ["ffmpeg", "deffcode"]: - raise ValueError("video_handler must be either 'ffmpeg' or 'deffcode'") - - minimum_duration = find_minimum_video_duration( - video_info_dict=video_info_dict, lag_dict=lag_dict - ) - minimum_frames = int(minimum_duration * fps) - - max_processes = min(len(video_info_dict), multiprocessing.cpu_count() - 1) - - with multiprocessing.Pool(processes=max_processes) as pool: - pool.starmap( - trim_single_video, - [ - ( - video_dict, - synchronized_folder_path, - minimum_duration, - minimum_frames, - lag_dict, - fps, - video_handler, - ) - for video_dict in video_info_dict.values() - ], - ) - - -def trim_single_video( - video_dict: dict, - synchronized_folder_path: Path, - minimum_duration: float, - minimum_frames: int, - lag_dict: Dict[str, float], - fps: float, - video_handler: str = "deffcode", -) -> None: - """Take a list of video files and a list of lags, and make all videos start and end at the same time.""" - - try: - logger.debug(f"trimming video file {video_dict['camera name']}") - synced_video_name = name_synced_video( - raw_video_filename=video_dict["camera name"] - ) - - start_time = lag_dict[video_dict["camera name"]] - start_frame = int(start_time * fps) - frame_list = get_frame_list( - start_frame=start_frame, duration_frames=minimum_frames - ) - - if video_handler == "ffmpeg": - logger.info( - f"Saving video - Cam name: {video_dict['camera name']} - target duration: {minimum_duration} seconds" - ) - trim_single_video_ffmpeg( - input_video_pathstring=video_dict["video pathstring"], - start_time=start_time, - desired_duration=minimum_duration, - output_video_pathstring=str( - synchronized_folder_path / synced_video_name - ), - ) - logger.info( - f"Video Saved - Cam name: {video_dict['camera name']}, Video Duration in Seconds: {minimum_duration}" - ) - if video_handler == "deffcode": - logger.info( - f"Saving video - Cam name: {video_dict['camera name']} - start frame: {start_frame} - target duration: {minimum_frames} frames" - ) - trim_single_video_deffcode( - input_video_pathstring=video_dict["video pathstring"], - frame_list=frame_list, - output_video_pathstring=str( - synchronized_folder_path / synced_video_name - ), - ) - logger.info( - f"Video Saved - Cam name: {video_dict['camera name']}, Video Duration in Frames: {minimum_frames}" - ) - except Exception as e: - logger.error( - f"Error trimming video {video_dict['camera name']}: {e}", - exc_info=True, - ) - raise e - - -def get_fps_list(video_info_dict: Dict[str, dict]): - """Get list of the frames per second in each video""" - return [video_dict["video fps"] for video_dict in video_info_dict.values()] - - -def get_frame_list(start_frame: int, duration_frames: int) -> list: - """Get a list of frame numbers for video to be trimmed to""" - return [start_frame + frame for frame in range(duration_frames)] - - -def find_minimum_video_duration( - video_info_dict: Dict[str, dict], lag_dict: dict -) -> float: - """Take a list of video files and a list of lags, and find what the shortest video is starting from each videos lag offset""" - - min_duration = min( - [ - video_dict["video duration"] - lag_dict[video_dict["camera name"]] - for video_dict in video_info_dict.values() - ] - ) - - return min_duration - - -def attach_audio_to_videos( - synchronized_video_folder_path: Path, - audio_folder_path: Path, - lag_dictionary: dict, - synchronized_video_length: float, -): - trimmed_audio_folder_path = trim_audio_files( - audio_folder_path=audio_folder_path, - lag_dictionary=lag_dictionary, - synced_video_length=synchronized_video_length, - ) - - with tempfile.TemporaryDirectory( - dir=str(synchronized_video_folder_path) - ) as temp_dir: - for video in get_video_file_list(synchronized_video_folder_path): - video_name = video.stem - if video_name.startswith("synced_"): - audio_filename = f"{str(video_name).split('_', maxsplit=1)[-1]}.{AudioExtension.WAV.value}" - else: - audio_filename = f"{video_name}.{AudioExtension.WAV.value}" - output_video_pathstring = str( - Path(temp_dir) - / f"{video_name}_with_audio_temp.{VideoExtension.MP4.value}" - ) - - logger.info(f"Attaching audio to video {video_name}") - attach_audio_to_video_ffmpeg( - input_video_pathstring=str(video), - audio_file_pathstring=str( - Path(trimmed_audio_folder_path) / audio_filename - ), - output_video_pathstring=output_video_pathstring, - ) - - # overwrite synced video with video containing audio - shutil.move(output_video_pathstring, video) diff --git a/skelly_synchronize_old/gui/skelly_synchronize_gui.py b/skelly_synchronize_old/gui/skelly_synchronize_gui.py deleted file mode 100644 index 40da6c9..0000000 --- a/skelly_synchronize_old/gui/skelly_synchronize_gui.py +++ /dev/null @@ -1,98 +0,0 @@ -from pathlib import Path -from PySide6.QtGui import QDoubleValidator -from PySide6.QtWidgets import ( - QApplication, - QWidget, - QVBoxLayout, - QPushButton, - QFileDialog, - QMainWindow, - QLabel, - QLineEdit, - QHBoxLayout, -) - -from skelly_synchronize_old.skelly_synchronize import ( - synchronize_videos_from_audio, - synchronize_videos_from_brightness, -) - - -class MainWindow(QMainWindow): - def __init__(self): - super().__init__() - self._folder_path = None - - self.setGeometry(100, 100, 600, 300) - - widget = QWidget() - self._layout = QVBoxLayout() - widget.setLayout(self._layout) - self.setCentralWidget(widget) - - self.folder_open_button = QPushButton("Load folder of raw videos") - self._layout.addWidget(self.folder_open_button) - self.folder_open_button.clicked.connect(self._open_session_folder_dialog) - - self.folder_path_label = QLabel( - f"Selected folder of raw videos: {self._folder_path}" - ) - self.folder_path_label.setFixedHeight(15) - self._layout.addWidget(self.folder_path_label) - - self.run_audio_synch_button = QPushButton( - "Synchronize videos with Audio Cross Correlation" - ) - self.run_audio_synch_button.setEnabled(False) - self._layout.addWidget(self.run_audio_synch_button) - self.run_audio_synch_button.clicked.connect( - lambda: synchronize_videos_from_audio( - raw_video_folder_path=self._folder_path - ) - ) - - self.run_brightness_synch_button = QPushButton( - "Synchronize videos with First Brightness Change" - ) - self.run_brightness_synch_button.setEnabled(False) - self._layout.addWidget(self.run_brightness_synch_button) - - hbox = QHBoxLayout() - brightness_threshold_default = 1000 - hbox.addWidget(QLabel("Brightness ratio threshold: ")) - self.brightness_threshold_lineedit = QLineEdit() - self.brightness_threshold_lineedit.setText(str(brightness_threshold_default)) - validator = QDoubleValidator() - validator.setBottom(1) - self.brightness_threshold_lineedit.setValidator(validator) - hbox.addWidget(self.brightness_threshold_lineedit) - self._layout.addLayout(hbox) - - self.run_brightness_synch_button.clicked.connect( - lambda: synchronize_videos_from_brightness( - raw_video_folder_path=self._folder_path, - brightness_ratio_threshold=float( - self.brightness_threshold_lineedit.text() - ), - ) - ) - - def _open_session_folder_dialog(self): - folder_input = QFileDialog.getExistingDirectory(None, "Choose a folder") - self._folder_path = Path(folder_input) - self.folder_path_label.setText( - f"Selected folder of raw videos: {self._folder_path}" - ) - self.run_audio_synch_button.setEnabled(True) - self.run_brightness_synch_button.setEnabled(True) - - -def main(): - app = QApplication([]) - win = MainWindow() - win.show() - app.exec() - - -if __name__ == "__main__": - main() diff --git a/skelly_synchronize_old/gui/widgets/run_button_widget.py b/skelly_synchronize_old/gui/widgets/run_button_widget.py deleted file mode 100644 index 7005e32..0000000 --- a/skelly_synchronize_old/gui/widgets/run_button_widget.py +++ /dev/null @@ -1,16 +0,0 @@ -from PySide6.QtWidgets import QWidget, QVBoxLayout, QPushButton - - -class RunButtonWidget(QWidget): - def __init__(self): - super().__init__() - - self._layout = QVBoxLayout() - self.setLayout = self._layout - - self.run_button_widget = QPushButton("Run", self) - - # self.run_button_widget.clicked.connect(self.run_script) - - def run_script(self): - print("Running a print statement!") diff --git a/skelly_synchronize_old/skelly_synchronize.py b/skelly_synchronize_old/skelly_synchronize.py deleted file mode 100644 index d5d1dc4..0000000 --- a/skelly_synchronize_old/skelly_synchronize.py +++ /dev/null @@ -1,278 +0,0 @@ -import time -import logging -from pathlib import Path -from typing import Optional -from skelly_synchronize_old.core_processes.debugging.debug_plots import ( - create_audio_debug_plots, - create_brightness_debug_plots, -) -from skelly_synchronize_old.core_processes.normalize_framerates import normalize_framerates - -from skelly_synchronize_old.utils.get_video_files import get_video_file_list -from skelly_synchronize_old.core_processes.audio_utilities import ( - extract_audio_files, - get_audio_sample_rates, -) -from skelly_synchronize_old.core_processes.correlation_functions import ( - find_brightest_point_lags, - find_brightness_across_frames, - find_cross_correlation_lags, -) -from skelly_synchronize_old.core_processes.video_functions.video_utilities import ( - attach_audio_to_videos, - get_fps_list, - create_video_info_dict, - trim_videos, -) -from skelly_synchronize_old.core_processes.debugging.debug_output import ( - remove_audio_files_from_audio_signal_dict, - save_dictionaries_to_toml, -) -from skelly_synchronize_old.utils.path_handling_utilities import ( - create_directory, -) -from skelly_synchronize_old.tests.utilities.check_list_values_are_equal import ( - check_list_values_are_equal, -) -from skelly_synchronize_old.tests.utilities.get_number_of_frames_of_videos_in_a_folder import ( - get_number_of_frames_of_videos_in_a_folder, -) -from skelly_synchronize_old.system.paths_and_file_names import ( - AUDIO_NAME, - DEBUG_TOML_NAME, - LAG_DICTIONARY_NAME, - NORMALIZED_VIDEOS_FOLDER_NAME, - RAW_VIDEO_NAME, - SYNCHRONIZED_VIDEO_NAME, - SYNCHRONIZED_VIDEOS_FOLDER_NAME, - AUDIO_FILES_FOLDER_NAME, -) -from skelly_synchronize_old.system.file_extensions import AudioExtension - -logger = logging.getLogger(__name__) - - -def synchronize_videos_from_audio( - raw_video_folder_path: Path, - synchronized_video_folder_path: Optional[Path] = None, - video_handler: str = "deffcode", - create_debug_plots_bool: bool = True, -): - """Synchronize all videos in the base path folder using audio cross correlation. - Uses deffcode and to handle the video files as default, set "video_handler" to "ffmpeg" to use ffmpeg methods instead. - ffmpeg is used to get audio from the video files with either method. - - Returns the folder path of the synchronized video folder. - """ - start_timer = time.time() - - video_file_list = get_video_file_list(folder_path=raw_video_folder_path) - if synchronized_video_folder_path is None: - synchronized_video_folder_path = create_directory( - parent_directory=raw_video_folder_path.parent, - directory_name=SYNCHRONIZED_VIDEOS_FOLDER_NAME, - ) - synchronized_video_folder_path = Path(synchronized_video_folder_path) - - audio_folder_path = create_directory( - parent_directory=synchronized_video_folder_path, - directory_name=AUDIO_FILES_FOLDER_NAME, - ) - - # create dictionaries with video and audio information - video_info_dict = create_video_info_dict( - video_filepath_list=video_file_list, video_handler="ffmpeg" - ) - - # get video fps and audio sample rate - fps_list = get_fps_list(video_info_dict=video_info_dict) - audio_sample_rates = get_audio_sample_rates(video_info_dict=video_info_dict) - - if len(set(fps_list)) > 1 or len(set(audio_sample_rates)) > 1: - normalized_video_folder_path = normalize_framerates( - raw_video_folder_path=raw_video_folder_path, - video_info_dict=video_info_dict, - fps_list=fps_list, - audio_samplerate_list=audio_sample_rates, - ) - - video_file_list = get_video_file_list(folder_path=normalized_video_folder_path) - - video_info_dict = create_video_info_dict( - video_filepath_list=video_file_list, video_handler="ffmpeg" - ) - - fps_list = get_fps_list(video_info_dict=video_info_dict) - audio_sample_rates = get_audio_sample_rates(video_info_dict=video_info_dict) - - audio_signal_dict = extract_audio_files( - video_info_dict=video_info_dict, - audio_extension=AudioExtension.WAV, - audio_folder_path=audio_folder_path, - ) - - # frame rates and audio sample rates must be the same duration for the trimming process to work correctly - fps = check_list_values_are_equal(input_list=fps_list) - audio_sample_rate = check_list_values_are_equal(input_list=audio_sample_rates) - - # find the lags between starting times - lag_dict = find_cross_correlation_lags( - audio_signal_dict=audio_signal_dict, sample_rate=audio_sample_rate - ) - - trim_videos( - video_info_dict=video_info_dict, - synchronized_folder_path=synchronized_video_folder_path, - lag_dict=lag_dict, - fps=fps, - video_handler=video_handler, - ) - - synchronized_video_framecounts = get_number_of_frames_of_videos_in_a_folder( - folder_path=synchronized_video_folder_path - ) - logger.info( - f"All videos are {check_list_values_are_equal(synchronized_video_framecounts)} frames long" - ) - - synchronized_video_info_dict = create_video_info_dict( - video_filepath_list=get_video_file_list(synchronized_video_folder_path) - ) - - save_dictionaries_to_toml( - input_dictionaries={ - RAW_VIDEO_NAME: video_info_dict, - SYNCHRONIZED_VIDEO_NAME: synchronized_video_info_dict, - AUDIO_NAME: remove_audio_files_from_audio_signal_dict( - audio_signal_dictionary=audio_signal_dict - ), - LAG_DICTIONARY_NAME: lag_dict, - }, - output_file_path=synchronized_video_folder_path / DEBUG_TOML_NAME, - ) - - attach_audio_to_videos( - synchronized_video_folder_path=synchronized_video_folder_path, - audio_folder_path=audio_folder_path, - lag_dictionary=lag_dict, - synchronized_video_length=next(iter(synchronized_video_info_dict.values()))[ - "video duration" - ], - ) - if create_debug_plots_bool: - create_audio_debug_plots( - synchronized_video_folder_path=synchronized_video_folder_path - ) - - end_timer = time.time() - - logger.info(f"Elapsed processing time in seconds: {end_timer - start_timer}") - - return synchronized_video_folder_path - - -def synchronize_videos_from_brightness( - raw_video_folder_path: Path, - synchronized_video_folder_path: Optional[Path] = None, - video_handler: str = "deffcode", - brightness_ratio_threshold: float = 1000, - create_debug_plots_bool: bool = True, -): - """Synchronize all videos in the base path folder using the first frame in each video with a high change in brightness between frames. - Uses deffcode and to handle the video files as default, set "video_handler" to "ffmpeg" to use ffmpeg methods instead. - - Returns the folder path of the synchronized video folder. - """ - start_timer = time.time() - - logger.info( - f"Synchronizing videos with a brightness ratio threshold of {brightness_ratio_threshold}" - ) - - video_file_list = get_video_file_list(folder_path=raw_video_folder_path) - if synchronized_video_folder_path is None: - synchronized_video_folder_path = create_directory( - parent_directory=raw_video_folder_path.parent, - directory_name=SYNCHRONIZED_VIDEOS_FOLDER_NAME, - ) - synchronized_video_folder_path = Path(synchronized_video_folder_path) - - # create dictionaries with video - video_info_dict = create_video_info_dict( - video_filepath_list=video_file_list, video_handler="ffmpeg" - ) - - # get video fps - fps_list = get_fps_list(video_info_dict=video_info_dict) - - if len(set(fps_list)) > 1: - normalized_video_folder_path = normalize_framerates( - raw_video_folder_path=raw_video_folder_path, - video_info_dict=video_info_dict, - fps_list=fps_list, - ) - - video_file_list = get_video_file_list(folder_path=normalized_video_folder_path) - - video_info_dict = create_video_info_dict( - video_filepath_list=video_file_list, video_handler="ffmpeg" - ) - - fps_list = get_fps_list(video_info_dict=video_info_dict) - - # frame rates must be the same duration for the trimming process to work correctly - fps = check_list_values_are_equal(input_list=fps_list) - - # find the lags between starting times - lag_dict = find_brightest_point_lags( - video_info_dict=video_info_dict, - frame_rate=fps, - brightness_ratio_threshold=brightness_ratio_threshold, - ) - - trim_videos( - video_info_dict=video_info_dict, - synchronized_folder_path=synchronized_video_folder_path, - lag_dict=lag_dict, - fps=fps, - video_handler=video_handler, - ) - - synchronized_video_framecounts = get_number_of_frames_of_videos_in_a_folder( - folder_path=synchronized_video_folder_path - ) - logger.info( - f"All videos are {check_list_values_are_equal(synchronized_video_framecounts)} frames long" - ) - - synchronized_video_info_dict = create_video_info_dict( - video_filepath_list=get_video_file_list(synchronized_video_folder_path) - ) - - save_dictionaries_to_toml( - input_dictionaries={ - RAW_VIDEO_NAME: video_info_dict, - SYNCHRONIZED_VIDEO_NAME: synchronized_video_info_dict, - LAG_DICTIONARY_NAME: lag_dict, - }, - output_file_path=synchronized_video_folder_path / DEBUG_TOML_NAME, - ) - - for video_dict in synchronized_video_info_dict.values(): - find_brightness_across_frames(video_pathstring=video_dict["video pathstring"]) - - if create_debug_plots_bool: - if Path(raw_video_folder_path / NORMALIZED_VIDEOS_FOLDER_NAME).exists: - path_to_npys = raw_video_folder_path / NORMALIZED_VIDEOS_FOLDER_NAME - else: - path_to_npys = raw_video_folder_path - create_brightness_debug_plots( - raw_video_folder_path=path_to_npys, - synchronized_video_folder_path=synchronized_video_folder_path, - ) - - end_timer = time.time() - - logger.info(f"Elapsed processing time in seconds: {end_timer - start_timer}") - - return synchronized_video_folder_path diff --git a/skelly_synchronize_old/system/__init__.py b/skelly_synchronize_old/system/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/skelly_synchronize_old/system/default_paths.py b/skelly_synchronize_old/system/default_paths.py deleted file mode 100644 index 09bb41c..0000000 --- a/skelly_synchronize_old/system/default_paths.py +++ /dev/null @@ -1,48 +0,0 @@ -from datetime import datetime -import time -from pathlib import Path - -from skelly_synchronize_old import __package_name__ - -BASE_FOLDER_NAME = f"{__package_name__}_data" -LOGS_INFO_AND_SETTINGS_FOLDER_NAME = "logs_info_and_settings" -LOG_FILE_FOLDER_NAME = "logs" - - -def get_base_folder_path(): - base_folder = Path().home() / BASE_FOLDER_NAME - base_folder.mkdir(exist_ok=True, parents=True) - return base_folder - - -def get_log_file_path(): - log_file_path = ( - get_base_folder_path() - / LOGS_INFO_AND_SETTINGS_FOLDER_NAME - / LOG_FILE_FOLDER_NAME - / create_log_file_name() - ) - log_file_path.parent.mkdir(exist_ok=True, parents=True) - return log_file_path - - -def create_log_file_name(): - return "log_" + get_iso6201_time_string() + ".log" - - -def get_gmt_offset_string(): - # from - https://stackoverflow.com/a/53860920/14662833 - gmt_offset_int = int(time.localtime().tm_gmtoff / 60 / 60) - return f"{gmt_offset_int:+}" - - -def get_iso6201_time_string( - timespec: str = "milliseconds", make_filename_friendly: bool = True -): - iso6201_timestamp = datetime.now().isoformat(timespec=timespec) - gmt_offset_string = f"_gmt{get_gmt_offset_string()}" - iso6201_timestamp_w_gmt = iso6201_timestamp + gmt_offset_string - if make_filename_friendly: - iso6201_timestamp_w_gmt = iso6201_timestamp_w_gmt.replace(":", "_") - iso6201_timestamp_w_gmt = iso6201_timestamp_w_gmt.replace(".", "ms") - return iso6201_timestamp_w_gmt diff --git a/skelly_synchronize_old/system/file_extensions.py b/skelly_synchronize_old/system/file_extensions.py deleted file mode 100644 index c77c698..0000000 --- a/skelly_synchronize_old/system/file_extensions.py +++ /dev/null @@ -1,18 +0,0 @@ -from enum import Enum - -NUMPY_EXTENSION = "npy" - - -class AudioExtension(Enum): - WAV = "wav" - FLAC = "flac" - MP3 = "mp3" - AAC = "aac" - - -class VideoExtension(Enum): - MP4 = "mp4" - MKV = "mkv" - AVI = "avi" - MPEG = "mpeg" - MOV = "mov" diff --git a/skelly_synchronize_old/system/logging_configuration.py b/skelly_synchronize_old/system/logging_configuration.py deleted file mode 100644 index dfc12a9..0000000 --- a/skelly_synchronize_old/system/logging_configuration.py +++ /dev/null @@ -1,42 +0,0 @@ -import logging -import logging.handlers -import sys -from logging.config import dictConfig -from typing import Optional - - -DEFAULT_LOGGING = {"version": 1, "disable_existing_loggers": False} - - -def get_logging_handlers(log_file_path: Optional[str] = ""): - dictConfig(DEFAULT_LOGGING) - - default_formatter = logging.Formatter( - "[%(asctime)s.%(msecs)04d] [%(levelname)8s] [%(name)s] [%(funcName)s():%(lineno)s] [PID:%(process)d " - "TID:%(thread)d] %(message)s", - "%Y-%m-%d %H:%M:%S", - ) - - console_handler = logging.StreamHandler(sys.stdout) - console_handler.setLevel(logging.INFO) - console_handler.setFormatter(default_formatter) - handlers = [console_handler] - if log_file_path: - file_handler = logging.FileHandler(log_file_path) - file_handler.setFormatter(default_formatter) - file_handler.setLevel(logging.INFO) - handlers.append(file_handler) - - return handlers - - -def configure_logging(log_file_path: Optional[str] = ""): - if len(logging.getLogger().handlers) == 0: - handlers = get_logging_handlers(log_file_path) - logging.getLogger("").handlers.extend(handlers) - logging.root.setLevel(logging.INFO) - logger = logging.getLogger(__name__) - logger.info(f"Added logging handlers: {handlers}") - else: - logger = logging.getLogger(__name__) - logger.info("Logging already configured!") diff --git a/skelly_synchronize_old/system/paths_and_file_names.py b/skelly_synchronize_old/system/paths_and_file_names.py deleted file mode 100644 index 0baf3bf..0000000 --- a/skelly_synchronize_old/system/paths_and_file_names.py +++ /dev/null @@ -1,24 +0,0 @@ -# directory names -SYNCHRONIZED_VIDEOS_FOLDER_NAME = "synchronized_videos" -RAW_VIDEOS_FOLDER_NAME = "raw_videos" -AUDIO_FILES_FOLDER_NAME = "audio_files" -TRIMMED_AUDIO_FOLDER_NAME = "trimmed_audio" -NORMALIZED_VIDEOS_FOLDER_NAME = "normalized_videos" - -# file names -DEBUG_TOML_NAME = "synchronization_debug.toml" -DEBUG_PLOT_NAME = "debug_plot.png" - -# debug dictionary keys -RAW_VIDEO_NAME = "Raw_video_information" -SYNCHRONIZED_VIDEO_NAME = "Synchronized_video_information" -AUDIO_NAME = "Audio_information" -LAG_DICTIONARY_NAME = "Lag_dictionary" - -# figshare info -FIGSHARE_ZIP_FILE_URL = "https://figshare.com/ndownloader/files/41066489" -FIGSHARE_SAMPLE_DATA_FILE_NAME = "skelly_synchronize_sample_data" - -# string constants -SYNCED_VIDEO_PRECURSOR = "synced_" -BRIGHTNESS_SUFFIX = "_brightness" diff --git a/skelly_synchronize_old/tests/conftest.py b/skelly_synchronize_old/tests/conftest.py deleted file mode 100644 index 430eeec..0000000 --- a/skelly_synchronize_old/tests/conftest.py +++ /dev/null @@ -1,50 +0,0 @@ -import sys -import pytest -from pathlib import Path - -print("Thank you for using skelly_synchronize!") -print(f"This is printing from: {__file__}") - -base_package_path = Path(__file__).parent.parent.parent -print(f"adding base_package_path: {base_package_path} : to sys.path") -sys.path.insert(0, str(base_package_path)) # add parent directory to sys.path - -from skelly_synchronize_old.skelly_synchronize import synchronize_videos_from_audio -from skelly_synchronize_old.tests.utilities.load_sample_data import ( - find_raw_videos_folder_path, - load_sample_data, -) -from skelly_synchronize_old.utils.get_video_files import get_video_file_list - - -def pytest_sessionstart(): - pytest.sample_session_folder_path = load_sample_data() - pytest.raw_video_folder_path = find_raw_videos_folder_path( - pytest.sample_session_folder_path - ) - pytest.synchronized_video_folder_path = synchronize_videos_from_audio( - pytest.raw_video_folder_path - ) - pytest.video_file_list = get_video_file_list( - folder_path=pytest.synchronized_video_folder_path - ) - - -@pytest.fixture -def raw_video_folder_path(): - return pytest.raw_video_folder_path - - -@pytest.fixture -def synchronized_video_folder_path(): - return pytest.synchronized_video_folder_path - - -@pytest.fixture -def test_video_pathstring(): - return str(pytest.video_file_list[0]) - - -@pytest.fixture -def output_video_pathstring(): - return str(pytest.sample_session_folder_path / "trimmed_sample_video.mp4") diff --git a/skelly_synchronize_old/tests/test_all_files_created.py b/skelly_synchronize_old/tests/test_all_files_created.py deleted file mode 100644 index 2e21cb9..0000000 --- a/skelly_synchronize_old/tests/test_all_files_created.py +++ /dev/null @@ -1,45 +0,0 @@ -import pytest -from pathlib import Path -from typing import Union - -from skelly_synchronize_old.system.paths_and_file_names import ( - AUDIO_FILES_FOLDER_NAME, - DEBUG_PLOT_NAME, - DEBUG_TOML_NAME, - TRIMMED_AUDIO_FOLDER_NAME, -) - - -@pytest.mark.usefixtures("synchronized_video_folder_path") -def test_synchronized_video_folder_exists( - synchronized_video_folder_path: Union[str, Path], -): - assert synchronized_video_folder_path.exists() - - -@pytest.mark.usefixtures("synchronized_video_folder_path") -def test_debug_plot_exists(synchronized_video_folder_path: Union[str, Path]): - debug_plot_filepath = Path(synchronized_video_folder_path) / DEBUG_PLOT_NAME - assert debug_plot_filepath.exists() - - -@pytest.mark.usefixtures("synchronized_video_folder_path") -def test_audio_files_folder_exists(synchronized_video_folder_path: Union[str, Path]): - audio_files_folder = Path(synchronized_video_folder_path) / AUDIO_FILES_FOLDER_NAME - assert audio_files_folder.exists() - - -@pytest.mark.usefixtures("synchronized_video_folder_path") -def test_trimmed_audio_folder_exists(synchronized_video_folder_path: Union[str, Path]): - trimmed_audio_folder_filepath = ( - Path(synchronized_video_folder_path) - / AUDIO_FILES_FOLDER_NAME - / TRIMMED_AUDIO_FOLDER_NAME - ) - assert trimmed_audio_folder_filepath.exists() - - -@pytest.mark.usefixtures("synchronized_video_folder_path") -def test_debug_toml_exists(synchronized_video_folder_path: Union[str, Path]): - debug_toml_filepath = Path(synchronized_video_folder_path) / DEBUG_TOML_NAME - assert debug_toml_filepath.exists() diff --git a/skelly_synchronize_old/tests/test_normalize_lag_dict.py b/skelly_synchronize_old/tests/test_normalize_lag_dict.py deleted file mode 100644 index 1a266e6..0000000 --- a/skelly_synchronize_old/tests/test_normalize_lag_dict.py +++ /dev/null @@ -1,26 +0,0 @@ -import pytest - -from skelly_synchronize_old.core_processes.correlation_functions import ( - normalize_lag_dictionary, -) - - -@pytest.fixture -def lag_dict(): - return {"Cam1": 0.0, "Cam2": 4.3402267573696145, "Cam3": 9.475963718820863} - - -@pytest.fixture -def normalized_lag_dict(): - return {"Cam1": 9.475963718820863, "Cam2": 5.135736961451248, "Cam3": 0.0} - - -def test_normalize_lag_dict(lag_dict, normalized_lag_dict): - assert ( - normalize_lag_dictionary(lag_dict) == normalized_lag_dict - ), "Lag dict did not normalize correctly" - - -if __name__ == "__main__": - test_normalize_lag_dict(lag_dict, normalized_lag_dict) - print("tests passed") diff --git a/skelly_synchronize_old/tests/test_number_of_videos_is_preserved.py b/skelly_synchronize_old/tests/test_number_of_videos_is_preserved.py deleted file mode 100644 index 4497267..0000000 --- a/skelly_synchronize_old/tests/test_number_of_videos_is_preserved.py +++ /dev/null @@ -1,15 +0,0 @@ -import pytest -from typing import Union -from pathlib import Path - -from skelly_synchronize_old.utils.get_video_files import get_video_file_list - - -@pytest.mark.usefixtures("raw_video_folder_path", "synchronized_video_folder_path") -def test_number_of_videosis_preserved( - raw_video_folder_path: Union[str, Path], - synchronized_video_folder_path: Union[str, Path], -): - assert len(get_video_file_list(folder_path=raw_video_folder_path)) == len( - get_video_file_list(folder_path=synchronized_video_folder_path) - ) diff --git a/skelly_synchronize_old/tests/test_trim_single_video_deffcode.py b/skelly_synchronize_old/tests/test_trim_single_video_deffcode.py deleted file mode 100644 index f751681..0000000 --- a/skelly_synchronize_old/tests/test_trim_single_video_deffcode.py +++ /dev/null @@ -1,33 +0,0 @@ -import pytest -import logging - - -from skelly_synchronize_old.tests.utilities.find_frame_count_of_video import ( - find_frame_count_of_video, -) -from skelly_synchronize_old.core_processes.video_functions.deffcode_functions import ( - trim_single_video_deffcode, -) - - -@pytest.fixture -def frame_list(): - start_frame = 10 - duration_frames = 200 - return [start_frame + frame for frame in range(duration_frames)] - - -def test_trim_single_video_deffcode( - test_video_pathstring: str, - frame_list: list, - output_video_pathstring: str, - caplog, -): - caplog.set_level(logging.INFO) - trim_single_video_deffcode( - input_video_pathstring=test_video_pathstring, - frame_list=frame_list, - output_video_pathstring=output_video_pathstring, - ) - - assert find_frame_count_of_video(output_video_pathstring) == len(frame_list) diff --git a/skelly_synchronize_old/tests/test_videos_are_same_length.py b/skelly_synchronize_old/tests/test_videos_are_same_length.py deleted file mode 100644 index ff2a244..0000000 --- a/skelly_synchronize_old/tests/test_videos_are_same_length.py +++ /dev/null @@ -1,20 +0,0 @@ -import pytest -from pathlib import Path -from typing import Union - -from skelly_synchronize_old.tests.utilities.check_list_values_are_equal import ( - check_list_values_are_equal, -) -from skelly_synchronize_old.tests.utilities.get_number_of_frames_of_videos_in_a_folder import ( - get_number_of_frames_of_videos_in_a_folder, -) - - -@pytest.mark.usefixtures("synchronized_video_folder_path") -def test_videos_are_same_length(synchronized_video_folder_path: Union[str, Path]): - synchronized_video_framecounts = get_number_of_frames_of_videos_in_a_folder( - folder_path=synchronized_video_folder_path - ) - assert isinstance( - check_list_values_are_equal(synchronized_video_framecounts), (int, float) - ) diff --git a/skelly_synchronize_old/tests/utilities/check_list_values_are_equal.py b/skelly_synchronize_old/tests/utilities/check_list_values_are_equal.py deleted file mode 100644 index 3a35408..0000000 --- a/skelly_synchronize_old/tests/utilities/check_list_values_are_equal.py +++ /dev/null @@ -1,19 +0,0 @@ -import logging -from typing import Any, List - -logger = logging.getLogger(__name__) - - -def check_list_values_are_equal(input_list: List[Any]) -> Any: - """Check if values in list are all equal, throw an exception if not (or if list is empty).""" - unique_values = set(input_list) - - if len(unique_values) == 0: - raise Exception("list empty") - - if len(unique_values) == 1: - unique_value = unique_values.pop() - logger.debug(f"all values in list are equal to {unique_value}") - return unique_value - else: - raise Exception(f"list values are not equal, list is {input_list}") diff --git a/skelly_synchronize_old/tests/utilities/find_frame_count_of_video.py b/skelly_synchronize_old/tests/utilities/find_frame_count_of_video.py deleted file mode 100644 index 4eafc19..0000000 --- a/skelly_synchronize_old/tests/utilities/find_frame_count_of_video.py +++ /dev/null @@ -1,9 +0,0 @@ -import cv2 - - -def find_frame_count_of_video(video_pathstring: str): - video_capture_object = cv2.VideoCapture(video_pathstring) - frame_count = video_capture_object.get(cv2.CAP_PROP_FRAME_COUNT) - video_capture_object.release() - - return int(frame_count) diff --git a/skelly_synchronize_old/tests/utilities/get_number_of_frames_of_videos_in_a_folder.py b/skelly_synchronize_old/tests/utilities/get_number_of_frames_of_videos_in_a_folder.py deleted file mode 100644 index a8c2e32..0000000 --- a/skelly_synchronize_old/tests/utilities/get_number_of_frames_of_videos_in_a_folder.py +++ /dev/null @@ -1,43 +0,0 @@ -import logging -import sys -from pathlib import Path -from typing import List, Union - -base_package_path = Path(__file__).parent.parent.parent.parent -print(f"adding base_package_path: {base_package_path} : to sys.path") -sys.path.insert(0, str(base_package_path)) # add parent directory to sys.path - -from skelly_synchronize_old.utils.get_video_files import get_video_file_list -from skelly_synchronize_old.tests.utilities.find_frame_count_of_video import ( - find_frame_count_of_video, -) - -logger = logging.getLogger(__name__) - - -def get_number_of_frames_of_videos_in_a_folder( - folder_path: Union[str, Path], -) -> List[int]: - """ - Get the number of frames in the first video in a folder - """ - - list_of_video_paths = get_video_file_list(folder_path=Path(folder_path)) - - if len(list_of_video_paths) == 0: - logger.error(f"No videos found in {folder_path}") - raise ValueError( - f"No videos found in {folder_path}, unable to extract framecounts" - ) - - frame_count_list = [] - for video_path in list_of_video_paths: - frame_count = find_frame_count_of_video(video_pathstring=str(video_path)) - frame_count_list.append(int(frame_count)) - - return frame_count_list - - -if __name__ == "__main__": - folder_path = Path("YOUR/FOLDER/PATH") - print(get_number_of_frames_of_videos_in_a_folder(folder_path)) diff --git a/skelly_synchronize_old/tests/utilities/load_sample_data.py b/skelly_synchronize_old/tests/utilities/load_sample_data.py deleted file mode 100644 index 4085f0a..0000000 --- a/skelly_synchronize_old/tests/utilities/load_sample_data.py +++ /dev/null @@ -1,38 +0,0 @@ -import io -import requests -import zipfile -from pathlib import Path - -from skelly_synchronize_old.system.paths_and_file_names import ( - FIGSHARE_SAMPLE_DATA_FILE_NAME, - FIGSHARE_ZIP_FILE_URL, - RAW_VIDEOS_FOLDER_NAME, -) - - -def load_sample_data() -> Path: - extract_to_path = Path.home() - extract_to_path.mkdir(exist_ok=True) - - figshare_sample_data_path = extract_to_path / FIGSHARE_SAMPLE_DATA_FILE_NAME - - if not Path.exists(figshare_sample_data_path): - r = requests.get(FIGSHARE_ZIP_FILE_URL, timeout=(10, 60)) - z = zipfile.ZipFile(io.BytesIO(r.content)) - z.extractall(figshare_sample_data_path) - - return figshare_sample_data_path - - -def find_raw_videos_folder_path(session_folder_path: Path) -> Path: - for subfolder_path in session_folder_path.iterdir(): - if subfolder_path.name == RAW_VIDEOS_FOLDER_NAME: - return subfolder_path - - raise Exception( - f"Could not find a videos folder in path {str(session_folder_path)}" - ) - - -if __name__ == "__main__": - sample_data_path = load_sample_data() diff --git a/skelly_synchronize_old/utils/get_video_files.py b/skelly_synchronize_old/utils/get_video_files.py deleted file mode 100644 index 7d7d25d..0000000 --- a/skelly_synchronize_old/utils/get_video_files.py +++ /dev/null @@ -1,33 +0,0 @@ -import logging -from pathlib import Path - -from skelly_synchronize_old.system.file_extensions import VideoExtension - -logger = logging.getLogger(__name__) - - -def get_video_file_list(folder_path: Path) -> list: - """Return a list of all video files in the base_path folder that match a video file type""" - list_of_video_formats = [extension.value for extension in VideoExtension] - - video_filepath_list = [] - for file_type in list_of_video_formats: - file_extension_upper = "*" + file_type.upper() - file_extension_lower = "*" + file_type.lower() - - video_filepath_list.extend(list(folder_path.glob(file_extension_upper))) - video_filepath_list.extend(list(folder_path.glob(file_extension_lower))) - - # because glob behaves differently on windows vs. mac/linux, we collect all files both upper and lowercase, and remove redundant files that appear on windows - unique_video_filepath_list = get_unique_list(video_filepath_list) - - logger.info(f"{len(unique_video_filepath_list)} videos found in folder") - - return sorted(unique_video_filepath_list, key=lambda p: str(p).lower()) - - -def get_unique_list(list: list) -> list: - """Return a list of the unique elements from input list""" - unique_list = [] - [unique_list.append(clip) for clip in list if clip not in unique_list] - return unique_list diff --git a/skelly_synchronize_old/utils/path_handling_utilities.py b/skelly_synchronize_old/utils/path_handling_utilities.py deleted file mode 100644 index 737d2fe..0000000 --- a/skelly_synchronize_old/utils/path_handling_utilities.py +++ /dev/null @@ -1,35 +0,0 @@ -import logging -from pathlib import Path - -from skelly_synchronize_old.system.file_extensions import VideoExtension -from skelly_synchronize_old.system.paths_and_file_names import SYNCED_VIDEO_PRECURSOR - -logger = logging.getLogger(__name__) - - -def create_directory(parent_directory: Path, directory_name: str) -> Path: - """Create a new directory under the specified parent directory.""" - parent_directory = Path(parent_directory) - new_directory_path = parent_directory / directory_name - - try: - new_directory_path.mkdir(parents=True, exist_ok=True) - logger.info(f"Created directory: {new_directory_path}") - except Exception as e: - logger.error(f"Error creating directory: {new_directory_path}. Exception: {e}") - raise - - return new_directory_path - - -def name_synced_video(raw_video_filename: str) -> str: - """Take a raw video filename, remove the raw prefix if its there, and return the synced video filename""" - raw_video_filename = str(raw_video_filename) - if raw_video_filename.split("_")[0] == "raw": - synced_video_name = f"{SYNCED_VIDEO_PRECURSOR}{raw_video_filename[4:]}.{VideoExtension.MP4.value}" - else: - synced_video_name = ( - f"{SYNCED_VIDEO_PRECURSOR}{raw_video_filename}.{VideoExtension.MP4.value}" - ) - - return synced_video_name From 9c844004a9788f83758c697a391d16048ef88eff Mon Sep 17 00:00:00 2001 From: philipqueen Date: Mon, 10 Aug 2026 13:26:36 -0600 Subject: [PATCH 08/16] add react frontend --- .gitignore | 6 + README.md | 24 +- frontend/.gitignore | 24 + frontend/.oxlintrc.json | 8 + frontend/README.md | 32 + frontend/index.html | 13 + frontend/package-lock.json | 1273 +++++++++++++++++++++++ frontend/package.json | 25 + frontend/public/favicon.svg | 1 + frontend/src/App.css | 22 + frontend/src/App.tsx | 57 + frontend/src/api/client.ts | 78 ++ frontend/src/api/types.ts | 65 ++ frontend/src/components/ErrorBanner.css | 18 + frontend/src/components/ErrorBanner.tsx | 24 + frontend/src/hooks/useJobPolling.ts | 68 ++ frontend/src/index.css | 60 ++ frontend/src/main.tsx | 10 + frontend/src/screens/HistoryScreen.css | 35 + frontend/src/screens/HistoryScreen.tsx | 55 + frontend/src/screens/ProgressScreen.css | 36 + frontend/src/screens/ProgressScreen.tsx | 77 ++ frontend/src/screens/ResultScreen.css | 48 + frontend/src/screens/ResultScreen.tsx | 69 ++ frontend/src/screens/SetupScreen.css | 45 + frontend/src/screens/SetupScreen.tsx | 119 +++ frontend/tsconfig.app.json | 26 + frontend/tsconfig.json | 7 + frontend/tsconfig.node.json | 23 + frontend/vite.config.ts | 7 + 30 files changed, 2348 insertions(+), 7 deletions(-) create mode 100644 frontend/.gitignore create mode 100644 frontend/.oxlintrc.json create mode 100644 frontend/README.md create mode 100644 frontend/index.html create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/public/favicon.svg create mode 100644 frontend/src/App.css create mode 100644 frontend/src/App.tsx create mode 100644 frontend/src/api/client.ts create mode 100644 frontend/src/api/types.ts create mode 100644 frontend/src/components/ErrorBanner.css create mode 100644 frontend/src/components/ErrorBanner.tsx create mode 100644 frontend/src/hooks/useJobPolling.ts create mode 100644 frontend/src/index.css create mode 100644 frontend/src/main.tsx create mode 100644 frontend/src/screens/HistoryScreen.css create mode 100644 frontend/src/screens/HistoryScreen.tsx create mode 100644 frontend/src/screens/ProgressScreen.css create mode 100644 frontend/src/screens/ProgressScreen.tsx create mode 100644 frontend/src/screens/ResultScreen.css create mode 100644 frontend/src/screens/ResultScreen.tsx create mode 100644 frontend/src/screens/SetupScreen.css create mode 100644 frontend/src/screens/SetupScreen.tsx create mode 100644 frontend/tsconfig.app.json create mode 100644 frontend/tsconfig.json create mode 100644 frontend/tsconfig.node.json create mode 100644 frontend/vite.config.ts diff --git a/.gitignore b/.gitignore index 8ed3a70..303eb45 100644 --- a/.gitignore +++ b/.gitignore @@ -155,3 +155,9 @@ cython_debug/ .DS_Store CLAUDE.md + +# Node (frontend/) +node_modules/ +dist/ +dist-ssr/ +*.local diff --git a/README.md b/README.md index 44d973f..7c7e3a5 100644 --- a/README.md +++ b/README.md @@ -4,30 +4,40 @@ Skelly Synchronize is a package for synchronizing videos post-recording, without ## Install and Run -Skelly_synchronize can be installed through pip by running `pip install skelly_synchronize` in your terminal. Once it has installed, it can be run with the command `python -m skelly_synchronize`. +Skelly Synchronize is a Python library (`core`), a FastAPI server (`api`), and a React web UI (`frontend`), plus a CLI. To use the web UI, run the API server and the frontend dev server as two processes: -While running, the GUI window may appear frozen, but the terminal should show the progress. Large videos may take a significant amount of time. +``` +pip install -e ".[api]" +skelly-sync-api +``` -Skelly_synchronize currently depends on FFmpeg, a command line tool that handles the video files. If you do not have FFmpeg downloaded, you will need to install it separately. You can download FFmpeg here: https://ffmpeg.org/download.html +This starts the API on `http://127.0.0.1:8000`. Then, in a second terminal: + +``` +cd frontend +npm install +npm run dev +``` -Screen Shot 2023-10-10 at 9 51 11 AM +Open the URL Vite prints (typically `http://localhost:5173`) in your browser. See [`frontend/README.md`](frontend/README.md) for details. +Skelly_synchronize currently depends on FFmpeg, a command line tool that handles the video files. If you do not have FFmpeg downloaded, you will need to install it separately. You can download FFmpeg here: https://ffmpeg.org/download.html ## Using Skelly Synchronize -Once you have the GUI open, choose a folder of raw videos that you would like to synchronize. The videos must overlap in time to be able to be synchronized. The software currently works with `mp4`, `mkv`, `avi`, `mpeg`, and `mov` files. Once the folder of videos has been selected, you can press the button for the synchronization method you would like to run. The synchronized videos will be placed in a folder called "synchronized_videos" that will be in the same directory as the folder of raw videos. +Once you have the web UI open, choose a folder of raw videos that you would like to synchronize. The videos must overlap in time to be able to be synchronized. The software currently works with `mp4`, `mkv`, `avi`, `mpeg`, and `mov` files. Once the folder of videos has been selected, you can choose the synchronization method you would like to run. The synchronized videos will be placed in a folder called "synchronized_videos" that will be in the same directory as the folder of raw videos (or in a custom output folder, if one was specified). ### Synchronization Methods **Audio Cross Correlation** synchronizes by aligning the audio files of each video as closely as possible. Cross correlation is a mathematical technique used to find the amount of offset between different signals. In this case, Skelly Synchronize is using cross correlation to find the time difference between the audio tracks of the video files. -**Brightness Contrast Detection** synchronizes by looking for a quick flash near the beginning of each video. This flash can be from a camera flash, turning on a light, or even opening curtains to a bright window. Skelly Synchronize looks for the first time in each video that the change in brightness (contrast) between subsequent frames passes a certain threshold, and then aligns the brightness change of each video. The brightness contrast threshold used can be set as a parameter in the GUI, and higher threshold values will require a more abrupt and brighter flash in the video. Synchronization will be best if all cameras see the flash at the same time, so methods like turning on a light will yield better synchronization than methods like opening curtains. +**Brightness Contrast Detection** synchronizes by looking for a quick flash near the beginning of each video. This flash can be from a camera flash, turning on a light, or even opening curtains to a bright window. Skelly Synchronize looks for the first time in each video that the change in brightness (contrast) between subsequent frames passes a certain threshold, and then aligns the brightness change of each video. The brightness contrast threshold used can be set as a parameter in the web UI, and higher threshold values will require a more abrupt and brighter flash in the video. Synchronization will be best if all cameras see the flash at the same time, so methods like turning on a light will yield better synchronization than methods like opening curtains. ### Video Requirements For **audio synchronization**, all videos must have audio tracks. Synchronization will work better if there are short, distinct sounds audible from each camera, for example a loud clap. -For **brightness synchronization**, there must be a quick increase in brightness across all of the video files. This method requires a significant brightness change visible to all cameras, for example turning on a bright light or firing a flash visible to all cameras. The synchronization will be based off of the first brightness change in each video that crosses a threshold. You can set the brightness ratio threshold in the gui before synchronizing. The threshold takes into account both the brightness contrast compared to the preceding frame, and the rate of change of brightness contrast. It may take multiple tries with different brightness ratio thresholds to get proper synchronization, although the default should work in most cases. +For **brightness synchronization**, there must be a quick increase in brightness across all of the video files. This method requires a significant brightness change visible to all cameras, for example turning on a bright light or firing a flash visible to all cameras. The synchronization will be based off of the first brightness change in each video that crosses a threshold. You can set the brightness ratio threshold in the web UI before synchronizing. The threshold takes into account both the brightness contrast compared to the preceding frame, and the rate of change of brightness contrast. It may take multiple tries with different brightness ratio thresholds to get proper synchronization, although the default should work in most cases. ### Additional Files diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/frontend/.oxlintrc.json b/frontend/.oxlintrc.json new file mode 100644 index 0000000..6fa991d --- /dev/null +++ b/frontend/.oxlintrc.json @@ -0,0 +1,8 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["react", "typescript", "oxc"], + "rules": { + "react/rules-of-hooks": "error", + "react/only-export-components": ["warn", { "allowConstantExport": true }] + } +} diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..7bc0478 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,32 @@ +# Skelly Synchronize — frontend + +React (Vite + TypeScript) UI for `skelly_synchronize`, talking to the `skelly_synchronize.api` FastAPI server over HTTP. + +## Running + +This is a two-process dev setup: the API server and the Vite dev server. + +1. From the repo root, install the API extra and start the API server: + + ``` + pip install -e ".[api]" + skelly-sync-api + ``` + + This runs on `http://127.0.0.1:8000`. + +2. In this directory, install dependencies and start the dev server: + + ``` + npm install + npm run dev + ``` + +3. Open the URL Vite prints (typically `http://localhost:5173`). + +The API's CORS policy allows any `localhost`/`127.0.0.1` origin, so no dev-server proxy is needed. + +## Notes + +- `src/api/client.ts` hardcodes the API base URL as `http://127.0.0.1:8000`. There's no environment-variable override yet — add one if/when the frontend needs to be bundled and served from FastAPI as a single process. +- No test runner or linter beyond TypeScript's own checks (`npm run build` runs `tsc -b`) is set up yet. diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..ffb4382 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + Skelly Synchronize + + +
+ + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..2619f26 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,1273 @@ +{ + "name": "frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.0.0", + "dependencies": { + "react": "^19.2.8", + "react-dom": "^19.2.8" + }, + "devDependencies": { + "@types/node": "^24.13.3", + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.4", + "oxlint": "^1.75.0", + "typescript": "~6.0.2", + "vite": "^8.2.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.143.0.tgz", + "integrity": "sha512-u6JZdLBTLotrNC9Vd6vPssINdzcCzleKAH6EJKImQb7GtYvX5keN2dxkoK44stCc4tffE6QQRtZTXVSzsLUlWA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@oxlint/binding-android-arm-eabi": { + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.78.0.tgz", + "integrity": "sha512-Bu819lmAfZMUHErrpe0cEWj3iaefuUODHSU8+UbXy67V/r7/7f4K3FL0NmbD85E+wiFLDYuhP8Zlv0XnVeXshw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-android-arm64": { + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.78.0.tgz", + "integrity": "sha512-CDfxZgB61B7buRdY2FJoAYYPPXCZ1EoC1LKscnC5dg3kjobdxiconvAvvN1BmHyW4PyFT3jRLDag/BY/roSNBQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-arm64": { + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.78.0.tgz", + "integrity": "sha512-2Y2U9Ahrz+OO0Ej88f9SJYq51/jUBp1Mc7iZu0ukrbeeZ3gpRGfzIFnoqfHDY96xr0GEfNrPUBFEy0nN5aD7HA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-x64": { + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.78.0.tgz", + "integrity": "sha512-rpych6eJq6m9jDRypTEaPD1xysaEW5h9+xuxhGK/QhOg+/xaqPZrCrTNoIl/f3nEjuJeCEmstNDlrE9rJi/3/g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-freebsd-x64": { + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.78.0.tgz", + "integrity": "sha512-IcMGrQT3QizkOESUJd5et+rOhVqSkNDfNik1cvrKDqIbzqx9KMtRswpFgkCuNTSwylCFLKhGUu8KmqY1ZnC0Dg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-gnueabihf": { + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.78.0.tgz", + "integrity": "sha512-/uLdoJ0IXE6vo/0f0LKjinQAp+re+VMaCWaNT8ENIv2EOCkSsc8SGaflXAuW0Jua2dq5+GLVWm1NQK7P3UFSNQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-musleabihf": { + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.78.0.tgz", + "integrity": "sha512-7xi4Wb/O8NRJhLoUXmDJMUVpNYvB5kefdhFU1Jb8rtae4QoXlTiLwI14X4YvAXVZLNZChP8m5qO9SQAlWQTbkQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-gnu": { + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.78.0.tgz", + "integrity": "sha512-4hFW0+fVXa3OIh1Y4A5SPkmvI4wuuBSrCVKzOyE7PTjhc7yEqZ1pmvEEeS5Lj/MaqvegFxXyF33N+6jkehxdyg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-musl": { + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.78.0.tgz", + "integrity": "sha512-oC0mvsgBJjlMijSDEhx9KuvR9zYeHXceA9MjbuXB1F8NSR78Yj2unOBrstEvTVaq+pko+kuue6DajC00eqvTdg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-ppc64-gnu": { + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.78.0.tgz", + "integrity": "sha512-XAllT5SUZS+ohjuZ3/5S0cwe0r7eboiuigeStCZ5DXRYx/2KVM2UvQXvAfyzXEimtQjAB7cDQ2YxDe2Zl2WNQQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-gnu": { + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.78.0.tgz", + "integrity": "sha512-trucMER/0QtecoXvc1y/UVqE3kwJipDwrx4oHfj+nNm3dq2zjP44WT0CfHNDPM3G1DXIkx/gY6lAD21NSCZVhA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-musl": { + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.78.0.tgz", + "integrity": "sha512-cm3O4F/HQbdzOUX5mKHqG5KDL6E5w0pnlZ+fbBy2rmLryPOowkuLagFHTopQsEIpjcaZoPOrL+BmmAytAG9HFg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-s390x-gnu": { + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.78.0.tgz", + "integrity": "sha512-33wRf6HqGNsybJ3qX4cGaQN2ODPxNmc1rMa0mrTmx3eFq1VzOnvQooi9bIGVYakW8a/wmqVx1mgsUm8R2xfTiw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-gnu": { + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.78.0.tgz", + "integrity": "sha512-rRdISSYegj6VganMZ9tjRjijowfHJ09IZU01i0toBAqr6n5LEtwHq2IeS4FjW2RoskOHlb6efB26H5izYb3GEQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-musl": { + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.78.0.tgz", + "integrity": "sha512-GmsP4rW0xTL6u5CVdcDsaN5Fbc7hBc382Wmar1kttbnwSEviM+rSINKOMQ+UQ6iH+AGwC+8gaAiwu134Tgh6Lg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-openharmony-arm64": { + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.78.0.tgz", + "integrity": "sha512-sy9yeYuADc8a+n4TLBayzMCZiHPW78DcIFVpOXTmdKHWQeM9xe5uzkqIIZmi326D5hY9XVwacipEB1p7tQjPAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-arm64-msvc": { + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.78.0.tgz", + "integrity": "sha512-rjc2hF1KfMi8fZj1X/m3AmnHbdsF3rL0v6KQg0Uc880Yb2khjz+3U14sfdZ7jWTpRnN1m1NQa/TT7uU9lJWPrA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-ia32-msvc": { + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.78.0.tgz", + "integrity": "sha512-zcuXFVrEFHIafRfkCQT8w/Xe41o07ozl/vwHq7p94vB29xVzsB0sZGYORU1jhcYKv3Lr0J3HbJ2T4fHH5rWmvA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-x64-msvc": { + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.78.0.tgz", + "integrity": "sha512-Sb5ocmLSuYeOuXd+CFOToGKp/gjXUEWDnvIGwhnh8aq8wY4TMmEnKnvbogSW7RdMZv77JSARduS7/gv+khYEjA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.3.tgz", + "integrity": "sha512-zrJtHDcaZJ1Fp7xf4hNl+7seH9Cn/N5TwLYkhgXREtBwAd/jaqW3uqeHxpDugJLVICWg4eW44kOQEGJ1r6jCGw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.3.tgz", + "integrity": "sha512-ieIiibVCp0tX7TLu2cafoNPv8wJyYi01ekXpbf8q2j7F4rGAhhXb/eQh7ge9DRBY78GwmRQtvjZDux7EDbA8kA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.3.tgz", + "integrity": "sha512-Zh9tCon19eDXJoihx0rqKhMUlMYqzwj3aPsSuHmI4RWZh62dWUL+DJN4C5YQya5TcQBJU/Fe8+rY0jhXTQITqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.3.tgz", + "integrity": "sha512-nGbJWewA1wrXXZiQhjAT5rhibGfns5ZNkDVqxsO6zJ3f3YvpoDNNmGMSbbhLuXKjNScaBJVOAboztAWVespQMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.3.tgz", + "integrity": "sha512-QNniJr5Kml0kDEB98jiDOJjXNroxIIi0IXIbdYzY26Xt1pVbeP62+KnoIZLwirOymX/0jDk/2gI/bNUv7A7OIw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.3.tgz", + "integrity": "sha512-TkqEAcmmvH3I/q4114NB4RVt6241Dao48pF45uLcFGrwAaIn0iITgTAKP/dLjbN0R4buJjGb91+UHSoFmpgIWw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.3.tgz", + "integrity": "sha512-NHqjnxpsndf4MPymxteFAWHHfkTL8HjWh1KB7z23ofZ6QO2euONuxDXjat69dKZRALnGypg8k8SsK8vZJoXv1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.3.tgz", + "integrity": "sha512-6tbrbwfz5GB9DQ4Jwo6hy9v+vR31xZlvzZ6n5Xut6Hhx5PvrA9q/HsK8KMaYQp063iqZGXwNvZtYNLD7EM/x0w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.3.tgz", + "integrity": "sha512-oyuXxXmoZHjXC917IAPFAAv4wWAa0cM9afk8nx1+9/jNNOX1uPf8yDA6p7G0RypOfw/X0PQt5IfoquY1um+zSg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.3.tgz", + "integrity": "sha512-TytMwF2KVGqP2tgd0I1OY0PAv78dZRAYcF5ssDzjM34SUXCED3uXvSd5+lHoC0bTD6eEdFz7LdQNCO1y0oVk9w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.3.tgz", + "integrity": "sha512-/E9m3qstrJFVPoULV25mVQblSNExY2+kBsYe4sy0Tn0yOOgJ8wZbZt3KnRbF/XeU2Gl1STKUQnDNTqhIE5MD4A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.3.tgz", + "integrity": "sha512-Kr0OcsoQI816i6HOl3vFHpd1K0eZyh76zgfj4c1nTyaTsd5r2Mj1lwM4R90y/qaCfmTn9eHy0SKwi98eitRxug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.3.tgz", + "integrity": "sha512-hOtMwTqnME+/gJcH/PCZ0wn0zPUjiWOgkHpxbSJpfGKMezHltx1S7/k1SitzVa7Ww2cqrDDaFbZEhcJZO8o+Jw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.3.tgz", + "integrity": "sha512-ekcqMMkI2PlhYnfzQnB/cEdYUVVJViWvoUyLrbzgDoi3Snfc1mVBwdnc306ufA5ejy8JSPjT2RlW1nQSjW7efg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz", + "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.18", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", + "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.5.tgz", + "integrity": "sha512-BOVzne/NL162sMdResB25mUv+vWMF5NoAjNf09TeGlE7ZpszZWSD3winycicLJw72yeVsoCn/2kOhEuCvEShMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/oxlint": { + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.78.0.tgz", + "integrity": "sha512-QgQePuxIqKOzo1KSjG2EnITEeWvWnKAm77eq8nrMtf6AGoA+zyGc4PFYtDNJSD25g/ibOwfQ851hZ4/SPkMVoA==", + "dev": true, + "license": "MIT", + "bin": { + "oxlint": "bin/oxlint" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxlint/binding-android-arm-eabi": "1.78.0", + "@oxlint/binding-android-arm64": "1.78.0", + "@oxlint/binding-darwin-arm64": "1.78.0", + "@oxlint/binding-darwin-x64": "1.78.0", + "@oxlint/binding-freebsd-x64": "1.78.0", + "@oxlint/binding-linux-arm-gnueabihf": "1.78.0", + "@oxlint/binding-linux-arm-musleabihf": "1.78.0", + "@oxlint/binding-linux-arm64-gnu": "1.78.0", + "@oxlint/binding-linux-arm64-musl": "1.78.0", + "@oxlint/binding-linux-ppc64-gnu": "1.78.0", + "@oxlint/binding-linux-riscv64-gnu": "1.78.0", + "@oxlint/binding-linux-riscv64-musl": "1.78.0", + "@oxlint/binding-linux-s390x-gnu": "1.78.0", + "@oxlint/binding-linux-x64-gnu": "1.78.0", + "@oxlint/binding-linux-x64-musl": "1.78.0", + "@oxlint/binding-openharmony-arm64": "1.78.0", + "@oxlint/binding-win32-arm64-msvc": "1.78.0", + "@oxlint/binding-win32-ia32-msvc": "1.78.0", + "@oxlint/binding-win32-x64-msvc": "1.78.0" + }, + "peerDependencies": { + "oxlint-tsgolint": ">=7.0.2001", + "vite-plus": "*" + }, + "peerDependenciesMeta": { + "oxlint-tsgolint": { + "optional": true + }, + "vite-plus": { + "optional": true + } + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.8" + } + }, + "node_modules/rolldown": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.3.tgz", + "integrity": "sha512-rn9wpmxplLf7NLNyCk9FyWh3FM43DbY8jOzCdEPzH7uflhTftRbCEpqi6Ly2osgoU8OwObtmavMbWLaWy4LX7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.143.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.2.3", + "@rolldown/binding-darwin-arm64": "1.2.3", + "@rolldown/binding-darwin-x64": "1.2.3", + "@rolldown/binding-freebsd-x64": "1.2.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.3", + "@rolldown/binding-linux-arm64-gnu": "1.2.3", + "@rolldown/binding-linux-arm64-musl": "1.2.3", + "@rolldown/binding-linux-ppc64-gnu": "1.2.3", + "@rolldown/binding-linux-s390x-gnu": "1.2.3", + "@rolldown/binding-linux-x64-gnu": "1.2.3", + "@rolldown/binding-linux-x64-musl": "1.2.3", + "@rolldown/binding-openharmony-arm64": "1.2.3", + "@rolldown/binding-win32-arm64-msvc": "1.2.3", + "@rolldown/binding-win32-x64-msvc": "1.2.3" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.1.tgz", + "integrity": "sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.25", + "rolldown": "~1.2.1", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.4.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..d5a090e --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,25 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "oxlint", + "preview": "vite preview" + }, + "dependencies": { + "react": "^19.2.8", + "react-dom": "^19.2.8" + }, + "devDependencies": { + "@types/node": "^24.13.3", + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.4", + "oxlint": "^1.75.0", + "typescript": "~6.0.2", + "vite": "^8.2.0" + } +} diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg new file mode 100644 index 0000000..6893eb1 --- /dev/null +++ b/frontend/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/App.css b/frontend/src/App.css new file mode 100644 index 0000000..1cd9de7 --- /dev/null +++ b/frontend/src/App.css @@ -0,0 +1,22 @@ +.app-header { + display: flex; + align-items: baseline; + justify-content: space-between; + margin-bottom: 24px; +} + +.app-header h1 { + font-size: 22px; + margin: 0; +} + +.app-header nav a { + color: var(--accent); + cursor: pointer; + text-decoration: none; + font-size: 14px; +} + +.app-header nav a:hover { + text-decoration: underline; +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..345d97b --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,57 @@ +import { useState } from "react"; +import "./App.css"; +import { SetupScreen } from "./screens/SetupScreen"; +import { ProgressScreen } from "./screens/ProgressScreen"; +import { ResultScreen } from "./screens/ResultScreen"; +import { HistoryScreen } from "./screens/HistoryScreen"; + +type View = + | { screen: "setup" } + | { screen: "progress"; jobId: string } + | { screen: "result"; jobId: string } + | { screen: "history" }; + +function App() { + const [view, setView] = useState({ screen: "setup" }); + + return ( + <> +
+

Skelly Synchronize

+ +
+ + {view.screen === "setup" && ( + setView({ screen: "progress", jobId })} + /> + )} + + {view.screen === "progress" && ( + setView({ screen: "result", jobId })} + onBackToSetup={() => setView({ screen: "setup" })} + /> + )} + + {view.screen === "result" && ( + setView({ screen: "setup" })} + /> + )} + + {view.screen === "history" && ( + setView({ screen: "result", jobId })} + onBackToSetup={() => setView({ screen: "setup" })} + /> + )} + + ); +} + +export default App; diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts new file mode 100644 index 0000000..b8ac113 --- /dev/null +++ b/frontend/src/api/client.ts @@ -0,0 +1,78 @@ +import type { + Job, + JobCreateResponse, + SyncRequest, + VideosResponse, +} from "./types"; + +export const API_BASE = "http://127.0.0.1:8000"; + +export class ApiError extends Error { + status: number; + detail: string; + stderr?: string; + + constructor(status: number, detail: string, stderr?: string) { + super(detail); + this.name = "ApiError"; + this.status = status; + this.detail = detail; + this.stderr = stderr; + } +} + +async function request(path: string, init?: RequestInit): Promise { + const response = await fetch(`${API_BASE}${path}`, init); + + if (!response.ok) { + let detail = response.statusText; + let stderr: string | undefined; + try { + const body = await response.json(); + if (typeof body?.detail === "string") { + detail = body.detail; + } + if (typeof body?.stderr === "string") { + stderr = body.stderr; + } + } catch { + // response body wasn't JSON; fall back to statusText + } + throw new ApiError(response.status, detail, stderr); + } + + return response.json() as Promise; +} + +export function getHealth(): Promise<{ status: string }> { + return request("/health"); +} + +export function listVideos(folderPath: string): Promise { + const query = new URLSearchParams({ folder_path: folderPath }); + return request(`/videos?${query.toString()}`); +} + +export function createJob(req: SyncRequest): Promise { + return request("/jobs", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(req), + }); +} + +export function getJob(jobId: string): Promise { + return request(`/jobs/${jobId}`); +} + +export function listJobs(): Promise { + return request("/jobs"); +} + +export function cancelJob(jobId: string): Promise { + return request(`/jobs/${jobId}`, { method: "DELETE" }); +} + +export function debugPlotUrl(jobId: string): string { + return `${API_BASE}/jobs/${jobId}/debug-plot`; +} diff --git a/frontend/src/api/types.ts b/frontend/src/api/types.ts new file mode 100644 index 0000000..5e02903 --- /dev/null +++ b/frontend/src/api/types.ts @@ -0,0 +1,65 @@ +export type VideoBackendKind = "ffmpeg" | "deffcode"; + +export type SyncMethod = "audio cross-correlation" | "brightness change detection"; + +export type JobStatus = "pending" | "running" | "succeeded" | "failed" | "cancelled"; + +export interface VideoInfo { + filepath: string; + video_name: string; + duration_seconds: number; + fps: number; + frame_count: number | null; +} + +export interface LagResult { + video_name: string; + lag_seconds: number; + confidence: number | null; +} + +export interface SyncRequest { + raw_video_folder_path: string; + synchronized_video_folder_path?: string | null; + method: SyncMethod; + video_handler: VideoBackendKind; + brightness_ratio_threshold: number; + create_debug_artifacts: boolean; +} + +export interface SyncResult { + synchronized_video_folder_path: string; + videos_before: VideoInfo[]; + videos_after: VideoInfo[]; + lags: LagResult[]; + debug_artifact_paths: string[]; + elapsed_seconds: number; + synchronized_frame_count: number | null; +} + +export interface Job { + id: string; + status: JobStatus; + progress: number; + progress_message: string | null; + created_at: string; + updated_at: string; + request: SyncRequest; + result: SyncResult | null; + error: string | null; +} + +export interface JobCreateResponse { + job_id: string; + status: JobStatus; +} + +export interface VideoPreview { + video_name: string; + filepath: string; +} + +export interface VideosResponse { + folder_path: string; + videos: VideoPreview[]; +} diff --git a/frontend/src/components/ErrorBanner.css b/frontend/src/components/ErrorBanner.css new file mode 100644 index 0000000..7eae49f --- /dev/null +++ b/frontend/src/components/ErrorBanner.css @@ -0,0 +1,18 @@ +.error-banner { + background: var(--danger-bg); + border: 1px solid var(--danger); + color: var(--danger); + border-radius: 6px; + padding: 12px 16px; + margin-bottom: 16px; +} + +.error-banner p { + margin: 0; +} + +.error-banner pre { + white-space: pre-wrap; + word-break: break-word; + font-size: 13px; +} diff --git a/frontend/src/components/ErrorBanner.tsx b/frontend/src/components/ErrorBanner.tsx new file mode 100644 index 0000000..1b2a80b --- /dev/null +++ b/frontend/src/components/ErrorBanner.tsx @@ -0,0 +1,24 @@ +import "./ErrorBanner.css"; + +interface ErrorBannerProps { + message: string | null | undefined; + detail?: string; +} + +export function ErrorBanner({ message, detail }: ErrorBannerProps) { + if (!message) { + return null; + } + + return ( +
+

{message}

+ {detail && ( +
+ Details +
{detail}
+
+ )} +
+ ); +} diff --git a/frontend/src/hooks/useJobPolling.ts b/frontend/src/hooks/useJobPolling.ts new file mode 100644 index 0000000..9750d6e --- /dev/null +++ b/frontend/src/hooks/useJobPolling.ts @@ -0,0 +1,68 @@ +import { useEffect, useState } from "react"; +import { ApiError, getJob } from "../api/client"; +import type { Job, JobStatus, SyncResult } from "../api/types"; + +const POLL_INTERVAL_MS = 1000; + +const TERMINAL_STATUSES: JobStatus[] = ["succeeded", "failed", "cancelled"]; + +interface JobPollingState { + job: Job | null; + status: JobStatus | null; + progress: number; + progressMessage: string | null; + result: SyncResult | null; + error: string | null; +} + +export function useJobPolling(jobId: string | null): JobPollingState { + const [job, setJob] = useState(null); + const [fetchError, setFetchError] = useState(null); + + useEffect(() => { + setJob(null); + setFetchError(null); + + if (jobId === null) { + return; + } + + let cancelled = false; + let intervalId: ReturnType | null = null; + + const poll = async () => { + try { + const latest = await getJob(jobId); + if (cancelled) return; + setJob(latest); + setFetchError(null); + if (TERMINAL_STATUSES.includes(latest.status) && intervalId !== null) { + clearInterval(intervalId); + intervalId = null; + } + } catch (e) { + if (cancelled) return; + setFetchError(e instanceof ApiError ? e.detail : "failed to fetch job status"); + } + }; + + poll(); + intervalId = setInterval(poll, POLL_INTERVAL_MS); + + return () => { + cancelled = true; + if (intervalId !== null) { + clearInterval(intervalId); + } + }; + }, [jobId]); + + return { + job, + status: job?.status ?? null, + progress: job?.progress ?? 0, + progressMessage: job?.progress_message ?? null, + result: job?.result ?? null, + error: fetchError ?? job?.error ?? null, + }; +} diff --git a/frontend/src/index.css b/frontend/src/index.css new file mode 100644 index 0000000..572f80d --- /dev/null +++ b/frontend/src/index.css @@ -0,0 +1,60 @@ +:root { + --text: #2b2b33; + --text-muted: #6b6375; + --bg: #ffffff; + --bg-muted: #f4f3ec; + --border: #e5e4e7; + --accent: #7c3aed; + --accent-contrast: #ffffff; + --danger: #b3261e; + --danger-bg: #fdecea; + --success: #2e7d32; + + font: 16px/1.45 system-ui, "Segoe UI", Roboto, sans-serif; + color-scheme: light dark; + color: var(--text); + background: var(--bg); +} + +@media (prefers-color-scheme: dark) { + :root { + --text: #e5e4e7; + --text-muted: #9ca3af; + --bg: #16171d; + --bg-muted: #1f2028; + --border: #2e303a; + --accent: #c084fc; + --accent-contrast: #16171d; + --danger: #f2b8b5; + --danger-bg: #3a1f1f; + --success: #7ac47f; + } +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; +} + +#root { + max-width: 760px; + margin: 0 auto; + padding: 24px 16px 64px; +} + +h1, +h2 { + color: var(--text); +} + +button { + font: inherit; +} + +input, +select { + font: inherit; +} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx new file mode 100644 index 0000000..bef5202 --- /dev/null +++ b/frontend/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './index.css' +import App from './App.tsx' + +createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/frontend/src/screens/HistoryScreen.css b/frontend/src/screens/HistoryScreen.css new file mode 100644 index 0000000..f4292fd --- /dev/null +++ b/frontend/src/screens/HistoryScreen.css @@ -0,0 +1,35 @@ +.history-table { + width: 100%; + border-collapse: collapse; + margin-top: 16px; +} + +.history-table th, +.history-table td { + border: 1px solid var(--border); + padding: 8px 10px; + text-align: left; + font-size: 14px; +} + +.history-table th { + background: var(--bg-muted); +} + +.history-table tbody tr { + cursor: pointer; +} + +.history-table tbody tr:hover { + background: var(--bg-muted); +} + +.history-screen button { + margin-top: 24px; + padding: 10px 20px; + border: 1px solid var(--border); + border-radius: 6px; + background: var(--bg); + color: var(--text); + cursor: pointer; +} diff --git a/frontend/src/screens/HistoryScreen.tsx b/frontend/src/screens/HistoryScreen.tsx new file mode 100644 index 0000000..5214061 --- /dev/null +++ b/frontend/src/screens/HistoryScreen.tsx @@ -0,0 +1,55 @@ +import { useEffect, useState } from "react"; +import { ApiError, listJobs } from "../api/client"; +import type { Job } from "../api/types"; +import { ErrorBanner } from "../components/ErrorBanner"; +import "./HistoryScreen.css"; + +interface HistoryScreenProps { + onSelectJob: (jobId: string) => void; + onBackToSetup: () => void; +} + +export function HistoryScreen({ onSelectJob, onBackToSetup }: HistoryScreenProps) { + const [jobs, setJobs] = useState([]); + const [loadError, setLoadError] = useState(null); + + useEffect(() => { + listJobs() + .then(setJobs) + .catch((e) => + setLoadError(e instanceof ApiError ? e.detail : "failed to load job history"), + ); + }, []); + + return ( +
+

Job history

+ + + {jobs.length === 0 && loadError === null &&

No jobs yet.

} + + {jobs.length > 0 && ( + + + + + + + + + + {jobs.map((job) => ( + onSelectJob(job.id)}> + + + + + ))} + +
CreatedMethodStatus
{new Date(job.created_at).toLocaleString()}{job.request.method}{job.status}
+ )} + + +
+ ); +} diff --git a/frontend/src/screens/ProgressScreen.css b/frontend/src/screens/ProgressScreen.css new file mode 100644 index 0000000..1b14d96 --- /dev/null +++ b/frontend/src/screens/ProgressScreen.css @@ -0,0 +1,36 @@ +.progress-bar-track { + width: 100%; + height: 12px; + border-radius: 6px; + background: var(--bg-muted); + border: 1px solid var(--border); + overflow: hidden; + margin-top: 16px; +} + +.progress-bar-fill { + height: 100%; + background: var(--accent); + transition: width 0.3s ease; +} + +.progress-status { + margin-top: 8px; + color: var(--text-muted); + font-size: 14px; +} + +.progress-screen button { + margin-top: 24px; + padding: 10px 20px; + border: 1px solid var(--border); + border-radius: 6px; + background: var(--bg); + color: var(--text); + cursor: pointer; +} + +.progress-screen button:disabled { + opacity: 0.6; + cursor: default; +} diff --git a/frontend/src/screens/ProgressScreen.tsx b/frontend/src/screens/ProgressScreen.tsx new file mode 100644 index 0000000..63c9563 --- /dev/null +++ b/frontend/src/screens/ProgressScreen.tsx @@ -0,0 +1,77 @@ +import { useEffect, useState } from "react"; +import { ApiError, cancelJob } from "../api/client"; +import { useJobPolling } from "../hooks/useJobPolling"; +import { ErrorBanner } from "../components/ErrorBanner"; +import "./ProgressScreen.css"; + +interface ProgressScreenProps { + jobId: string; + onSucceeded: (jobId: string) => void; + onBackToSetup: () => void; +} + +const TERMINAL_STATUSES = ["succeeded", "failed", "cancelled"]; + +export function ProgressScreen({ jobId, onSucceeded, onBackToSetup }: ProgressScreenProps) { + const { status, progress, progressMessage, error } = useJobPolling(jobId); + const [cancelling, setCancelling] = useState(false); + const [cancelError, setCancelError] = useState(null); + + useEffect(() => { + if (status === "succeeded") { + onSucceeded(jobId); + } + }, [status, jobId, onSucceeded]); + + const handleCancel = async () => { + setCancelling(true); + setCancelError(null); + try { + await cancelJob(jobId); + } catch (e) { + setCancelError(e instanceof ApiError ? e.detail : "failed to cancel job"); + } finally { + setCancelling(false); + } + }; + + const isTerminal = status !== null && TERMINAL_STATUSES.includes(status); + + return ( +
+

Synchronizing…

+ + +
+
+
+

+ {status ?? "loading…"} + {progressMessage ? ` — ${progressMessage}` : ""} +

+ + {status === "failed" && ( + <> + + + + )} + + {status === "cancelled" && ( + <> +

Job cancelled.

+ + + )} + + {!isTerminal && status !== null && ( + + )} +
+ ); +} diff --git a/frontend/src/screens/ResultScreen.css b/frontend/src/screens/ResultScreen.css new file mode 100644 index 0000000..3fdcfd2 --- /dev/null +++ b/frontend/src/screens/ResultScreen.css @@ -0,0 +1,48 @@ +.lag-table { + width: 100%; + border-collapse: collapse; + margin-top: 16px; +} + +.lag-table th, +.lag-table td { + border: 1px solid var(--border); + padding: 8px 10px; + text-align: left; + font-size: 14px; +} + +.lag-table th { + background: var(--bg-muted); +} + +.debug-plot { + margin-top: 24px; +} + +.debug-plot img { + max-width: 100%; + border: 1px solid var(--border); + border-radius: 6px; +} + +.output-path { + margin-top: 16px; + font-size: 14px; + color: var(--text-muted); +} + +.output-path .selectable { + user-select: all; + font-family: ui-monospace, Consolas, monospace; +} + +.result-screen button { + margin-top: 24px; + padding: 10px 20px; + border: none; + border-radius: 6px; + background: var(--accent); + color: var(--accent-contrast); + cursor: pointer; +} diff --git a/frontend/src/screens/ResultScreen.tsx b/frontend/src/screens/ResultScreen.tsx new file mode 100644 index 0000000..4680083 --- /dev/null +++ b/frontend/src/screens/ResultScreen.tsx @@ -0,0 +1,69 @@ +import { debugPlotUrl } from "../api/client"; +import { useJobPolling } from "../hooks/useJobPolling"; +import { ErrorBanner } from "../components/ErrorBanner"; +import "./ResultScreen.css"; + +interface ResultScreenProps { + jobId: string; + onRunAnother: () => void; +} + +export function ResultScreen({ jobId, onRunAnother }: ResultScreenProps) { + const { job, status, result, error } = useJobPolling(jobId); + + if (job === null) { + return

Loading…

; + } + + if (status === "failed" || status === "cancelled") { + return ( +
+

Job {status}

+ + +
+ ); + } + + if (result === null) { + return

Loading result…

; + } + + return ( +
+

Synchronization complete

+ + + + + + + + + + + {result.lags.map((lag) => ( + + + + + + ))} + +
VideoLag (s)Confidence
{lag.video_name}{lag.lag_seconds.toFixed(3)}{lag.confidence !== null ? lag.confidence.toFixed(3) : "—"}
+ + {job.request.create_debug_artifacts && ( +
+

Debug plot

+ Synchronization debug plot +
+ )} + +

+ Output folder: {result.synchronized_video_folder_path} +

+ + +
+ ); +} diff --git a/frontend/src/screens/SetupScreen.css b/frontend/src/screens/SetupScreen.css new file mode 100644 index 0000000..423f620 --- /dev/null +++ b/frontend/src/screens/SetupScreen.css @@ -0,0 +1,45 @@ +.setup-screen form { + display: flex; + flex-direction: column; + gap: 16px; +} + +.setup-screen label { + display: flex; + flex-direction: column; + gap: 4px; + text-align: left; + font-size: 14px; + color: var(--text-muted); +} + +.setup-screen label.checkbox-label { + flex-direction: row; + align-items: center; + gap: 8px; +} + +.setup-screen input[type="text"], +.setup-screen input[type="number"], +.setup-screen select { + padding: 8px 10px; + border: 1px solid var(--border); + border-radius: 6px; + background: var(--bg); + color: var(--text); +} + +.setup-screen button { + align-self: flex-start; + padding: 10px 20px; + border: none; + border-radius: 6px; + background: var(--accent); + color: var(--accent-contrast); + cursor: pointer; +} + +.setup-screen button:disabled { + opacity: 0.6; + cursor: default; +} diff --git a/frontend/src/screens/SetupScreen.tsx b/frontend/src/screens/SetupScreen.tsx new file mode 100644 index 0000000..c1aa125 --- /dev/null +++ b/frontend/src/screens/SetupScreen.tsx @@ -0,0 +1,119 @@ +import { useState } from "react"; +import { ApiError, createJob } from "../api/client"; +import type { SyncMethod, VideoBackendKind } from "../api/types"; +import { ErrorBanner } from "../components/ErrorBanner"; +import "./SetupScreen.css"; + +interface SetupScreenProps { + onJobCreated: (jobId: string) => void; +} + +export function SetupScreen({ onJobCreated }: SetupScreenProps) { + const [rawFolderPath, setRawFolderPath] = useState(""); + const [outputFolderPath, setOutputFolderPath] = useState(""); + const [method, setMethod] = useState("audio cross-correlation"); + const [videoHandler, setVideoHandler] = useState("deffcode"); + const [brightnessRatioThreshold, setBrightnessRatioThreshold] = useState(1000); + const [createDebugArtifacts, setCreateDebugArtifacts] = useState(true); + const [submitting, setSubmitting] = useState(false); + const [submitError, setSubmitError] = useState(null); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setSubmitting(true); + setSubmitError(null); + try { + const response = await createJob({ + raw_video_folder_path: rawFolderPath, + synchronized_video_folder_path: outputFolderPath.trim() || null, + method, + video_handler: videoHandler, + brightness_ratio_threshold: brightnessRatioThreshold, + create_debug_artifacts: createDebugArtifacts, + }); + onJobCreated(response.job_id); + } catch (e) { + setSubmitError(e instanceof ApiError ? e.detail : "failed to start sync job"); + } finally { + setSubmitting(false); + } + }; + + return ( +
+

New synchronization

+ +
+ + + + + {method === "brightness change detection" && ( + + )} + + + + + + + + +
+
+ ); +} diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json new file mode 100644 index 0000000..6830b6f --- /dev/null +++ b/frontend/tsconfig.app.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023", "DOM"], + "module": "esnext", + "types": ["vite/client"], + "allowArbitraryExtensions": true, + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json new file mode 100644 index 0000000..8455dcb --- /dev/null +++ b/frontend/tsconfig.node.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023"], + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "module": "nodenext", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["vite.config.ts"] +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..8b0f57b --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], +}) From 3a607a5c4f644adf396e0610a71a5944ed5a5337 Mon Sep 17 00:00:00 2001 From: philipqueen Date: Mon, 10 Aug 2026 15:26:55 -0600 Subject: [PATCH 09/16] tauri first shot --- .gitignore | 5 + CONTRIBUTING.md | 118 + README.md | 8 + docs/architecture/01-package-layout.md | 5 +- docs/architecture/04-frontend.md | 10 +- docs/architecture/06-tauri-desktop.md | 71 + docs/architecture/README.md | 44 +- frontend/package-lock.json | 249 + frontend/package.json | 9 +- frontend/src/App.css | 8 + frontend/src/App.tsx | 10 + frontend/src/hooks/useApiReadiness.ts | 15 + frontend/src/hooks/useJobPolling.ts | 53 +- frontend/src/hooks/usePolling.ts | 91 + frontend/src/screens/SetupScreen.css | 15 + frontend/src/screens/SetupScreen.tsx | 50 +- frontend/vite.config.ts | 9 + packaging/pyinstaller/entrypoint.py | 11 + packaging/pyinstaller/skelly-sync-api.spec | 75 + pyproject.toml | 27 + skelly_synchronize/api/main.py | 3 + src-tauri/.gitignore | 4 + src-tauri/Cargo.lock | 4845 ++++++++++++++++++++ src-tauri/Cargo.toml | 27 + src-tauri/build.rs | 3 + src-tauri/capabilities/default.json | 13 + src-tauri/icons/128x128.png | Bin 0 -> 11059 bytes src-tauri/icons/128x128@2x.png | Bin 0 -> 23137 bytes src-tauri/icons/32x32.png | Bin 0 -> 2225 bytes src-tauri/icons/Square107x107Logo.png | Bin 0 -> 9202 bytes src-tauri/icons/Square142x142Logo.png | Bin 0 -> 12530 bytes src-tauri/icons/Square150x150Logo.png | Bin 0 -> 13032 bytes src-tauri/icons/Square284x284Logo.png | Bin 0 -> 25943 bytes src-tauri/icons/Square30x30Logo.png | Bin 0 -> 2078 bytes src-tauri/icons/Square310x310Logo.png | Bin 0 -> 28507 bytes src-tauri/icons/Square44x44Logo.png | Bin 0 -> 3419 bytes src-tauri/icons/Square71x71Logo.png | Bin 0 -> 6027 bytes src-tauri/icons/Square89x89Logo.png | Bin 0 -> 7551 bytes src-tauri/icons/StoreLogo.png | Bin 0 -> 3971 bytes src-tauri/icons/icon.icns | Bin 0 -> 277003 bytes src-tauri/icons/icon.ico | Bin 0 -> 37710 bytes src-tauri/icons/icon.png | Bin 0 -> 49979 bytes src-tauri/src/lib.rs | 93 + src-tauri/src/main.rs | 6 + src-tauri/tauri.conf.json | 49 + uv.lock | 975 ++-- 46 files changed, 6364 insertions(+), 537 deletions(-) create mode 100644 CONTRIBUTING.md create mode 100644 docs/architecture/06-tauri-desktop.md create mode 100644 frontend/src/hooks/useApiReadiness.ts create mode 100644 frontend/src/hooks/usePolling.ts create mode 100644 packaging/pyinstaller/entrypoint.py create mode 100644 packaging/pyinstaller/skelly-sync-api.spec create mode 100644 src-tauri/.gitignore create mode 100644 src-tauri/Cargo.lock create mode 100644 src-tauri/Cargo.toml create mode 100644 src-tauri/build.rs create mode 100644 src-tauri/capabilities/default.json create mode 100644 src-tauri/icons/128x128.png create mode 100644 src-tauri/icons/128x128@2x.png create mode 100644 src-tauri/icons/32x32.png create mode 100644 src-tauri/icons/Square107x107Logo.png create mode 100644 src-tauri/icons/Square142x142Logo.png create mode 100644 src-tauri/icons/Square150x150Logo.png create mode 100644 src-tauri/icons/Square284x284Logo.png create mode 100644 src-tauri/icons/Square30x30Logo.png create mode 100644 src-tauri/icons/Square310x310Logo.png create mode 100644 src-tauri/icons/Square44x44Logo.png create mode 100644 src-tauri/icons/Square71x71Logo.png create mode 100644 src-tauri/icons/Square89x89Logo.png create mode 100644 src-tauri/icons/StoreLogo.png create mode 100644 src-tauri/icons/icon.icns create mode 100644 src-tauri/icons/icon.ico create mode 100644 src-tauri/icons/icon.png create mode 100644 src-tauri/src/lib.rs create mode 100644 src-tauri/src/main.rs create mode 100644 src-tauri/tauri.conf.json diff --git a/.gitignore b/.gitignore index 303eb45..8e47f1e 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,11 @@ MANIFEST # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec +!packaging/pyinstaller/*.spec + +# Tauri desktop shell +src-tauri/target/ +src-tauri/binaries/ # Installer logs pip-log.txt diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..ba9fa29 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,118 @@ +# Contributing + +This covers setting up a dev environment and running/building the project's pieces: +the `core` library, the `api` server, the `frontend`, and the Tauri desktop app. For +architecture background, see [`docs/architecture/`](docs/architecture/README.md). + +## Setup + +Requires [uv](https://docs.astral.sh/uv/), Node.js/npm, and (for the desktop app) the +Rust toolchain (`rustup`). FFmpeg must also be installed and on `PATH` — see the +[README](README.md). + +```bash +uv venv +uv pip install -e ".[api,dev,packaging]" + +cd frontend && npm install && cd .. +``` + +`packaging` pulls in PyInstaller, needed only for building the desktop app's sidecar +binary (see below) — omit it if you're just working on `core`/`api`/`cli`. + +## Running the pieces + +Run Python commands via `uv run ` (or activate the venv with +`source .venv/bin/activate` first, if you'd rather not prefix every command). + +```bash +# Tests +uv run poe test # fast unit + integration tests +uv run poe test-slow # opt-in slow tier (real dataset, network + ffmpeg/deffcode) + +# Formatting (CI runs black --check on PRs) +uv run poe format + +# CLI +uv run skelly-synchronize [-o OUTPUT] [-m audio|brightness] + +# API server (http://127.0.0.1:8000) +uv run skelly-sync-api +``` + +`test`, `test-slow`, and `format` are [poe](https://poethepoet.natn.io/) tasks defined +in `pyproject.toml`'s `[tool.poe.tasks]` — run `uv run poe --help` to list all of them. + +For the frontend dev server (talks to the API server above — run both): + +```bash +cd frontend && npm run dev +``` + +## Desktop app (Tauri) + +The desktop app wraps `frontend` in a Tauri shell that spawns `api` as a managed +sidecar process. Design details: [`docs/architecture/06-tauri-desktop.md`](docs/architecture/06-tauri-desktop.md). +Currently macOS-only. + +### Dev mode + +The venv must be **activated** (not just `uv run`) so the Rust shell can find +`skelly-sync-api` on `PATH` when it spawns it, and the command must run from the +**repo root**, not `frontend/` — Tauri's CLI only finds `src-tauri/` by searching +subdirectories of the current directory, and `src-tauri/` is a sibling of `frontend/`, +not nested inside it. + +```bash +source .venv/bin/activate +npx --prefix frontend tauri dev +``` + +This runs the Vite dev server and `cargo run` together, spawns `skelly-sync-api` from +the activated venv's `PATH`, and opens the app window. The first run compiles the Rust +dependency graph from scratch (a few minutes); subsequent runs are fast. + +### Building the release app + +The release build uses a PyInstaller-frozen `skelly-sync-api` binary as its sidecar. +Freezing isn't wired into `tauri build` itself yet, so it's a separate step first: + +```bash +uv run poe freeze-api +``` + +This freezes `skelly-sync-api` with PyInstaller and places the binary at +`src-tauri/binaries/skelly-sync-api-`, matching Tauri's sidecar naming +convention (see the task's shell script in `pyproject.toml` if you want the manual +equivalent). Rerun it any time the Python side changes; if you're only touching Rust +or frontend code, the previously-frozen binary is reused. + +Then build the bundle: + +```bash +npx --prefix frontend tauri build +``` + +Output: +- `src-tauri/target/release/bundle/macos/Skelly Synchronize.app` +- `src-tauri/target/release/bundle/dmg/Skelly Synchronize_0.1.0_aarch64.dmg` + +### Testing the built app + +```bash +open "src-tauri/target/release/bundle/macos/Skelly Synchronize.app" + +# give the onefile sidecar a few seconds to self-extract and bind, then: +curl http://127.0.0.1:8000/health # expect {"status":"ok"} +``` + +From there, use the app normally: pick a raw video folder (or paste a path), submit a +job, watch it complete. Quit the app normally (Cmd+Q or the menu) rather than `kill`ing +the process directly — a raw `kill` bypasses Tauri's exit-cleanup hook and isn't +representative of real usage. + +To confirm the sidecar didn't leak a process after quitting: + +```bash +ps -ef | grep skelly-sync-api | grep -v grep # should print nothing +``` diff --git a/README.md b/README.md index 7c7e3a5..c1aa208 100644 --- a/README.md +++ b/README.md @@ -21,8 +21,16 @@ npm run dev Open the URL Vite prints (typically `http://localhost:5173`) in your browser. See [`frontend/README.md`](frontend/README.md) for details. +A standalone macOS desktop app (packaged with Tauri, bundling the API so you don't +need to run it separately) is also in progress — see [CONTRIBUTING.md](CONTRIBUTING.md#desktop-app-tauri). + Skelly_synchronize currently depends on FFmpeg, a command line tool that handles the video files. If you do not have FFmpeg downloaded, you will need to install it separately. You can download FFmpeg here: https://ffmpeg.org/download.html +## Contributing + +For dev environment setup (via [uv](https://docs.astral.sh/uv/)), running tests, and +building/testing the desktop app, see [CONTRIBUTING.md](CONTRIBUTING.md). + ## Using Skelly Synchronize Once you have the web UI open, choose a folder of raw videos that you would like to synchronize. The videos must overlap in time to be able to be synchronized. The software currently works with `mp4`, `mkv`, `avi`, `mpeg`, and `mov` files. Once the folder of videos has been selected, you can choose the synchronization method you would like to run. The synchronized videos will be placed in a folder called "synchronized_videos" that will be in the same directory as the folder of raw videos (or in a custom output folder, if one was specified). diff --git a/docs/architecture/01-package-layout.md b/docs/architecture/01-package-layout.md index 6fea6cb..af2fd03 100644 --- a/docs/architecture/01-package-layout.md +++ b/docs/architecture/01-package-layout.md @@ -37,6 +37,9 @@ skelly_synchronize/ │ │ └── schemas.py # API-only wrapper models (e.g. JobCreateResponse) │ └── cli/ # thin argparse/typer wrapper over core, replaces __main__.py ├── frontend/ # React app (Vite + TypeScript), talks to api/ over HTTP only +├── src-tauri/ # Tauri desktop shell — spawns/manages api as a sidecar; see 06-tauri-desktop.md +├── packaging/ +│ └── pyinstaller/ # PyInstaller spec(s) freezing api into the Tauri sidecar binary └── docs/ └── architecture/ ``` @@ -46,7 +49,7 @@ skelly_synchronize/ - `core` has zero knowledge of FastAPI, uvicorn, or PySide6. It only depends on the algorithm libraries it actually needs (numpy, scipy, librosa, opencv, deffcode, pydantic). - `api` depends on `core` (an internal import within the same package — no separate dependency declaration needed). - `cli` depends only on `core` (not `api`). -- `frontend` depends on nothing Python — it talks to `api` over HTTP only. +- `frontend` depends on nothing Python — it talks to `api` over HTTP only. In the packaged desktop app, the Tauri shell (`src-tauri/`) starts and manages `api` as a sidecar process instead of a developer/user starting it by hand — see [06-tauri-desktop.md](06-tauri-desktop.md). `frontend`'s own dependencies gain `@tauri-apps/cli`, `@tauri-apps/api`, `@tauri-apps/plugin-dialog`, and `@tauri-apps/plugin-shell`. ## Single package, `api` as an optional extra diff --git a/docs/architecture/04-frontend.md b/docs/architecture/04-frontend.md index 68635ee..93ff323 100644 --- a/docs/architecture/04-frontend.md +++ b/docs/architecture/04-frontend.md @@ -19,12 +19,12 @@ Design for the React app that replaces the PySide6 desktop GUI. Kept deliberatel Replaces today's GUI, which only exposes 2 of the several parameters `core` actually supports. The new setup screen exposes all of them: -- Raw video folder path — a plain text input for an absolute path, not a browser file picker. (Browsers cannot reliably expose real filesystem *paths* from a picker due to the File System Access API's security model, and since the API and browser run on the same machine here, a plain path field is simpler and fully sufficient — documented explicitly as the reason, not an oversight.) +- Raw video folder path — a text input for an absolute path, plus a "Browse…" button that opens a native OS folder picker via the Tauri shell (`@tauri-apps/plugin-dialog`, see [06-tauri-desktop.md](06-tauri-desktop.md)), which returns a real absolute path directly. (Earlier versions of this doc specified a plain text-only field, reasoning that a browser-only frontend cannot reliably obtain real filesystem *paths* from a picker due to the File System Access API's security model — that constraint no longer applies now that the frontend only runs inside the Tauri shell, which does have real filesystem access.) - Sync method selector (audio / brightness). - Backend selector (ffmpeg / deffcode) — newly exposed; today's GUI has no way to choose this. - Brightness ratio threshold field — shown only when brightness is selected (matches today's one exposed parameter). - Debug-artifacts toggle — newly exposed. -- Optional custom output folder path — newly exposed (`core` already supports overriding `synchronized_video_folder_path`; the GUI never surfaced it). +- Optional custom output folder path — newly exposed (`core` already supports overriding `synchronized_video_folder_path`; the GUI never surfaced it). Same text input + native-picker "Browse…" pattern as the raw folder path field above. - "Start sync" button — `POST /jobs`, then navigates to the Job Progress screen with the returned `job_id`. ### Job Progress screen @@ -54,9 +54,7 @@ Minimal, plain CSS (or CSS Modules) — no heavyweight design system or componen ## Dev / run model -**v1**: two processes — `skelly-sync-api` (FastAPI/uvicorn on `127.0.0.1:8000`) and `npm run dev` (Vite dev server, proxying API calls to the FastAPI port). The user runs both and opens the Vite dev server URL in a browser. - -Bundling the frontend as static files served directly by FastAPI (a single-process app, closer to the original desktop-app feel) is deferred to a later polish phase, once the API/frontend split has proven itself — not attempted in the initial rewrite. +Superseded by [06-tauri-desktop.md](06-tauri-desktop.md): the frontend runs only inside a Tauri desktop shell, which manages the API as a sidecar process, rather than a developer/user starting `skelly-sync-api` and a Vite dev server by hand and opening a browser tab. `npm run tauri dev` (dev) / `npm run tauri build` (release) replace the old two-process model. ## Location @@ -64,4 +62,4 @@ Lives in-repo under `frontend/`, per the monorepo decision in [01-package-layout ## Known issues resolved by this document -KI-22 (blocking, feedback-less sync UI replaced by an async progress screen with error display), and the parameter-exposure gap noted in the current GUI (only 2 of several `core` parameters were ever surfaced). +KI-22 (blocking, feedback-less sync UI replaced by an async progress screen with error display), and the parameter-exposure gap noted in the current GUI (only 2 of several `core` parameters were ever surfaced). The folder-picker limitation noted earlier in this document is now actually resolved rather than merely accepted, once the frontend runs inside the Tauri shell — see [06-tauri-desktop.md](06-tauri-desktop.md). diff --git a/docs/architecture/06-tauri-desktop.md b/docs/architecture/06-tauri-desktop.md new file mode 100644 index 0000000..70a8437 --- /dev/null +++ b/docs/architecture/06-tauri-desktop.md @@ -0,0 +1,71 @@ +# Tauri Desktop Packaging + +## Purpose + +Design for wrapping the React frontend ([04-frontend.md](04-frontend.md)) and the FastAPI server ([03-api-design.md](03-api-design.md)) into a single distributable desktop app, using [Tauri](https://tauri.app/) 2 as the native shell. This replaces the "run two processes, open a browser tab" dev/run model with one packaged app a user can launch directly, and gives the frontend a real native OS folder picker in the process. + +## Tech choices + +- **Tauri 2** (Rust shell + the OS's system webview — no bundled Chromium/Node runtime, unlike Electron). +- **`tauri-plugin-dialog`** for the native folder-picker dialog. +- **`tauri-plugin-shell`** for spawning and managing the API as a child process (a "sidecar"). +- **PyInstaller** to freeze the Python API into a standalone binary — see "Packaging" below. + +## Process architecture + +The Tauri Rust shell owns the API process's lifecycle instead of a developer/user starting it by hand: + +- On app startup, Rust spawns the API. Release builds use the bundled PyInstaller sidecar binary via `tauri-plugin-shell`'s `Command::sidecar`; dev builds spawn `skelly-sync-api` directly from `PATH` (relying on the activated Python venv), selected via `cfg!(debug_assertions)`. This avoids needing a frozen binary on every dev iteration. +- The API keeps binding `127.0.0.1:8000`, unchanged from today. **Documented v1 simplification**: no dynamic port negotiation between Rust and the API — if port 8000 is already in use on a user's machine, startup fails. Revisit with a random free port passed to the sidecar's args and forwarded to the frontend via Tauri IPC if this becomes a real problem. +- The frontend polls `GET /health` on mount with retry/backoff and shows a "Starting sync engine…" loading state until it responds, before rendering the Setup screen. This reuses the same polling pattern already established by `useJobPolling` ([04-frontend.md](04-frontend.md)) rather than introducing a separate Rust↔JS readiness signal. +- Shutdown: Rust kills the child process handle when the app exits. A hard kill is acceptable here — the job store is in-memory/ephemeral by design ([03-api-design.md](03-api-design.md)), and each sync job already runs in its own isolated `multiprocessing.Process` ([`skelly_synchronize/api/jobs.py`](../../skelly_synchronize/api/jobs.py)), so there's no shared state to corrupt on an abrupt exit. + +## Folder selection + +`SetupScreen` gets a "Browse…" button using `@tauri-apps/plugin-dialog`'s `open({ directory: true })`, which returns a real OS-native absolute path directly. The text field stays editable alongside the button, for manual entry/power users. + +This resolves the file-picker limitation [04-frontend.md](04-frontend.md) previously documented as an accepted limitation of running in a plain browser (browsers cannot reliably expose real filesystem paths from a picker, per the File System Access API's security model) — that constraint doesn't apply once the frontend only runs inside the Tauri shell. It also retires an earlier idea of adding a server-side `/browse` endpoint that would let the frontend walk the filesystem itself; unnecessary once a native dialog is available. + +## Packaging (PyInstaller sidecar) + +A new `packaging/pyinstaller/skelly-sync-api.spec` freezes the `skelly-sync-api` entry point — and its heavy dependencies (numpy, scipy, librosa, opencv-contrib, deffcode) — into a standalone binary. The output is renamed per Tauri's sidecar convention (`skelly-sync-api-`, e.g. `skelly-sync-api-aarch64-apple-darwin`) and placed in `src-tauri/binaries/`, referenced by `tauri.conf.json`'s `bundle.externalBin` so `tauri build` bundles it into the app. + +**Primary technical risk**: the job execution model in `skelly_synchronize/api/jobs.py` depends on `multiprocessing.Process` and `multiprocessing.Manager()` for per-job isolation and progress bridging. Frozen executables have well-known multiprocessing wrinkles — the entry point needs `multiprocessing.freeze_support()` guarding, and spawn-method behavior differs under PyInstaller's onefile vs onedir modes (worse on Windows, but not risk-free on macOS either). **Recommendation**: freeze the sidecar and run one real sync job through it as an early spike, before investing in the rest of the Tauri shell — this is the piece most likely to need rework if it doesn't work cleanly on the first attempt. + +**FFmpeg** stays an external system dependency for v1 — it is not bundled into the app or the sidecar binary. This is consistent with the project's existing FFmpeg requirement (see the root `README.md`) and is a deliberate, documented v1 limitation in the same style as other accepted-for-now decisions in this rewrite (e.g. KI-11, KI-16 in [00-known-issues.md](00-known-issues.md)). Revisit bundling a static ffmpeg binary as an app resource in a later pass if the external-dependency requirement proves to be a real adoption blocker. + +## Repo layout addition + +``` +src-tauri/ +├── Cargo.toml +├── tauri.conf.json +├── capabilities/ # Tauri 2 permission grants (dialog, shell/sidecar) +├── icons/ +├── binaries/ # frozen sidecar binaries land here; git-ignored, built via packaging/pyinstaller +└── src/ + ├── main.rs + └── lib.rs # app setup: spawn/track/kill the API child process +packaging/ +└── pyinstaller/ + └── skelly-sync-api.spec +``` + +`frontend/package.json` gains `@tauri-apps/cli` (dev dependency, provides `npm run tauri ...`), plus runtime dependencies `@tauri-apps/api`, `@tauri-apps/plugin-dialog`, `@tauri-apps/plugin-shell`. + +## Dev vs release workflow + +- **Dev**: `npm run tauri dev` (via `@tauri-apps/cli`). Tauri's `devUrl` points at the Vite dev server; `beforeDevCommand` runs `npm run dev` inside `frontend/`. Rust spawns `skelly-sync-api` from the activated venv's `PATH` rather than a frozen binary, so dev iteration doesn't require re-running PyInstaller. +- **Release**: `npm run tauri build`. `beforeBuildCommand` runs `npm run build` inside `frontend/`. The PyInstaller sidecar must be frozen first — a separate, manual step for now (not yet wired into `tauri build` itself) — and placed in `src-tauri/binaries/` before `tauri build` bundles it via `externalBin`. Automating this handoff (e.g. a `beforeBundleCommand` or a wrapper script) is a reasonable follow-up once the manual flow is proven to work. + +This retires the "two-process, opened in a plain browser tab" dev/run model documented in [04-frontend.md](04-frontend.md) — that model is superseded by `tauri dev`. + +## Platform scope + +Initial target is macOS, the primary dev machine. Windows and Linux each need their own PyInstaller-frozen sidecar binary (built on/for that target triple) and their own bundle testing — this is not assumed to come "for free" from getting macOS working, and is called out explicitly so it doesn't silently become a gap when the app is eventually shared with users on other platforms. + +## Known issues / limitations resolved by this document + +- Resolves the file-picker limitation noted in [04-frontend.md](04-frontend.md) (previously an accepted limitation of the browser-only frontend; now actually fixed via a native dialog). +- Supersedes the two-process browser dev/run model in [04-frontend.md](04-frontend.md). +- No `KI-##` item from [00-known-issues.md](00-known-issues.md) maps directly to this doc — all of those describe the pre-rewrite codebase; desktop packaging is new scope introduced after the rewrite's original plan. diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 9a41d88..44bb4b9 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -6,9 +6,9 @@ This directory describes the **target** architecture for the `skelly_synchronize 1. Improve the code quality of the core synchronization library — typed data models, a proper backend abstraction, unified pipeline orchestration, and fixes to the concurrency/correctness smells listed in [00-known-issues.md](00-known-issues.md). 2. Add a FastAPI server exposing sync functionality over HTTP. -3. Replace the PySide6 desktop GUI with a simple React frontend. +3. Replace the PySide6 desktop GUI with a React frontend, packaged as a standalone Tauri desktop app. -**Done** looks like: a `core` library with typed models and no known-issue regressions, a FastAPI server backing all the functionality the old GUI exposed (plus previously-hidden parameters it never surfaced), and a React frontend at feature parity with the old GUI — after which the PySide6 GUI package is deleted. +**Done** looks like: a `core` library with typed models and no known-issue regressions, a FastAPI server backing all the functionality the old GUI exposed (plus previously-hidden parameters it never surfaced), and a React frontend at feature parity with the old GUI, packaged into one Tauri desktop app — after which the PySide6 GUI package is deleted. ## Document map @@ -20,27 +20,34 @@ This directory describes the **target** architecture for the `skelly_synchronize | [03-api-design.md](03-api-design.md) | `skelly_synchronize.api` design: FastAPI endpoints, schemas, job model, progress bridge. | | [04-frontend.md](04-frontend.md) | React app: screens, API client, state management approach. | | [05-testing-strategy.md](05-testing-strategy.md) | Test pyramid across `core`/`api`/frontend, fixture redesign, CI plan. | +| [06-tauri-desktop.md](06-tauri-desktop.md) | Tauri desktop shell: sidecar process model for `api`, native folder picker, PyInstaller packaging, dev/release workflow. | Read in the numbered order above — each later doc assumes the decisions made in the earlier ones (package boundaries before core design, core design before the API that wraps it, API before the frontend that consumes it). ## Target architecture at a glance ``` - ┌────────────┐ - │ frontend │ React (Vite + TS) — talks HTTP only - └─────┬──────┘ - │ HTTP (polling) - ┌─────▼──────┐ - │ api │ FastAPI (skelly_synchronize.api) — job orchestration, HTTP boundary - └─────┬──────┘ - │ Python calls - ┌─────▼──────┐ ┌────────────┐ - │ core │◄──────┤ cli │ both depend only on core - │(skelly_synchronize.core) └──────┘ - └────────────┘ + ┌───────────────────────────────────┐ + │ Tauri desktop app │ + │ │ + │ ┌────────────┐ │ + │ │ frontend │ React (Vite + TS) │ + │ └─────┬──────┘ │ + │ │ HTTP (polling) │ + │ ┌─────▼──────┐ │ + │ │ api │ spawned as a │ + │ └─────┬──────┘ managed sidecar │ + │ │ process — see │ + │ │ 06-tauri-desktop │ + └─────────┼───────────────────────────┘ + │ Python calls + ┌─────▼──────┐ ┌────────────┐ + │ core │◄──────┤ cli │ both depend only on core + │(skelly_synchronize.core) └──────┘ + └────────────┘ ``` -`core` has no knowledge of `api` or `frontend`. `api` and `cli` are both thin consumers of `core`, so the sync engine is usable standalone (scriptable, embeddable elsewhere within this same package) independent of whether the API/frontend exist at all. `core`, `api`, and `cli` all live in one PyPI distribution (`skelly_synchronize`), with `api`'s dependencies behind an optional extra — see [01-package-layout.md](01-package-layout.md) for the full package tree and the rationale for keeping this as one package for now. +`core` has no knowledge of `api`, `frontend`, or the Tauri shell. `api` and `cli` are both thin consumers of `core`, so the sync engine is usable standalone (scriptable, embeddable elsewhere within this same package) independent of whether the API/frontend/desktop shell exist at all. `core`, `api`, and `cli` all live in one PyPI distribution (`skelly_synchronize`), with `api`'s dependencies behind an optional extra — see [01-package-layout.md](01-package-layout.md) for the full package tree and the rationale for keeping this as one package for now. The Tauri shell (`src-tauri/`) is a separate, non-Python part of the same repo — see [06-tauri-desktop.md](06-tauri-desktop.md). ## Non-goals @@ -57,7 +64,8 @@ The rewrite proceeds in phases. **Each phase ends with the tool still fully usab 2. **Phase 1 — finish the `core` rewrite.** Complete the pipeline abstraction, the brightness path, the `DeffcodeBackend`, the audio subsystem, and debug artifacts. The old GUI now runs entirely against the new `core` library — this phase proves `core`'s public surface is sufficient before any API work begins. 3. **Phase 2 — FastAPI layer.** Build `skelly_synchronize.api` wrapping the now-finished `core`, starting with an in-memory job store. Test manually via the FastAPI-generated `/docs` UI — no frontend exists yet. 4. **Phase 3 — React frontend.** Build the frontend against the FastAPI layer from Phase 2. Once it reaches feature parity with the old GUI, delete the PySide6 GUI package entirely. -5. **Phase 4 — cleanup.** Remove any remaining old dict-based code paths, finalize packaging/CI per [01-package-layout.md](01-package-layout.md) and [05-testing-strategy.md](05-testing-strategy.md), update the top-level README, and tag a release. +5. **Phase 4 — Tauri desktop packaging.** Wrap the frontend in a Tauri shell that manages the API as a sidecar process (per [06-tauri-desktop.md](06-tauri-desktop.md)), freeze the API with PyInstaller, and add a native folder picker. Ends with a distributable standalone desktop app. +6. **Phase 5 — cleanup.** Remove any remaining old dict-based code paths, finalize packaging/CI per [01-package-layout.md](01-package-layout.md) and [05-testing-strategy.md](05-testing-strategy.md), update the top-level README, and tag a release. ## Key decisions at a glance @@ -70,3 +78,7 @@ The rewrite proceeds in phases. **Each phase ends with the tool still fully usab | Progress reporting | `core` exposes a generic callback hook; `api` supplies the actual mechanism | Keeps `core` deployment-agnostic. [02-core-library.md](02-core-library.md), [03-api-design.md](03-api-design.md) | | Typed data model approach | Pydantic throughout `core`/`api` (raw audio arrays excluded) | Avoids a dataclass↔Pydantic translation layer since FastAPI already requires Pydantic. [02-core-library.md](02-core-library.md) | | Long-running job UX | Async job + polling (not WebSockets) | Simple, sufficient for a local single-user app. [03-api-design.md](03-api-design.md) | +| Desktop shell | Tauri 2 (system webview, not Electron) | Wraps the existing React frontend unchanged; small bundle size vs. a bundled-Chromium alternative. [06-tauri-desktop.md](06-tauri-desktop.md) | +| Python packaging for desktop | PyInstaller sidecar, spawned/managed by the Tauri shell | True standalone app, no separate Python install required by end users. [06-tauri-desktop.md](06-tauri-desktop.md) | +| Folder selection | Native OS dialog via `tauri-plugin-dialog` | Real fix for the browser file-picker limitation, once the frontend only runs inside Tauri — no server-side browse endpoint needed. [06-tauri-desktop.md](06-tauri-desktop.md) | +| FFmpeg bundling | Stays an external system dependency, not bundled into the app | Avoids binary-bundling/licensing complexity for v1; consistent with the project's existing FFmpeg requirement. [06-tauri-desktop.md](06-tauri-desktop.md) | diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 2619f26..1c1250a 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,10 +8,14 @@ "name": "frontend", "version": "0.0.0", "dependencies": { + "@tauri-apps/api": "^2", + "@tauri-apps/plugin-dialog": "^2", + "@tauri-apps/plugin-shell": "^2", "react": "^19.2.8", "react-dom": "^19.2.8" }, "devDependencies": { + "@tauri-apps/cli": "^2.11.4", "@types/node": "^24.13.3", "@types/react": "^19.2.17", "@types/react-dom": "^19.2.3", @@ -599,6 +603,251 @@ "dev": true, "license": "MIT" }, + "node_modules/@tauri-apps/api": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.11.1.tgz", + "integrity": "sha512-M2FPuYND2m+wh5hfW9ZpSdxMPdEJovPBWwoHJmwUpysTYNHaOkVFN419m/K0LIgjb/7KU2vBgsUepJWugQCvAA==", + "license": "Apache-2.0 OR MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + } + }, + "node_modules/@tauri-apps/cli": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.11.4.tgz", + "integrity": "sha512-R8xGtMpwyetawSqm9kYOuMmEqkhUbvcUy8n0aNXIxollKBLESUu5f4Fx+64hgASYm1H+jSWq6jCW6zqTnH6hqQ==", + "dev": true, + "license": "Apache-2.0 OR MIT", + "bin": { + "tauri": "tauri.js" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + }, + "optionalDependencies": { + "@tauri-apps/cli-darwin-arm64": "2.11.4", + "@tauri-apps/cli-darwin-x64": "2.11.4", + "@tauri-apps/cli-linux-arm-gnueabihf": "2.11.4", + "@tauri-apps/cli-linux-arm64-gnu": "2.11.4", + "@tauri-apps/cli-linux-arm64-musl": "2.11.4", + "@tauri-apps/cli-linux-riscv64-gnu": "2.11.4", + "@tauri-apps/cli-linux-x64-gnu": "2.11.4", + "@tauri-apps/cli-linux-x64-musl": "2.11.4", + "@tauri-apps/cli-win32-arm64-msvc": "2.11.4", + "@tauri-apps/cli-win32-ia32-msvc": "2.11.4", + "@tauri-apps/cli-win32-x64-msvc": "2.11.4" + } + }, + "node_modules/@tauri-apps/cli-darwin-arm64": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.11.4.tgz", + "integrity": "sha512-1ryOF3ZhpZ/nemHV5zVwBQBz9jDGKmKPvWPADOhc83ig0P4bMc2iER4NbC6r9sjeIZ6RVQ4g3RZIYvezhcl4TQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-darwin-x64": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.11.4.tgz", + "integrity": "sha512-uFsGQAAfuyz1k/yGLmkWfkBlgKAqZfxqlHmLWx81QU27RJWfmbNHCIq8T8w1e+VClleIuZUjpHWfoE4E3DLo3A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.11.4.tgz", + "integrity": "sha512-IaHZn5CdBL21oUmjiVOS1ctw6Ip1O0pjp70FwOWmYz1myWe0SY96ZIj2FYf7pT0m8bI2h/hrs5ZbEXXh44/MkQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-gnu": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.11.4.tgz", + "integrity": "sha512-N41/ukTRVe6XSuUTESuFdGeOW2i7k62tK+6gHK5Kd5/q5RPvvi19GaWAVPPb9u95HSGmTChSolBfzynUsssFaA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-musl": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.11.4.tgz", + "integrity": "sha512-v277UnT/fB64xAfSroL5N3Km3tLmvATWqJJw/wRI+g6o+HkeD0slyE7gOhNs1MbjE41R7bQOTxMVoL3aomUJmw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-riscv64-gnu": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.11.4.tgz", + "integrity": "sha512-qqgNkQ2u1yZHxjhxsZaxUtRDW8dIqIYm33rx/mzwQv0SfY9x1B+iraj8vWeFiXjjSVVhEMepXSOts1TqPzvXNQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-gnu": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.11.4.tgz", + "integrity": "sha512-2VRNWl84FOH0m2giiDkO2h0QXlcMJeX+zJDpI5kDIQAx6s+geF3v48F4DXfJez4GS/FdoDGnPnw1C2iYGbQ7bQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-musl": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.11.4.tgz", + "integrity": "sha512-o9GyhYor/nc7xarmwDE3ka2szuW3uuZzXjHWh64Q8YX5AtSgxdQkFWzrY4O8KiGtVNvFBI14H3Q49Qj5TOIP/A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-arm64-msvc": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.11.4.tgz", + "integrity": "sha512-ld5Ehb598m0VkYyylRPNeCFsBe/km0jxis6KgMpl3IGY6I/i1RwQXO05I1AsXUXO2WC6AvB/Lw4qTf/asiuEiQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-ia32-msvc": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.11.4.tgz", + "integrity": "sha512-12Hxi0XX/H5VFxO/bGgHkFWhml9VMgEOu9CidjeCeTNQ1l6fpUlbiGgSP7CLI3PFtW9/FfbeHieZ+kyWK5H7CA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-x64-msvc": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.11.4.tgz", + "integrity": "sha512-+vDiqBIU5dMISg/wNvX3sF+ZHfgJGJ5T0AcO+EHNXV9GGAG+P5fzodlDXD3QdKCRgZxMoCm5PPvj3BqLNjBthw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/plugin-dialog": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.7.2.tgz", + "integrity": "sha512-pX0IGm1I3I6wc+zeKYcq1GSqogK6okCNX5fOdaNU5ab1AjGS6l1E5wFNjEb7meg7ZFSp0JUs+0jQGQNyOvLrsg==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.11.0" + } + }, + "node_modules/@tauri-apps/plugin-shell": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-shell/-/plugin-shell-2.3.5.tgz", + "integrity": "sha512-jewtULhiQ7lI7+owCKAjc8tYLJr92U16bPOeAa472LHJdgaibLP83NcfAF2e+wkEcA53FxKQAZ7byDzs2eeizg==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.10.1" + } + }, "node_modules/@types/node": { "version": "24.13.3", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz", diff --git a/frontend/package.json b/frontend/package.json index d5a090e..ff487f8 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -7,13 +7,18 @@ "dev": "vite", "build": "tsc -b && vite build", "lint": "oxlint", - "preview": "vite preview" + "preview": "vite preview", + "tauri": "tauri" }, "dependencies": { "react": "^19.2.8", - "react-dom": "^19.2.8" + "react-dom": "^19.2.8", + "@tauri-apps/api": "^2", + "@tauri-apps/plugin-dialog": "^2", + "@tauri-apps/plugin-shell": "^2" }, "devDependencies": { + "@tauri-apps/cli": "^2.11.4", "@types/node": "^24.13.3", "@types/react": "^19.2.17", "@types/react-dom": "^19.2.3", diff --git a/frontend/src/App.css b/frontend/src/App.css index 1cd9de7..cb1c712 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -20,3 +20,11 @@ .app-header nav a:hover { text-decoration: underline; } + +.app-loading { + display: flex; + align-items: center; + justify-content: center; + height: 100vh; + color: var(--text-secondary, #666); +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 345d97b..444bf79 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -4,6 +4,7 @@ import { SetupScreen } from "./screens/SetupScreen"; import { ProgressScreen } from "./screens/ProgressScreen"; import { ResultScreen } from "./screens/ResultScreen"; import { HistoryScreen } from "./screens/HistoryScreen"; +import { useApiReadiness } from "./hooks/useApiReadiness"; type View = | { screen: "setup" } @@ -13,6 +14,15 @@ type View = function App() { const [view, setView] = useState({ screen: "setup" }); + const { ready } = useApiReadiness(); + + if (!ready) { + return ( +
+

Starting sync engine…

+
+ ); + } return ( <> diff --git a/frontend/src/hooks/useApiReadiness.ts b/frontend/src/hooks/useApiReadiness.ts new file mode 100644 index 0000000..89a9b1a --- /dev/null +++ b/frontend/src/hooks/useApiReadiness.ts @@ -0,0 +1,15 @@ +import { getHealth } from "../api/client"; +import { usePolling } from "./usePolling"; + +const READINESS_BACKOFF_MS = [500, 1000, 2000, 3000]; + +/** Polls GET /health with backoff until the Tauri-spawned API sidecar is up. */ +export function useApiReadiness(): { ready: boolean } { + const { value } = usePolling<{ status: string }>({ + fetcher: () => getHealth(), + isTerminal: (latest) => latest.status === "ok", + backoffMs: READINESS_BACKOFF_MS, + }); + + return { ready: value?.status === "ok" }; +} diff --git a/frontend/src/hooks/useJobPolling.ts b/frontend/src/hooks/useJobPolling.ts index 9750d6e..9ee2e49 100644 --- a/frontend/src/hooks/useJobPolling.ts +++ b/frontend/src/hooks/useJobPolling.ts @@ -1,6 +1,6 @@ -import { useEffect, useState } from "react"; -import { ApiError, getJob } from "../api/client"; +import { getJob } from "../api/client"; import type { Job, JobStatus, SyncResult } from "../api/types"; +import { usePolling } from "./usePolling"; const POLL_INTERVAL_MS = 1000; @@ -16,46 +16,13 @@ interface JobPollingState { } export function useJobPolling(jobId: string | null): JobPollingState { - const [job, setJob] = useState(null); - const [fetchError, setFetchError] = useState(null); - - useEffect(() => { - setJob(null); - setFetchError(null); - - if (jobId === null) { - return; - } - - let cancelled = false; - let intervalId: ReturnType | null = null; - - const poll = async () => { - try { - const latest = await getJob(jobId); - if (cancelled) return; - setJob(latest); - setFetchError(null); - if (TERMINAL_STATUSES.includes(latest.status) && intervalId !== null) { - clearInterval(intervalId); - intervalId = null; - } - } catch (e) { - if (cancelled) return; - setFetchError(e instanceof ApiError ? e.detail : "failed to fetch job status"); - } - }; - - poll(); - intervalId = setInterval(poll, POLL_INTERVAL_MS); - - return () => { - cancelled = true; - if (intervalId !== null) { - clearInterval(intervalId); - } - }; - }, [jobId]); + const { value: job, error } = usePolling({ + fetcher: () => getJob(jobId as string), + isTerminal: (latest) => TERMINAL_STATUSES.includes(latest.status), + intervalMs: POLL_INTERVAL_MS, + enabled: jobId !== null, + restartKey: jobId, + }); return { job, @@ -63,6 +30,6 @@ export function useJobPolling(jobId: string | null): JobPollingState { progress: job?.progress ?? 0, progressMessage: job?.progress_message ?? null, result: job?.result ?? null, - error: fetchError ?? job?.error ?? null, + error: error ?? job?.error ?? null, }; } diff --git a/frontend/src/hooks/usePolling.ts b/frontend/src/hooks/usePolling.ts new file mode 100644 index 0000000..6ee6ee9 --- /dev/null +++ b/frontend/src/hooks/usePolling.ts @@ -0,0 +1,91 @@ +import { useEffect, useState } from "react"; + +interface PollingOptions { + /** Called on every poll tick. Return the latest value. */ + fetcher: () => Promise; + /** Stop polling once this returns true for the latest fetched value. */ + isTerminal: (value: T) => boolean; + /** Fixed poll interval in ms. Mutually exclusive with `backoffMs`. */ + intervalMs?: number; + /** + * Capped exponential backoff schedule (ms) used instead of a fixed + * interval, e.g. [500, 1000, 2000, 3000] repeating the last value once + * exhausted. Useful when the thing being polled may take a moment to + * become available (e.g. waiting for a just-spawned process to bind its + * port). + */ + backoffMs?: number[]; + enabled?: boolean; + /** Polling restarts whenever this value changes (e.g. a job id). */ + restartKey?: string | number | null; +} + +interface PollingState { + value: T | null; + error: string | null; +} + +/** Generic interval/backoff poller: fetch, store latest value, stop on terminal. */ +export function usePolling({ + fetcher, + isTerminal, + intervalMs, + backoffMs, + enabled = true, + restartKey = null, +}: PollingOptions): PollingState { + const [value, setValue] = useState(null); + const [error, setError] = useState(null); + + useEffect(() => { + setValue(null); + setError(null); + + if (!enabled) { + return; + } + + let cancelled = false; + let timeoutId: ReturnType | null = null; + let attempt = 0; + + const nextDelay = () => { + if (backoffMs && backoffMs.length > 0) { + const delay = backoffMs[Math.min(attempt, backoffMs.length - 1)]; + attempt += 1; + return delay; + } + return intervalMs ?? 1000; + }; + + const poll = async () => { + try { + const latest = await fetcher(); + if (cancelled) return; + setValue(latest); + setError(null); + if (isTerminal(latest)) { + return; + } + } catch (e) { + if (cancelled) return; + setError(e instanceof Error ? e.message : "polling failed"); + } + if (!cancelled) { + timeoutId = setTimeout(poll, nextDelay()); + } + }; + + poll(); + + return () => { + cancelled = true; + if (timeoutId !== null) { + clearTimeout(timeoutId); + } + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [enabled, restartKey]); + + return { value, error }; +} diff --git a/frontend/src/screens/SetupScreen.css b/frontend/src/screens/SetupScreen.css index 423f620..ba42ff1 100644 --- a/frontend/src/screens/SetupScreen.css +++ b/frontend/src/screens/SetupScreen.css @@ -29,6 +29,21 @@ color: var(--text); } +.setup-screen .path-input-row { + display: flex; + gap: 8px; +} + +.setup-screen .path-input-row input[type="text"] { + flex: 1; +} + +.setup-screen .path-input-row button { + align-self: auto; + padding: 8px 14px; + white-space: nowrap; +} + .setup-screen button { align-self: flex-start; padding: 10px 20px; diff --git a/frontend/src/screens/SetupScreen.tsx b/frontend/src/screens/SetupScreen.tsx index c1aa125..8c5b14d 100644 --- a/frontend/src/screens/SetupScreen.tsx +++ b/frontend/src/screens/SetupScreen.tsx @@ -1,4 +1,5 @@ import { useState } from "react"; +import { open } from "@tauri-apps/plugin-dialog"; import { ApiError, createJob } from "../api/client"; import type { SyncMethod, VideoBackendKind } from "../api/types"; import { ErrorBanner } from "../components/ErrorBanner"; @@ -18,6 +19,13 @@ export function SetupScreen({ onJobCreated }: SetupScreenProps) { const [submitting, setSubmitting] = useState(false); const [submitError, setSubmitError] = useState(null); + const handleBrowse = async (setPath: (path: string) => void) => { + const selected = await open({ directory: true, multiple: false }); + if (typeof selected === "string") { + setPath(selected); + } + }; + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setSubmitting(true); @@ -46,13 +54,21 @@ export function SetupScreen({ onJobCreated }: SetupScreenProps) {