From 9df879e76ee686adef5b4be70ef1953db71ebc4e Mon Sep 17 00:00:00 2001 From: Ismael Date: Tue, 1 Sep 2026 22:14:22 -0700 Subject: [PATCH 01/19] add agents guidance pytest path and skill contract guard --- AGENTS.md | 111 +++++++++++++++++++++++++++++++++++ helpers/timeline_view.py | 2 +- pyproject.toml | 5 +- tests/test_skill_contract.py | 63 ++++++++++++++++++++ 4 files changed, 179 insertions(+), 2 deletions(-) create mode 100644 AGENTS.md create mode 100644 tests/test_skill_contract.py diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..6668a308 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,111 @@ +# video-use repository context + +video-use is a public, conversation-driven video production framework. Changes +made here may be packaged into the skill and reused by people with different +machines, media, workflows, providers, brands, and output goals. Treat the +repository as a general product, never as one person's customized clone. + +## Product principles + +- The delivered video is the product. Prioritize visual and audio quality, + editorial judgment, factual correctness, synchronization, pacing, and + production reliability. +- Design reusable contracts and capabilities. Do not hardcode personal paths, + credentials, account ids, prompts, brands, preferences, lane history, or + assumptions about one project. +- Keep provider-specific behavior behind narrow boundaries. Core EDL validation, + rendering, reframing, and QC must remain usable without the localhost GUI or + Modal. +- Preserve backwards compatibility when practical. If a format must change, + provide a clear migration path and reject unsupported input with an actionable + error. +- Never silently downgrade a requested feature. A missing source, track, model, + codec, or dependency should fail before expensive work begins and explain what + is required. +- Defaults should be safe and broadly useful, while explicit project or user + requirements always win. +- Keep credentials out of source, logs, fixtures, prompts, and generated + artifacts. Configuration belongs in environment variables or provider secret + stores. + +## Architecture boundaries + +- `SKILL.md` defines the agent workflow and public editing contract. +- `helpers/` contains provider-independent production tools and validation. +- `skills/` contains focused companion skills and reusable production assets. +- `gui/` is an optional internal client and remote-execution adapter. It may + orchestrate core features but must not become the only place those features + exist. +- `tests/` and `gui/tests/` protect public behavior and adapter behavior + respectively. + +Keep decision data explicit in portable project files such as `edit/edl.json`. +Renderers should consume declared inputs deterministically. UI state, agent +history, and cloud runtime state must not be required to reproduce an output. + +## Experimental GUI harness + +The four-lane GUI in `gui/` exists to generate many independent outputs quickly +so developers can find ugly frames, weak editorial decisions, unsupported +queries, and renderer failures before video-use users encounter them. + +- Use the four parallel Modal lanes for this branch's output-discovery runs. + Avoid one-off manual local benchmark renders when the harness can run the same + experiment and preserve comparable evidence. +- Do not assume the public video-use workflow will adopt the GUI, Modal, its lane + model, or its run-storage APIs. Promote a finding into `helpers/`, `skills/`, + or `SKILL.md` only when the underlying capability is generally useful without + the harness. +- Keep each lane's agent context fresh. Past run traces are evaluation evidence + for developers and must not leak into a new lane's creative context. +- Preserve every run's durable evidence through `gui/run_store.py`: the compact + `run_summary.md`, deduplicated `trace.json`, raw Codex protocol when available, + complete `render.log`, generated edit handoff, and returned outputs. +- Prefer `run_summary.md` when an agent needs quick context. Read the raw trace or + protocol only to investigate a specific decision, tool call, or failure. +- Pin valuable successes and representative failures before cleanup. Never + delete a pinned run, and never delete an active lane. + +## Change workflow + +1. Identify whether a change belongs to the public editing contract, a reusable + helper, a focused skill, or an optional adapter. +2. Implement the smallest complete general capability at the lowest reusable + layer. Wire adapters to that capability instead of duplicating it. +3. Validate inputs locally before uploads or paid compute. Validate again at + remote execution boundaries. +4. Add tests for successful use, invalid input, backwards compatibility, and + provider-boundary behavior where relevant. +5. For render changes, create representative media and inspect the encoded + dimensions, duration, frame rate, visual framing, and audible output. +6. Update the public EDL example or usage documentation whenever users or agents + need to author a new field. + +Cost and latency are secondary unless the user sets a budget or deadline. Improve +them only when output quality and reliability remain equal or improve. + +## Communication and commits + +After code changes, summarize the affected files, the functions or contracts +added, and what each does in plain language. Keep this technical context compact +so someone can learn an unfamiliar codebase without reading every diff. + +Write simple, readable commit messages. Prefer short lowercase wording without +punctuation. + +## Branch discipline + +Several agents work on this repository at once. To keep one agent's progress +from being overwritten by another: + +- One feature per branch, one agent per branch. Never edit a worktree that + belongs to another branch; take files from a commit or tag instead. +- Commit early. Uncommitted work in a worktree has no merge base and no + history, so a later sync silently discards it. +- Hard rules in `SKILL.md` are append-only. New rules get the next number. + Removing or renumbering a rule requires an explicit reason in the commit. +- Procedure prose belongs in `references/.md`. Edits to `SKILL.md` + are limited to rules, helper-index bullets, directory-tree lines, the EDL + example, and one-line pointers to the reference files. +- `tests/test_skill_contract.py` checks that the rules and every referenced + path still exist. Run it before committing a `SKILL.md` change. diff --git a/helpers/timeline_view.py b/helpers/timeline_view.py index 0b8f72a7..68a74515 100644 --- a/helpers/timeline_view.py +++ b/helpers/timeline_view.py @@ -54,7 +54,7 @@ def extract_frames(video: Path, start: float, end: float, n: int, dest_dir: Path "-i", str(video), "-frames:v", "1", "-q:v", "4", - "-vf", "scale=320:-2", + "-vf", "scale=320:-2,format=yuvj420p", str(out), ] subprocess.run(cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) diff --git a/pyproject.toml b/pyproject.toml index c2cff294..02531dfe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "video-use" version = "0.1.0" -description = "Conversation-driven video editor skill for Claude Code" +description = "Conversation-driven video production framework for coding agents" license = { file = "LICENSE" } requires-python = ">=3.10" dependencies = [ @@ -21,3 +21,6 @@ build-backend = "setuptools.build_meta" [tool.setuptools] py-modules = [] + +[tool.pytest.ini_options] +pythonpath = ["."] diff --git a/tests/test_skill_contract.py b/tests/test_skill_contract.py new file mode 100644 index 00000000..3e8e2c4d --- /dev/null +++ b/tests/test_skill_contract.py @@ -0,0 +1,63 @@ +"""Guard the public editing contract in SKILL.md. + +Two failure modes this protects against when several agents edit the file: +a hard rule silently disappears or gets renumbered, and a helper or reference +path is documented but no longer exists in the tree. +""" + +import re +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +SKILL = ROOT / "SKILL.md" + +# Append-only. Add new rules to the end of this list when SKILL.md gains one. +HARD_RULES = [ + "Subtitles are applied LAST in the filter chain", + "Per-segment extract", + "30ms audio fades at every segment boundary", + "Overlays use `setpts=PTS-STARTPTS+T/TB`", + "Master SRT uses output-timeline offsets", + "Never cut inside a word.", + "Pad every cut edge.", + "Word-level verbatim ASR only.", + "Cache transcripts per source.", + "Parallel sub-agents for multiple animations.", + "Strategy confirmation before execution.", + "All session outputs in `/edit/`.", +] + +PATH_PATTERN = re.compile(r"`((?:references|helpers|skills)/[A-Za-z0-9_./-]+)`") + + +def hard_rules_section(text: str) -> str: + match = re.search(r"^## Hard Rules.*?$(.*?)^## ", text, re.S | re.M) + assert match, "SKILL.md must contain a '## Hard Rules' section" + return match.group(1) + + +class SkillContractTests(unittest.TestCase): + def setUp(self) -> None: + self.text = SKILL.read_text(encoding="utf-8") + + def test_hard_rules_are_present_in_order(self): + section = hard_rules_section(self.text) + items = re.findall(r"^(\d+)\. \*\*(.+?)\*\*", section, re.M) + numbers = [int(number) for number, _ in items] + self.assertEqual(numbers, list(range(1, len(numbers) + 1)), "rules must be numbered consecutively") + self.assertGreaterEqual(len(items), len(HARD_RULES), "a hard rule was removed") + for index, expected in enumerate(HARD_RULES): + self.assertIn(expected, items[index][1], f"rule {index + 1} changed or moved") + + def test_referenced_paths_exist(self): + missing = sorted( + path + for path in set(PATH_PATTERN.findall(self.text)) + if not (ROOT / path).exists() + ) + self.assertEqual(missing, [], "SKILL.md names paths that do not exist") + + +if __name__ == "__main__": + unittest.main() From 131251897111d17552ca687b0a0dadee98ca1b60 Mon Sep 17 00:00:00 2001 From: Ismael Date: Tue, 1 Sep 2026 22:34:44 -0700 Subject: [PATCH 02/19] add plain language comments to helpers and tests --- helpers/grade.py | 11 +++++++++++ helpers/pack_transcripts.py | 14 ++++++++++++++ helpers/timeline_view.py | 20 ++++++++++++++++++++ helpers/transcribe.py | 15 +++++++++++++++ helpers/transcribe_batch.py | 6 ++++++ tests/test_render_fps.py | 20 ++++++++++++++++++++ tests/test_skill_contract.py | 12 ++++++++++++ 7 files changed, 98 insertions(+) diff --git a/helpers/grade.py b/helpers/grade.py index becf6fb4..a4d30a91 100644 --- a/helpers/grade.py +++ b/helpers/grade.py @@ -1,3 +1,7 @@ +# color grading helper that builds ffmpeg filter strings and applies them to a video +# it ships a few named presets and an auto mode that samples frame stats to pick a small corrective eq +# render py imports get_preset and auto_grade_for_clip and the cli wraps the same functions + """Apply a color grade to a video via ffmpeg filter chain. Two modes: @@ -63,6 +67,7 @@ } +# look up the ffmpeg filter string for a named preset and fail loudly on unknown names def get_preset(name: str) -> str: """Return the ffmpeg filter string for a preset name. Empty string for 'none'.""" if name not in PRESETS: @@ -75,6 +80,7 @@ def get_preset(name: str) -> str: # -------- Auto grade (data-driven, per-clip) -------------------------------- +# run ffmpeg signalstats over a sampled range and average the luma and saturation readings into normalized stats def _sample_frame_stats( video: Path, start: float, @@ -121,6 +127,7 @@ def _sample_frame_stats( sat_avgs: list[float] = [] bit_depth: int = 8 + # pull the numeric value after the last equals sign on a metadata line def _parse_value(line: str) -> float | None: try: return float(line.rsplit("=", 1)[1]) @@ -175,6 +182,7 @@ def _parse_value(line: str) -> float | None: Path(metadata_path).unlink(missing_ok=True) +# analyze a clip range and derive a bounded corrective eq filter with no creative color shift def auto_grade_for_clip( video: Path, start: float = 0.0, @@ -271,8 +279,10 @@ def auto_grade_for_clip( return filter_string, stats +# run ffmpeg to write the graded output or stream copy when the filter is empty def apply_grade(input_path: Path, output_path: Path, filter_string: str) -> None: output_path.parent.mkdir(parents=True, exist_ok=True) + # an empty filter means no grade so copy streams without re encoding if not filter_string: cmd = [ "ffmpeg", "-y", "-i", str(input_path), @@ -291,6 +301,7 @@ def apply_grade(input_path: Path, output_path: Path, filter_string: str) -> None subprocess.run(cmd, check=True) +# cli entry point that handles preset listing analysis and the normal grade run def main() -> None: ap = argparse.ArgumentParser(description="Apply a color grade via ffmpeg filter chain") ap.add_argument("input", type=Path, nargs="?", help="Input video") diff --git a/helpers/pack_transcripts.py b/helpers/pack_transcripts.py index 036a1276..c6619fe8 100644 --- a/helpers/pack_transcripts.py +++ b/helpers/pack_transcripts.py @@ -1,3 +1,7 @@ +# packs scribe word level transcript json files into one phrase level markdown document +# words are grouped into phrases split on long silences or speaker changes and each phrase carries a start and end stamp +# the editor sub agent reads the result to pick cuts so it favors compactness over raw detail + """Pack all Scribe transcripts in /transcripts/ into one readable markdown. Groups word-level entries into phrase-level lines, breaking on any silence @@ -21,11 +25,13 @@ from pathlib import Path +# render seconds as a zero padded fixed width string so stamps line up in columns def format_time(seconds: float) -> str: """Format a time in seconds as "NNN.NN" with fixed 6-char width for alignment.""" return f"{seconds:06.2f}" +# render a duration as seconds or minutes plus seconds for the section headers def format_duration(seconds: float) -> str: """Format a duration as "Ms" or "Mm SSs".""" if seconds < 60: @@ -35,6 +41,7 @@ def format_duration(seconds: float) -> str: return f"{m}m {s:04.1f}s" +# walk scribe word entries and cut them into phrases on silence gaps or speaker changes def group_into_phrases( words: list[dict], silence_threshold: float = 0.5, @@ -51,6 +58,7 @@ def group_into_phrases( current_start: float | None = None current_speaker: str | None = None + # emit the current phrase if it has any visible text then reset the accumulator def flush() -> None: nonlocal current_words, current_start, current_speaker if not current_words: @@ -61,6 +69,7 @@ def flush() -> None: raw = (w.get("text") or "").strip() if not raw: continue + # wrap audio events in parentheses unless scribe already did if t == "audio_event": if not raw.startswith("("): raw = f"({raw})" @@ -71,7 +80,9 @@ def flush() -> None: current_speaker = None return text = " ".join(text_parts) + # glue punctuation tokens back onto the preceding word text = text.replace(" ,", ",").replace(" .", ".").replace(" ?", "?").replace(" !", "!") + # fall back through end then start then phrase start so a missing timestamp cannot break the flush end_time = current_words[-1].get("end", current_words[-1].get("start", current_start or 0.0)) phrases.append({ "start": current_start, @@ -122,6 +133,7 @@ def flush() -> None: return phrases +# load one transcript json and return its name duration and phrase list def pack_one_file(json_path: Path, silence_threshold: float) -> tuple[str, float, list[dict]]: """Return (header_name, duration, phrases) for one transcript file.""" data = json.loads(json_path.read_text()) @@ -134,6 +146,7 @@ def pack_one_file(json_path: Path, silence_threshold: float) -> tuple[str, float return json_path.stem, duration, phrases +# build the markdown document with a header block and one section per transcript def render_markdown(entries: list[tuple[str, float, list[dict]]], silence_threshold: float) -> str: lines: list[str] = [] lines.append("# Packed transcripts") @@ -162,6 +175,7 @@ def render_markdown(entries: list[tuple[str, float, list[dict]]], silence_thresh return "\n".join(lines) +# cli entry point that finds transcript json files packs them and writes takes_packed md def main() -> None: ap = argparse.ArgumentParser(description="Pack Scribe transcripts into takes_packed.md") ap.add_argument("--edit-dir", type=Path, required=True, help="Edit directory containing transcripts/") diff --git a/helpers/timeline_view.py b/helpers/timeline_view.py index 68a74515..b16da40c 100644 --- a/helpers/timeline_view.py +++ b/helpers/timeline_view.py @@ -1,3 +1,7 @@ +# renders a filmstrip and waveform composite png for a time range of a video +# frames come from ffmpeg the audio envelope is a windowed rms over a temp wav and transcript words and silences are drawn over the ribbon +# meant as an on demand drill down at cut decisions rather than a bulk index + """Filmstrip + waveform composite PNG for a time range of a video. The only visual drill-down tool. Given a video and a [start, end] range, @@ -34,11 +38,13 @@ # -------- Frame extraction --------------------------------------------------- +# grab n evenly spaced jpeg frames from the range with ffmpeg and return their paths in order def extract_frames(video: Path, start: float, end: float, n: int, dest_dir: Path) -> list[Path]: """Extract N frames evenly spaced across [start, end]. Returns paths in order.""" dest_dir.mkdir(parents=True, exist_ok=True) if n < 1: n = 1 + # a single frame sits at the midpoint otherwise spread frames so the first and last land on the range edges if n == 1: times = [(start + end) / 2.0] else: @@ -65,6 +71,7 @@ def extract_frames(video: Path, start: float, end: float, n: int, dest_dir: Path # -------- Audio envelope (librosa if available, ffmpeg fallback) ------------ +# dump the audio range to a mono wav and turn it into a normalized rms envelope of fixed length def compute_envelope(video: Path, start: float, end: float, samples: int = 2000) -> np.ndarray: """Extract the audio segment and return an RMS envelope of length `samples`. @@ -100,6 +107,7 @@ def compute_envelope(video: Path, start: float, end: float, samples: int = 2000) usable = (n // window) * window reshaped = pcm[:usable].reshape(-1, window) env = np.sqrt(np.mean(reshaped ** 2, axis=1)) + # pad or trim so the envelope always has exactly samples entries if env.size < samples: env = np.pad(env, (0, samples - env.size)) elif env.size > samples: @@ -115,6 +123,7 @@ def compute_envelope(video: Path, start: float, end: float, samples: int = 2000) # -------- Transcript word overlays ------------------------------------------ +# return transcript word entries that overlap the requested range def words_in_range(transcript_path: Path, start: float, end: float) -> list[dict]: if not transcript_path.exists(): return [] @@ -126,12 +135,14 @@ def words_in_range(transcript_path: Path, start: float, end: float) -> list[dict we = w.get("end") if ws is None or we is None: continue + # skip tokens that end before the range or begin after it if we <= start or ws >= end: continue out.append(w) return out +# scan kept tokens for gaps of at least threshold seconds and return them as time pairs def find_silences(words: list[dict], start: float, end: float, threshold: float = 0.4) -> list[tuple[float, float]]: """Find gaps >= threshold seconds inside [start, end] between kept tokens.""" gaps: list[tuple[float, float]] = [] @@ -143,6 +154,7 @@ def find_silences(words: list[dict], start: float, end: float, threshold: float if ws - prev_end >= threshold: gaps.append((prev_end, ws)) prev_end = max(prev_end, w.get("end", ws)) + # also report a trailing gap between the last token and the range end if end - prev_end >= threshold: gaps.append((prev_end, end)) return gaps @@ -160,6 +172,7 @@ def find_silences(words: list[dict], start: float, end: float, threshold: float ] +# try the known monospace and system font paths and fall back to the pillow default def load_font(size: int) -> ImageFont.ImageFont: for fp in FONT_CANDIDATES: if Path(fp).exists(): @@ -181,6 +194,7 @@ def load_font(size: int) -> ImageFont.ImageFont: WAVE = (140, 180, 255) +# draw the full composite of header filmstrip silence bands waveform word labels and time ruler then save it def render_timeline( video: Path, start: float, @@ -235,6 +249,7 @@ def render_timeline( # Filmstrip x = 50 strip_width = canvas_width - 100 + # paste frames at full size when they fit otherwise scale the whole strip down to the canvas width if total_frame_w <= strip_width: cursor = 50 for img in imgs: @@ -256,6 +271,7 @@ def render_timeline( strip_x1 = 50 + draw_width strip_span = strip_x1 - strip_x0 + # map a timestamp inside the range onto a pixel column of the strip def time_to_x(t: float) -> int: frac = (t - start) / max(1e-6, (end - start)) return int(strip_x0 + frac * strip_span) @@ -277,6 +293,7 @@ def time_to_x(t: float) -> int: max_amp = wave_h // 2 - 8 points_top: list[tuple[int, int]] = [] points_bot: list[tuple[int, int]] = [] + # mirror the envelope above and below the midline to draw a symmetric waveform for i, v in enumerate(env): xi = strip_x0 + int(i * strip_span / max(1, len(env) - 1)) a = int(v * max_amp) @@ -299,6 +316,7 @@ def time_to_x(t: float) -> int: text = (w.get("text") or "").strip() if not text or ws is None or we is None: continue + # skip very short words and labels that would land within 28 px of the previous one if (we - ws) < 0.05: continue cx = (time_to_x(ws) + time_to_x(we)) // 2 @@ -330,6 +348,7 @@ def time_to_x(t: float) -> int: print(f"saved: {out_path} ({out_path.stat().st_size // 1024} KB)") +# cli entry point that validates the range resolves the transcript and output paths and renders the png def main() -> None: ap = argparse.ArgumentParser(description="Filmstrip + waveform composite for a video range") ap.add_argument("video", type=Path, nargs="?", help="Source video") @@ -372,6 +391,7 @@ def main() -> None: if auto.exists(): transcript = auto + # default the output into the verify folder under the edit directory with the range in the file name out_path = args.output if out_path is None: out_dir = video.parent / "edit" / "verify" diff --git a/helpers/transcribe.py b/helpers/transcribe.py index b474fdf1..ac5efbb4 100644 --- a/helpers/transcribe.py +++ b/helpers/transcribe.py @@ -1,3 +1,7 @@ +# transcribes a single video with elevenlabs scribe and caches the response as json +# audio is extracted to a mono 16khz wav with ffmpeg then uploaded with diarization audio events and word timestamps +# transcribe_batch imports load_api_key and transcribe_one to fan the same work out across files + """Transcribe a video with ElevenLabs Scribe. Extracts mono 16kHz audio via ffmpeg, uploads to Scribe with verbatim + @@ -33,9 +37,12 @@ SCRIBE_URL = "https://api.elevenlabs.io/v1/speech-to-text" +# read the elevenlabs api key from a dotenv file or the environment and exit if neither has it def load_api_key() -> str: + # check the repo root dotenv first then the working directory for candidate in [Path(__file__).resolve().parent.parent / ".env", Path(".env")]: if candidate.exists(): + # parse key equals value lines and strip surrounding quotes from the value for line in candidate.read_text().splitlines(): line = line.strip() if not line or line.startswith("#") or "=" not in line: @@ -49,6 +56,7 @@ def load_api_key() -> str: return v +# count the audio streams in a container so multi track recordings can be flagged def count_audio_tracks(video_path: Path) -> int: """How many audio streams the container holds.""" out = subprocess.run( @@ -59,6 +67,7 @@ def count_audio_tracks(video_path: Path) -> int: return len([ln for ln in out.stdout.splitlines() if ln.strip()]) +# measure the peak level of a wav in dbfs so a silent track is caught before upload def peak_dbfs(wav_path: Path) -> float: """Peak level of a 16-bit PCM wav, in dBFS. -inf for digital silence.""" peak = 0 @@ -71,6 +80,7 @@ def peak_dbfs(wav_path: Path) -> float: return 20 * math.log10(peak / 32768) if peak > 0 else float("-inf") +# use ffmpeg to write a mono 16khz pcm wav from the video def extract_audio(video_path: Path, dest: Path, audio_track: int = 0) -> None: cmd = [ "ffmpeg", "-y", "-i", str(video_path), @@ -81,6 +91,7 @@ def extract_audio(video_path: Path, dest: Path, audio_track: int = 0) -> None: subprocess.run(cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) +# upload the wav to the scribe endpoint with verbatim settings and return the parsed json def call_scribe( audio_path: Path, api_key: str, @@ -107,12 +118,14 @@ def call_scribe( timeout=1800, ) + # any non 200 response is treated as a hard failure with a trimmed body for context if resp.status_code != 200: raise RuntimeError(f"Scribe returned {resp.status_code}: {resp.text[:500]}") return resp.json() +# resolve where a transcript lands with the track number in the name for anything but track zero def transcript_path(edit_dir: Path, video: Path, audio_track: int = 0) -> Path: """Where a video's transcript lands. @@ -125,6 +138,7 @@ def transcript_path(edit_dir: Path, video: Path, audio_track: int = 0) -> Path: return edit_dir / "transcripts" / f"{video.stem}{suffix}.json" +# transcribe one video into the transcripts folder and return the json path reusing a cached file if present def transcribe_one( video: Path, edit_dir: Path, @@ -189,6 +203,7 @@ def transcribe_one( return out_path +# cli entry point that resolves the video and edit directory then runs a single transcription def main() -> None: ap = argparse.ArgumentParser(description="Transcribe a video with ElevenLabs Scribe") ap.add_argument("video", type=Path, help="Path to video file") diff --git a/helpers/transcribe_batch.py b/helpers/transcribe_batch.py index aa665260..7267ab16 100644 --- a/helpers/transcribe_batch.py +++ b/helpers/transcribe_batch.py @@ -1,3 +1,7 @@ +# batch transcribes every video in a directory using a thread pool of scribe workers +# it reuses transcribe_one so per file caching still applies and only pending sources are uploaded +# failures are collected and reported at the end with a non zero exit + """Batch-transcribe every video in a directory with 4 parallel workers. Walks for common video extensions, runs ElevenLabs Scribe on @@ -26,6 +30,7 @@ VIDEO_EXTS = {".mp4", ".MP4", ".mov", ".MOV", ".mkv", ".MKV", ".avi", ".AVI", ".m4v"} +# list files in the directory whose suffix is a known video extension in sorted order def find_videos(videos_dir: Path) -> list[Path]: videos = sorted( p for p in videos_dir.iterdir() @@ -34,6 +39,7 @@ def find_videos(videos_dir: Path) -> list[Path]: return videos +# cli entry point that splits videos into cached and pending sets then transcribes the pending ones in parallel def main() -> None: ap = argparse.ArgumentParser(description="Parallel batch transcription of a videos directory") ap.add_argument("videos_dir", type=Path, help="Directory containing source videos") diff --git a/tests/test_render_fps.py b/tests/test_render_fps.py index b82a693f..4f7f410f 100644 --- a/tests/test_render_fps.py +++ b/tests/test_render_fps.py @@ -1,3 +1,7 @@ +# unit tests for the frame rate handling in helpers render py +# they cover parse_fps canonical forms probe_source_fps ffprobe parsing and the rate resolution in extract_all_segments +# render py is loaded by file path so the tests do not depend on it being importable as a package + import argparse import contextlib import importlib.util @@ -10,6 +14,7 @@ from unittest.mock import patch +# load render py straight from its file path under a private module name MODULE_PATH = Path(__file__).parents[1] / "helpers" / "render.py" SPEC = importlib.util.spec_from_file_location("video_use_render", MODULE_PATH) assert SPEC and SPEC.loader @@ -17,7 +22,9 @@ SPEC.loader.exec_module(render) +# tests for parse_fps which turns user supplied rates into canonical rational strings class ParseFpsTests(unittest.TestCase): + # integer decimal and rational inputs all canonicalize to the expected fraction def test_accepts_integer_decimal_and_rational_rates(self): expected = { "60": "60/1", @@ -28,12 +35,14 @@ def test_accepts_integer_decimal_and_rational_rates(self): with self.subTest(value=value): self.assertEqual(render.parse_fps(value), canonical) + # feeding a canonical rate back through parse_fps returns it unchanged def test_canonical_rates_are_idempotent(self): for value in ("60", "29.97", "30000/1001"): with self.subTest(value=value): canonical = render.parse_fps(value) self.assertEqual(render.parse_fps(canonical), canonical) + # malformed zero negative or oversized inputs raise an argparse type error def test_rejects_invalid_or_non_positive_rates(self): for value in ( "", @@ -51,7 +60,9 @@ def test_rejects_invalid_or_non_positive_rates(self): render.parse_fps(value) +# tests for probe_source_fps with ffprobe replaced by canned completed process results class ProbeSourceFpsTests(unittest.TestCase): + # build a fake ffprobe result whose json carries the given average and nominal rates @staticmethod def _probe_result(avg: str, nominal: str) -> subprocess.CompletedProcess: stdout = json.dumps({ @@ -59,28 +70,34 @@ def _probe_result(avg: str, nominal: str) -> subprocess.CompletedProcess: }) return subprocess.CompletedProcess([], 0, stdout=stdout, stderr="") + # the average frame rate wins when ffprobe reports a usable one def test_prefers_average_rate(self): result = self._probe_result("30000/1001", "30/1") with patch.object(render.subprocess, "run", return_value=result): self.assertEqual(render.probe_source_fps(Path("source.mp4")), "30000/1001") + # a zero average rate falls back to the nominal r_frame_rate def test_falls_back_to_nominal_rate(self): result = self._probe_result("0/0", "60/1") with patch.object(render.subprocess, "run", return_value=result): self.assertEqual(render.probe_source_fps(Path("source.mp4")), "60/1") + # an empty streams list yields none instead of a rate def test_returns_none_for_unusable_probe_output(self): result = subprocess.CompletedProcess([], 0, stdout='{"streams": []}', stderr="") with patch.object(render.subprocess, "run", return_value=result): self.assertIsNone(render.probe_source_fps(Path("source.mp4"))) + # an ffprobe process error yields none rather than propagating def test_returns_none_when_ffprobe_fails(self): error = subprocess.CalledProcessError(1, ["ffprobe"]) with patch.object(render.subprocess, "run", side_effect=error): self.assertIsNone(render.probe_source_fps(Path("source.mp4"))) +# tests for how extract_all_segments chooses one rate for every segment class RenderRateTests(unittest.TestCase): + # minimal two source edl with one range per source @staticmethod def _edl() -> dict: return { @@ -91,6 +108,7 @@ def _edl() -> dict: ], } + # only the first source is probed and its rate is applied to every segment def test_multi_source_render_resolves_one_rate_from_first_source(self): edl = self._edl() with tempfile.TemporaryDirectory() as temp_dir: @@ -105,6 +123,7 @@ def test_multi_source_render_resolves_one_rate_from_first_source(self): probe.assert_called_once_with((edit_dir / "first.mp4").resolve()) self.assertEqual([call.kwargs["rate"] for call in extract.call_args_list], ["60/1", "60/1"]) + # an explicit fps argument skips probing and is canonicalized for every segment def test_explicit_rate_skips_probe_and_applies_to_every_segment(self): with tempfile.TemporaryDirectory() as temp_dir: with ( @@ -122,6 +141,7 @@ def test_explicit_rate_skips_probe_and_applies_to_every_segment(self): ["30/1", "30/1"], ) + # when probing fails every segment falls back to the 24 default def test_failed_probe_falls_back_to_24_for_every_segment(self): with tempfile.TemporaryDirectory() as temp_dir: with ( diff --git a/tests/test_skill_contract.py b/tests/test_skill_contract.py index 3e8e2c4d..50efb876 100644 --- a/tests/test_skill_contract.py +++ b/tests/test_skill_contract.py @@ -1,3 +1,7 @@ +# tests that guard the public editing contract documented in skill md +# they check that the numbered hard rules stay present and in order and that every referenced helper or reference path exists +# the hard rules list is append only so new rules go at the end + """Guard the public editing contract in SKILL.md. Two failure modes this protects against when several agents edit the file: @@ -28,21 +32,28 @@ "All session outputs in `/edit/`.", ] +# matches backticked paths under references helpers or skills PATH_PATTERN = re.compile(r"`((?:references|helpers|skills)/[A-Za-z0-9_./-]+)`") +# slice out the body of the hard rules section up to the next second level heading def hard_rules_section(text: str) -> str: + # dotall plus multiline so the lazy group spans lines and the heading anchors match at line starts match = re.search(r"^## Hard Rules.*?$(.*?)^## ", text, re.S | re.M) assert match, "SKILL.md must contain a '## Hard Rules' section" return match.group(1) +# test case that reads skill md once per test and checks its contract class SkillContractTests(unittest.TestCase): + # load the skill md text for each test def setUp(self) -> None: self.text = SKILL.read_text(encoding="utf-8") + # numbered bold rule titles must be consecutive and each expected rule must still sit at its index def test_hard_rules_are_present_in_order(self): section = hard_rules_section(self.text) + # capture the number and bold title of each numbered list item items = re.findall(r"^(\d+)\. \*\*(.+?)\*\*", section, re.M) numbers = [int(number) for number, _ in items] self.assertEqual(numbers, list(range(1, len(numbers) + 1)), "rules must be numbered consecutively") @@ -50,6 +61,7 @@ def test_hard_rules_are_present_in_order(self): for index, expected in enumerate(HARD_RULES): self.assertIn(expected, items[index][1], f"rule {index + 1} changed or moved") + # every backticked helper or reference path in skill md must exist on disk def test_referenced_paths_exist(self): missing = sorted( path From b27cd4ff6914a609d7430be084f3adda56740c4e Mon Sep 17 00:00:00 2001 From: Ismael Date: Wed, 2 Sep 2026 10:30:09 -0700 Subject: [PATCH 03/19] add plain language comments to the original render helper --- helpers/render.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/helpers/render.py b/helpers/render.py index e464c476..a65ed30f 100644 --- a/helpers/render.py +++ b/helpers/render.py @@ -1,3 +1,7 @@ +# the ffmpeg render pipeline that turns an edl into a finished video file +# it extracts graded segments and concatenates them and then composites overlays and subtitles and normalizes loudness +# it also builds the master srt from transcripts and holds the command line entry point + """Render a video from an EDL. Implements the HEURISTICS render pipeline in the correct order: @@ -32,9 +36,13 @@ try: from grade import get_preset, auto_grade_for_clip # same directory except Exception: + # fallback that only knows the identity grade when the grade helper is unavailable + # fallback that only knows the identity grade when the grade helper is unavailable def get_preset(name: str) -> str: return "" + # fallback that skips auto grading when the grade helper is unavailable + # fallback that skips auto grading when the grade helper is unavailable def auto_grade_for_clip(video, start=0.0, duration=None, verbose=False): # type: ignore return "eq=contrast=1.03:saturation=0.98", {} @@ -59,12 +67,14 @@ def auto_grade_for_clip(video, start=0.0, duration=None, verbose=False): # type # -------- Helpers ------------------------------------------------------------ +# run a command with check and print an abbreviated version unless quiet def run(cmd: list[str], quiet: bool = False) -> None: if not quiet: print(f" $ {' '.join(str(c) for c in cmd[:6])}{' …' if len(cmd) > 6 else ''}") subprocess.run(cmd, check=True) +# turn the edl grade field into a filter string or the auto sentinel def resolve_grade_filter(grade_field: str | None) -> str: """The EDL's 'grade' field can be a preset name, a raw ffmpeg filter, or 'auto'. @@ -85,6 +95,7 @@ def resolve_grade_filter(grade_field: str | None) -> str: return grade_field +# resolve a path relative to base unless it is already absolute def resolve_path(maybe_path: str, base: Path) -> Path: """Resolve a path that may be absolute or relative to `base`.""" p = Path(maybe_path) @@ -118,6 +129,7 @@ def resolve_path(maybe_path: str, base: Path) -> Path: ) +# detect pq or hlg transfer metadata on the first video stream def is_hdr_source(video: Path) -> bool: """Return True if the source uses a PQ or HLG transfer function.""" try: @@ -132,6 +144,7 @@ def is_hdr_source(video: Path) -> bool: return False +# report whether the first video stream is taller than it is wide def is_portrait_source(video: Path) -> bool: """Return True if the displayed video is portrait, including rotation.""" try: @@ -172,6 +185,7 @@ def is_portrait_source(video: Path) -> bool: return False +# validate an fps argument and return it as a reduced rational string def parse_fps(value: str) -> str: """Validate and canonicalize an ffmpeg frame rate.""" text = value.strip() @@ -198,6 +212,7 @@ def parse_fps(value: str) -> str: return f"{rate.numerator}/{rate.denominator}" +# read the source frame rate from ffprobe preferring the average rate def probe_source_fps(video: Path) -> str | None: """Return an ffmpeg-ready source rate, preferring the average frame rate. @@ -216,6 +231,7 @@ def probe_source_fps(video: Path) -> str | None: streams = json.loads(out.stdout).get("streams") or [] if not streams: return None + # take the first usable rate and skip zero or unparsable values for field in ("avg_frame_rate", "r_frame_rate"): value = streams[0].get(field) if value and value != "0/0": @@ -231,6 +247,7 @@ def probe_source_fps(video: Path) -> str | None: # -------- Per-segment extraction (Rule 2 + Rule 3) -------------------------- +# encode one edl range as its own mp4 with tone mapping scaling grade and audio fades applied def extract_segment( source: Path, seg_start: float, @@ -253,12 +270,14 @@ def extract_segment( """ out_path.parent.mkdir(parents=True, exist_ok=True) + # scale by height for portrait sources so orientation is preserved portrait = is_portrait_source(source) if draft: scale = "scale=-2:1280" if portrait else "scale=1280:-2" else: scale = "scale=-2:1920" if portrait else "scale=1920:-2" + # tone map hdr first then scale then grade vf_parts: list[str] = [] if is_hdr_source(source): vf_parts.append(TONEMAP_CHAIN) @@ -300,6 +319,7 @@ def extract_segment( subprocess.run(cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE) +# extract every edl range into graded segment files sharing one output frame rate def extract_all_segments( edl: dict, edit_dir: Path, @@ -368,10 +388,12 @@ def extract_all_segments( # -------- Lossless concat ---------------------------------------------------- +# join segment files losslessly with the concat demuxer def concat_segments(segment_paths: list[Path], out_path: Path, edit_dir: Path) -> None: """Lossless concat via the concat demuxer. No re-encode.""" out_path.parent.mkdir(parents=True, exist_ok=True) concat_list = edit_dir / "_concat.txt" + # the concat demuxer reads a text file listing each segment path concat_list.write_text("".join(f"file '{p.resolve()}'\n" for p in segment_paths)) cmd = [ @@ -393,6 +415,7 @@ def concat_segments(segment_paths: list[Path], out_path: Path, edit_dir: Path) - PUNCT_BREAK = set(".,!?;:") +# format seconds as an srt timestamp with millisecond precision def _srt_timestamp(seconds: float) -> str: total_ms = int(round(seconds * 1000)) h, rem = divmod(total_ms, 3600_000) @@ -401,6 +424,7 @@ def _srt_timestamp(seconds: float) -> str: return f"{h:02d}:{m:02d}:{s:02d},{ms:03d}" +# return transcript words that overlap the given source time range def _words_in_range(transcript: dict, t_start: float, t_end: float) -> list[dict]: out: list[dict] = [] for w in transcript.get("words", []): @@ -410,12 +434,14 @@ def _words_in_range(transcript: dict, t_start: float, t_end: float) -> list[dict we = w.get("end") if ws is None or we is None: continue + # keep only words that overlap the range if we <= t_start or ws >= t_end: continue out.append(w) return out +# build an output timeline srt from per source transcripts using two word uppercase chunks def build_master_srt(edl: dict, edit_dir: Path, out_path: Path) -> None: """Build an output-timeline SRT from per-source transcripts. @@ -461,6 +487,7 @@ def build_master_srt(edl: dict, edit_dir: Path, out_path: Path) -> None: chunks.append(current) for chunk in chunks: + # clamp chunk times to the segment then shift them onto the output timeline local_start = max(seg_start, chunk[0].get("start", seg_start)) local_end = min(seg_end, chunk[-1].get("end", seg_end)) out_start = max(0.0, local_start - seg_start) + seg_offset @@ -498,6 +525,7 @@ def build_master_srt(edl: dict, edit_dir: Path, out_path: Path) -> None: LOUDNORM_LRA = 11.0 +# run the loudnorm first pass and parse its json measurement from stderr def measure_loudness(video_path: Path) -> dict[str, str] | None: """Run ffmpeg loudnorm first pass and parse the JSON measurement. @@ -532,6 +560,7 @@ def measure_loudness(video_path: Path) -> dict[str, str] | None: return data +# normalize audio with two pass loudnorm or a one pass approximation in preview mode def apply_loudnorm_two_pass( input_path: Path, output_path: Path, @@ -597,6 +626,7 @@ def apply_loudnorm_two_pass( # -------- Final compositing (Rule 1 + Rule 4) ------------------------------- +# composite overlays onto the base and burn subtitles last in one ffmpeg filter graph def build_final_composite( base_path: Path, overlays: list[dict], @@ -676,6 +706,7 @@ def build_final_composite( # -------- Main --------------------------------------------------------------- +# command line entry point that validates the edl and runs the full pipeline def main() -> None: ap = argparse.ArgumentParser(description="Render a video from an EDL") ap.add_argument("edl", type=Path, help="Path to edl.json") From 70e6aa1c04eb4182623f06aa1577bc0ff8cd3ecb Mon Sep 17 00:00:00 2001 From: Ismael Date: Wed, 2 Sep 2026 10:32:34 -0700 Subject: [PATCH 04/19] add comment convention guard test --- tests/test_comment_style.py | 58 +++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 tests/test_comment_style.py diff --git a/tests/test_comment_style.py b/tests/test_comment_style.py new file mode 100644 index 00000000..650f9d10 --- /dev/null +++ b/tests/test_comment_style.py @@ -0,0 +1,58 @@ +# guards the comment convention across every python file in the repo +# each file starts with a plain header block and every def or class has one plain comment line above it +# comments must not carry punctuation so they stay short and readable at a glance + +import re +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +SKIP_DIRS = {".venv", "venv", "node_modules", "__pycache__", ".git", "media", "edit"} +PUNCTUATION = re.compile(r"[.,:;()\"'`]") +DEFINITION = re.compile(r"\s*(async def|def|class) \w") + + +# yield every python file in the repo that is not inside a skipped directory +def python_files(): + for path in ROOT.rglob("*.py"): + if not any(part in SKIP_DIRS for part in path.relative_to(ROOT).parts): + yield path + + +# return the problems found in one file as short strings +def audit(text): + lines = text.splitlines() + problems = [] + first = [line for line in lines[:6] if line.strip() and not line.startswith("#!")] + if not first or not first[0].startswith("#"): + problems.append("missing header comment block") + for index, line in enumerate(lines): + if not DEFINITION.match(line): + continue + above = index - 1 + # decorators sit between the comment and the definition + while above >= 0 and lines[above].strip().startswith("@"): + above -= 1 + if above < 0 or not lines[above].strip().startswith("#"): + problems.append(f"line {index + 1} has no comment above {line.strip()[:40]}") + continue + comment = lines[above].strip().lstrip("#").strip() + if PUNCTUATION.search(comment): + problems.append(f"line {above + 1} comment contains punctuation") + return problems + + +# one test that scans the whole repo so a single failure lists every offending file +class CommentStyleTests(unittest.TestCase): + # every python file must satisfy the header and per definition comment rules + def test_every_python_file_follows_the_convention(self): + failures = {} + for path in python_files(): + problems = audit(path.read_text(encoding="utf-8")) + if problems: + failures[str(path.relative_to(ROOT))] = problems[:5] + self.assertEqual(failures, {}, "comment convention violations") + + +if __name__ == "__main__": + unittest.main() From f4bf4b4ebf71d23bccc3010664d0e33a8655ac6a Mon Sep 17 00:00:00 2001 From: Ismael Date: Wed, 2 Sep 2026 10:58:46 -0700 Subject: [PATCH 05/19] add plain language comments to the orientation test --- tests/test_render_orientation.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/test_render_orientation.py b/tests/test_render_orientation.py index 36c0ee61..9489b6fa 100644 --- a/tests/test_render_orientation.py +++ b/tests/test_render_orientation.py @@ -1,3 +1,6 @@ +# tests for portrait detection in the render helper including rotation side data +# it loads the helper from its file path and patches ffprobe so no real media is needed + import importlib.util import json import subprocess @@ -13,7 +16,9 @@ SPEC.loader.exec_module(render) +# portrait detection must honor display rotation but ignore plain rotate tags class PortraitDetectionTests(unittest.TestCase): + # run the detector against a fake ffprobe json stream def _is_portrait(self, stream: dict) -> bool: result = subprocess.CompletedProcess( [], 0, stdout=json.dumps({"streams": [stream]}), stderr="" @@ -26,12 +31,15 @@ def _is_portrait(self, stream: dict) -> bool: self.assertNotIn("stream_tags=rotate", show_entries) return portrait + # taller than wide with no rotation is portrait def test_native_portrait_dimensions(self): self.assertTrue(self._is_portrait({"width": 1080, "height": 1920})) + # wider than tall with no rotation is landscape def test_native_landscape_dimensions(self): self.assertFalse(self._is_portrait({"width": 1920, "height": 1080})) + # a quarter turn in display side data flips a coded landscape into portrait def test_side_data_rotation_turns_coded_landscape_into_portrait(self): stream = { "width": 1920, @@ -40,10 +48,12 @@ def test_side_data_rotation_turns_coded_landscape_into_portrait(self): } self.assertTrue(self._is_portrait(stream)) + # a rotate tag without side data does not change the answer def test_plain_rotation_tag_without_side_data_is_ignored(self): stream = {"width": 1920, "height": 1080, "tags": {"rotate": "270"}} self.assertFalse(self._is_portrait(stream)) + # a quarter turn can also flip a coded portrait into landscape def test_rotation_can_turn_coded_portrait_into_landscape(self): stream = { "width": 1080, @@ -52,6 +62,7 @@ def test_rotation_can_turn_coded_portrait_into_landscape(self): } self.assertFalse(self._is_portrait(stream)) + # unreadable probe output falls back to landscape def test_invalid_probe_output_falls_back_to_landscape(self): result = subprocess.CompletedProcess([], 0, stdout="not json", stderr="") with patch.object(render.subprocess, "run", return_value=result): From 2aca4bb52796727aebf2ab17184e25f440ebcf78 Mon Sep 17 00:00:00 2001 From: Ismael Date: Wed, 2 Sep 2026 11:46:10 -0700 Subject: [PATCH 06/19] trim internal harness guidance out of the agent rules --- AGENTS.md | 39 +++++++-------------------------------- 1 file changed, 7 insertions(+), 32 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 6668a308..7e76e818 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,11 +11,11 @@ repository as a general product, never as one person's customized clone. editorial judgment, factual correctness, synchronization, pacing, and production reliability. - Design reusable contracts and capabilities. Do not hardcode personal paths, - credentials, account ids, prompts, brands, preferences, lane history, or - assumptions about one project. + credentials, account ids, prompts, brands, preferences, or assumptions + about one project. - Keep provider-specific behavior behind narrow boundaries. Core EDL validation, - rendering, reframing, and QC must remain usable without the localhost GUI or - Modal. + rendering, reframing, and QC must remain usable from the command line without + any optional client or remote runner. - Preserve backwards compatibility when practical. If a format must change, provide a clear migration path and reject unsupported input with an actionable error. @@ -33,39 +33,14 @@ repository as a general product, never as one person's customized clone. - `SKILL.md` defines the agent workflow and public editing contract. - `helpers/` contains provider-independent production tools and validation. - `skills/` contains focused companion skills and reusable production assets. -- `gui/` is an optional internal client and remote-execution adapter. It may - orchestrate core features but must not become the only place those features - exist. -- `tests/` and `gui/tests/` protect public behavior and adapter behavior - respectively. +- `tests/` protects public behavior. Optional clients or remote runners may + orchestrate core features but must never become the only place a feature + exists. Keep decision data explicit in portable project files such as `edit/edl.json`. Renderers should consume declared inputs deterministically. UI state, agent history, and cloud runtime state must not be required to reproduce an output. -## Experimental GUI harness - -The four-lane GUI in `gui/` exists to generate many independent outputs quickly -so developers can find ugly frames, weak editorial decisions, unsupported -queries, and renderer failures before video-use users encounter them. - -- Use the four parallel Modal lanes for this branch's output-discovery runs. - Avoid one-off manual local benchmark renders when the harness can run the same - experiment and preserve comparable evidence. -- Do not assume the public video-use workflow will adopt the GUI, Modal, its lane - model, or its run-storage APIs. Promote a finding into `helpers/`, `skills/`, - or `SKILL.md` only when the underlying capability is generally useful without - the harness. -- Keep each lane's agent context fresh. Past run traces are evaluation evidence - for developers and must not leak into a new lane's creative context. -- Preserve every run's durable evidence through `gui/run_store.py`: the compact - `run_summary.md`, deduplicated `trace.json`, raw Codex protocol when available, - complete `render.log`, generated edit handoff, and returned outputs. -- Prefer `run_summary.md` when an agent needs quick context. Read the raw trace or - protocol only to investigate a specific decision, tool call, or failure. -- Pin valuable successes and representative failures before cleanup. Never - delete a pinned run, and never delete an active lane. - ## Change workflow 1. Identify whether a change belongs to the public editing contract, a reusable From 54199c00b930917cd2e83e298046c82f80c2efd4 Mon Sep 17 00:00:00 2001 From: Ismael Date: Wed, 2 Sep 2026 14:19:30 -0700 Subject: [PATCH 07/19] use module docstrings instead of hash headers and guard that shape --- helpers/grade.py | 4 ---- helpers/pack_transcripts.py | 4 ---- helpers/render.py | 4 ---- helpers/timeline_view.py | 4 ---- helpers/transcribe.py | 4 ---- helpers/transcribe_batch.py | 4 ---- tests/test_comment_style.py | 18 +++++++++++++----- tests/test_render_fps.py | 7 ++++--- tests/test_render_orientation.py | 5 +++-- tests/test_skill_contract.py | 4 ---- 10 files changed, 20 insertions(+), 38 deletions(-) diff --git a/helpers/grade.py b/helpers/grade.py index a4d30a91..f4bc79d5 100644 --- a/helpers/grade.py +++ b/helpers/grade.py @@ -1,7 +1,3 @@ -# color grading helper that builds ffmpeg filter strings and applies them to a video -# it ships a few named presets and an auto mode that samples frame stats to pick a small corrective eq -# render py imports get_preset and auto_grade_for_clip and the cli wraps the same functions - """Apply a color grade to a video via ffmpeg filter chain. Two modes: diff --git a/helpers/pack_transcripts.py b/helpers/pack_transcripts.py index c6619fe8..f780f9ad 100644 --- a/helpers/pack_transcripts.py +++ b/helpers/pack_transcripts.py @@ -1,7 +1,3 @@ -# packs scribe word level transcript json files into one phrase level markdown document -# words are grouped into phrases split on long silences or speaker changes and each phrase carries a start and end stamp -# the editor sub agent reads the result to pick cuts so it favors compactness over raw detail - """Pack all Scribe transcripts in /transcripts/ into one readable markdown. Groups word-level entries into phrase-level lines, breaking on any silence diff --git a/helpers/render.py b/helpers/render.py index a65ed30f..18f86223 100644 --- a/helpers/render.py +++ b/helpers/render.py @@ -1,7 +1,3 @@ -# the ffmpeg render pipeline that turns an edl into a finished video file -# it extracts graded segments and concatenates them and then composites overlays and subtitles and normalizes loudness -# it also builds the master srt from transcripts and holds the command line entry point - """Render a video from an EDL. Implements the HEURISTICS render pipeline in the correct order: diff --git a/helpers/timeline_view.py b/helpers/timeline_view.py index b16da40c..0ff60e0b 100644 --- a/helpers/timeline_view.py +++ b/helpers/timeline_view.py @@ -1,7 +1,3 @@ -# renders a filmstrip and waveform composite png for a time range of a video -# frames come from ffmpeg the audio envelope is a windowed rms over a temp wav and transcript words and silences are drawn over the ribbon -# meant as an on demand drill down at cut decisions rather than a bulk index - """Filmstrip + waveform composite PNG for a time range of a video. The only visual drill-down tool. Given a video and a [start, end] range, diff --git a/helpers/transcribe.py b/helpers/transcribe.py index ac5efbb4..ce18dd8c 100644 --- a/helpers/transcribe.py +++ b/helpers/transcribe.py @@ -1,7 +1,3 @@ -# transcribes a single video with elevenlabs scribe and caches the response as json -# audio is extracted to a mono 16khz wav with ffmpeg then uploaded with diarization audio events and word timestamps -# transcribe_batch imports load_api_key and transcribe_one to fan the same work out across files - """Transcribe a video with ElevenLabs Scribe. Extracts mono 16kHz audio via ffmpeg, uploads to Scribe with verbatim + diff --git a/helpers/transcribe_batch.py b/helpers/transcribe_batch.py index 7267ab16..19a8f4ae 100644 --- a/helpers/transcribe_batch.py +++ b/helpers/transcribe_batch.py @@ -1,7 +1,3 @@ -# batch transcribes every video in a directory using a thread pool of scribe workers -# it reuses transcribe_one so per file caching still applies and only pending sources are uploaded -# failures are collected and reported at the end with a non zero exit - """Batch-transcribe every video in a directory with 4 parallel workers. Walks for common video extensions, runs ElevenLabs Scribe on diff --git a/tests/test_comment_style.py b/tests/test_comment_style.py index 650f9d10..5ed67f9b 100644 --- a/tests/test_comment_style.py +++ b/tests/test_comment_style.py @@ -1,7 +1,9 @@ -# guards the comment convention across every python file in the repo -# each file starts with a plain header block and every def or class has one plain comment line above it -# comments must not carry punctuation so they stay short and readable at a glance +"""guards the comment convention across every python file in the repo +each file opens with a module docstring and every def or class has one plain comment line above it +comments must not carry punctuation so they stay short and readable at a glance +""" +import ast import re import unittest from pathlib import Path @@ -23,9 +25,15 @@ def python_files(): def audit(text): lines = text.splitlines() problems = [] + # the file opens with a docstring and not with a block of hash comments first = [line for line in lines[:6] if line.strip() and not line.startswith("#!")] - if not first or not first[0].startswith("#"): - problems.append("missing header comment block") + if first and first[0].startswith("#"): + problems.append("hash comment header should be a module docstring") + try: + if ast.get_docstring(ast.parse(text)) is None: + problems.append("missing module docstring") + except SyntaxError: + problems.append("file does not parse") for index, line in enumerate(lines): if not DEFINITION.match(line): continue diff --git a/tests/test_render_fps.py b/tests/test_render_fps.py index 4f7f410f..a5471b31 100644 --- a/tests/test_render_fps.py +++ b/tests/test_render_fps.py @@ -1,6 +1,7 @@ -# unit tests for the frame rate handling in helpers render py -# they cover parse_fps canonical forms probe_source_fps ffprobe parsing and the rate resolution in extract_all_segments -# render py is loaded by file path so the tests do not depend on it being importable as a package +"""unit tests for the frame rate handling in helpers render py +they cover parse_fps canonical forms probe_source_fps ffprobe parsing and the rate resolution in extract_all_segments +render py is loaded by file path so the tests do not depend on it being importable as a package +""" import argparse import contextlib diff --git a/tests/test_render_orientation.py b/tests/test_render_orientation.py index 9489b6fa..0e0bf451 100644 --- a/tests/test_render_orientation.py +++ b/tests/test_render_orientation.py @@ -1,5 +1,6 @@ -# tests for portrait detection in the render helper including rotation side data -# it loads the helper from its file path and patches ffprobe so no real media is needed +"""tests for portrait detection in the render helper including rotation side data +it loads the helper from its file path and patches ffprobe so no real media is needed +""" import importlib.util import json diff --git a/tests/test_skill_contract.py b/tests/test_skill_contract.py index 50efb876..0aea14d0 100644 --- a/tests/test_skill_contract.py +++ b/tests/test_skill_contract.py @@ -1,7 +1,3 @@ -# tests that guard the public editing contract documented in skill md -# they check that the numbered hard rules stay present and in order and that every referenced helper or reference path exists -# the hard rules list is append only so new rules go at the end - """Guard the public editing contract in SKILL.md. Two failure modes this protects against when several agents edit the file: From 4f8ab2d147fa6e1d84b6af0f3c8747d7e728eb2f Mon Sep 17 00:00:00 2001 From: Ismael Date: Wed, 2 Sep 2026 16:24:32 -0700 Subject: [PATCH 08/19] derive comment guard definitions from the ast and check bare helper names in the skill contract --- AGENTS.md | 3 ++- tests/test_comment_style.py | 47 ++++++++++++++++++++++++------------ tests/test_skill_contract.py | 11 +++++++++ 3 files changed, 45 insertions(+), 16 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 7e76e818..4c6e0028 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -79,7 +79,8 @@ from being overwritten by another: history, so a later sync silently discards it. - Hard rules in `SKILL.md` are append-only. New rules get the next number. Removing or renumbering a rule requires an explicit reason in the commit. -- Procedure prose belongs in `references/.md`. Edits to `SKILL.md` +- Procedure prose belongs in `references/.md` at the repository root; + create that folder with the first reference file. Edits to `SKILL.md` are limited to rules, helper-index bullets, directory-tree lines, the EDL example, and one-line pointers to the reference files. - `tests/test_skill_contract.py` checks that the rules and every referenced diff --git a/tests/test_comment_style.py b/tests/test_comment_style.py index 5ed67f9b..f0f02a5b 100644 --- a/tests/test_comment_style.py +++ b/tests/test_comment_style.py @@ -4,14 +4,16 @@ """ import ast +import io import re +import tokenize import unittest from pathlib import Path ROOT = Path(__file__).resolve().parents[1] SKIP_DIRS = {".venv", "venv", "node_modules", "__pycache__", ".git", "media", "edit"} PUNCTUATION = re.compile(r"[.,:;()\"'`]") -DEFINITION = re.compile(r"\s*(async def|def|class) \w") +DEFINITIONS = (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef) # yield every python file in the repo that is not inside a skipped directory @@ -30,26 +32,41 @@ def audit(text): if first and first[0].startswith("#"): problems.append("hash comment header should be a module docstring") try: - if ast.get_docstring(ast.parse(text)) is None: - problems.append("missing module docstring") + tree = ast.parse(text) except SyntaxError: problems.append("file does not parse") - for index, line in enumerate(lines): - if not DEFINITION.match(line): + return problems + if ast.get_docstring(tree) is None: + problems.append("missing module docstring") + comments = real_comments(text) + for node in sorted(definitions(tree), key=lambda item: item.lineno): + # decorators sit between the comment and the definition so look above the first decorator + start = min([node.lineno, *[item.lineno for item in node.decorator_list]]) + above = start - 1 + if above not in comments: + problems.append(f"line {node.lineno} has no comment above {node.name}") continue - above = index - 1 - # decorators sit between the comment and the definition - while above >= 0 and lines[above].strip().startswith("@"): - above -= 1 - if above < 0 or not lines[above].strip().startswith("#"): - problems.append(f"line {index + 1} has no comment above {line.strip()[:40]}") - continue - comment = lines[above].strip().lstrip("#").strip() - if PUNCTUATION.search(comment): - problems.append(f"line {above + 1} comment contains punctuation") + if PUNCTUATION.search(comments[above]): + problems.append(f"line {above} comment contains punctuation") return problems +# yield every function and class node in the tree so text inside strings never counts as a definition +def definitions(tree): + for node in ast.walk(tree): + if isinstance(node, DEFINITIONS): + yield node + + +# map line number to comment text for lines that hold nothing but a real comment token +def real_comments(text): + comments = {} + for token in tokenize.generate_tokens(io.StringIO(text).readline): + if token.type == tokenize.COMMENT and token.line.strip().startswith("#"): + comments[token.start[0]] = token.string.lstrip("#").strip() + return comments + + # one test that scans the whole repo so a single failure lists every offending file class CommentStyleTests(unittest.TestCase): # every python file must satisfy the header and per definition comment rules diff --git a/tests/test_skill_contract.py b/tests/test_skill_contract.py index 0aea14d0..7475e1c4 100644 --- a/tests/test_skill_contract.py +++ b/tests/test_skill_contract.py @@ -30,6 +30,8 @@ # matches backticked paths under references helpers or skills PATH_PATTERN = re.compile(r"`((?:references|helpers|skills)/[A-Za-z0-9_./-]+)`") +# matches a bare helper script name at the start of a backticked command +HELPER_PATTERN = re.compile(r"`([A-Za-z0-9_]+\.py)\b") # slice out the body of the hard rules section up to the next second level heading @@ -66,6 +68,15 @@ def test_referenced_paths_exist(self): ) self.assertEqual(missing, [], "SKILL.md names paths that do not exist") + # every bare helper script named in a backticked command must exist under helpers + def test_bare_helper_names_exist(self): + missing = sorted( + name + for name in set(HELPER_PATTERN.findall(self.text)) + if not (ROOT / "helpers" / name).exists() + ) + self.assertEqual(missing, [], "SKILL.md names helper scripts that do not exist") + if __name__ == "__main__": unittest.main() From b48e6031f135dfa58b38305fe0ef6b9e9c5dd447 Mon Sep 17 00:00:00 2001 From: Ismael Date: Wed, 2 Sep 2026 16:43:56 -0700 Subject: [PATCH 09/19] require comment text above a definition instead of a bare hash --- tests/test_comment_style.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_comment_style.py b/tests/test_comment_style.py index f0f02a5b..093c68ce 100644 --- a/tests/test_comment_style.py +++ b/tests/test_comment_style.py @@ -43,7 +43,7 @@ def audit(text): # decorators sit between the comment and the definition so look above the first decorator start = min([node.lineno, *[item.lineno for item in node.decorator_list]]) above = start - 1 - if above not in comments: + if above not in comments or not comments[above]: problems.append(f"line {node.lineno} has no comment above {node.name}") continue if PUNCTUATION.search(comments[above]): From 8eb96e91f3d7554c83676a332ee5a2f365af3491 Mon Sep 17 00:00:00 2001 From: Ismael Date: Tue, 1 Sep 2026 22:16:10 -0700 Subject: [PATCH 10/19] add manim teaching assets semantic chapters and preview tooling --- SKILL.md | 9 + skills/manim-video/README.md | 11 +- skills/manim-video/SKILL.md | 191 +++-- skills/manim-video/assets/__init__.py | 21 + .../manim-video/assets/concept_explainer.py | 223 ++++++ skills/manim-video/assets/domains/__init__.py | 37 + skills/manim-video/assets/domains/_common.py | 227 ++++++ skills/manim-video/assets/domains/biology.py | 195 ++++++ .../manim-video/assets/domains/computing.py | 328 +++++++++ skills/manim-video/assets/domains/finance.py | 309 ++++++++ skills/manim-video/assets/domains/math.py | 317 +++++++++ skills/manim-video/assets/domains/physics.py | 250 +++++++ skills/manim-video/assets/domains/systems.py | 252 +++++++ skills/manim-video/assets/teaching.py | 658 ++++++++++++++++++ .../references/animation-design-thinking.md | 10 +- skills/manim-video/references/animations.md | 5 +- .../references/concept-explainer.md | 177 +++++ skills/manim-video/references/mobjects.md | 4 +- .../manim-video/references/paper-explainer.md | 45 +- .../references/production-quality.md | 262 +++---- skills/manim-video/references/rendering.md | 7 +- .../manim-video/references/scene-planning.md | 222 +++--- skills/manim-video/references/teaching-api.md | 49 ++ .../manim-video/references/troubleshooting.md | 5 +- .../manim-video/references/visual-design.md | 205 +++--- skills/manim-video/scripts/preview_scene.py | 385 ++++++++++ tests/fixtures/manim_domain_gallery.py | 101 +++ tests/test_manim_domains.py | 66 ++ tests/test_manim_gallery.py | 40 ++ tests/test_manim_teaching.py | 149 ++++ tests/test_preview_scene.py | 110 +++ 31 files changed, 4368 insertions(+), 502 deletions(-) create mode 100644 skills/manim-video/assets/__init__.py create mode 100644 skills/manim-video/assets/concept_explainer.py create mode 100644 skills/manim-video/assets/domains/__init__.py create mode 100644 skills/manim-video/assets/domains/_common.py create mode 100644 skills/manim-video/assets/domains/biology.py create mode 100644 skills/manim-video/assets/domains/computing.py create mode 100644 skills/manim-video/assets/domains/finance.py create mode 100644 skills/manim-video/assets/domains/math.py create mode 100644 skills/manim-video/assets/domains/physics.py create mode 100644 skills/manim-video/assets/domains/systems.py create mode 100644 skills/manim-video/assets/teaching.py create mode 100644 skills/manim-video/references/concept-explainer.md create mode 100644 skills/manim-video/references/teaching-api.md create mode 100755 skills/manim-video/scripts/preview_scene.py create mode 100644 tests/fixtures/manim_domain_gallery.py create mode 100644 tests/test_manim_domains.py create mode 100644 tests/test_manim_gallery.py create mode 100644 tests/test_manim_teaching.py create mode 100644 tests/test_preview_scene.py diff --git a/SKILL.md b/SKILL.md index fa4b776d..495dc0f9 100644 --- a/SKILL.md +++ b/SKILL.md @@ -207,6 +207,15 @@ Pick the engine per animation slot. Do not default to Remotion just because the - **Manim** — formal diagrams, state machines, equation derivations, graph morphs. Read `skills/manim-video/SKILL.md` and its references for depth. - **PIL + PNG sequence + ffmpeg** — simple overlay cards: counters, typewriter text, single bar reveals, progressive draws. Fast to iterate, any aesthetic you want. The launch video used this. +For an original end-to-end explainer, complete `edit/visual_plan.md`, use one +independently renderable Manim class per narrative chapter with named internal +beats, and build persistent semantic objects from +`skills/manim-video/assets/teaching.py` and `skills/manim-video/assets/domains/`. +Narration is part of this deliverable by default unless the user explicitly +requests silence. This requirement does not apply to clip editing or an isolated +illustration overlay. Read `skills/manim-video/references/concept-explainer.md` +before authoring a Manim scene. + For HyperFrames slots, scaffold the slot inside `edit/animations/slot_/` with `npx --yes hyperframes init . --example blank --non-interactive --skip-skills`, build the HTML composition there, run the HyperFrames checks that fit the slot (`lint`, `validate`, and a draft render when practical), then produce the final overlay video with `npx --yes hyperframes render . -o render.mp4` or `--format webm -o render.webm` when alpha is required. Point the EDL overlay `file` at the actual rendered path. For Remotion slots, keep the Remotion project isolated inside the same slot directory, scaffold with `npx create-video@latest` or install Remotion locally there, render the composition to `render.mp4` with the project-local `remotion render` command, and verify duration and dimensions with `ffprobe`. diff --git a/skills/manim-video/README.md b/skills/manim-video/README.md index 4ed03d89..a8f3d465 100644 --- a/skills/manim-video/README.md +++ b/skills/manim-video/README.md @@ -4,7 +4,9 @@ Production pipeline for mathematical and technical animations using [Manim Commu ## What it does -Creates 3Blue1Brown-style animated videos from text prompts. The agent handles the full pipeline: creative planning, Python code generation, rendering, scene stitching, and iterative refinement. +Creates original semantic animated videos from text prompts. The agent handles +research, narration, the required visual plan, chapter authoring, targeted +preview, rendering, composition, and complete-video review. ## Use cases @@ -19,5 +21,10 @@ Creates 3Blue1Brown-style animated videos from text prompts. The agent handles t Python 3.10+, Manim CE (`pip install manim`), LaTeX, ffmpeg. ```bash -bash skills/creative/manim-video/scripts/setup.sh +bash skills/manim-video/scripts/setup.sh ``` + +Original explainers use `assets/teaching.py` for persistent named objects, +linked values, attention choreography, and chapter continuity. The broad +`assets/domains/` kit supplies style-neutral math, AI, systems, physics, biology, +finance, and business components without constraining custom Manim code. diff --git a/skills/manim-video/SKILL.md b/skills/manim-video/SKILL.md index 6edab8e7..9ed3782c 100644 --- a/skills/manim-video/SKILL.md +++ b/skills/manim-video/SKILL.md @@ -1,7 +1,7 @@ --- name: manim-video -description: "Production pipeline for mathematical and technical animations using Manim Community Edition. Creates 3Blue1Brown-style explainer videos, algorithm visualizations, equation derivations, architecture diagrams, and data stories. Use when users request: animated explanations, math animations, concept visualizations, algorithm walkthroughs, technical explainers, 3Blue1Brown style videos, or any programmatic animation with geometric/mathematical content." -version: 1.0.0 +description: "Production pipeline for original mathematical and technical animations using Manim Community Edition. Creates semantic explainers, algorithm visualizations, equation derivations, architecture diagrams, and data stories. Use when users request animated explanations, math animations, concept visualizations, algorithm walkthroughs, technical explainers, or programmatic geometric and mathematical animation." +version: 2.0.0 --- # Manim Video Production Pipeline @@ -16,12 +16,17 @@ This is educational cinema. Every frame teaches. Every animation reveals structu **First-render excellence is non-negotiable.** The output must be visually clear and aesthetically cohesive without revision rounds. If something looks cluttered, poorly timed, or like "AI-generated slides," it is wrong. -**Opacity layering directs attention.** Never show everything at full brightness. Primary elements at 1.0, contextual elements at 0.4, structural elements (axes, grids) at 0.15. The brain processes visual salience in layers. +**Continuity carries meaning.** Keep important objects alive across multiple teaching beats. Transform their state, update their linked representations together, and move the camera or dim context to direct attention. Reset an object only when that reset teaches something. -**Breathing room.** Every animation needs `self.wait()` after it. The viewer needs time to absorb what just appeared. Never rush from one animation to the next. A 2-second pause after a key reveal is never wasted. +**Breathing room.** Pause after a meaningful reveal for as long as the viewer needs to comprehend it. Every hold must serve reading, comparison, prediction, or payoff rather than mechanically following every animation. **Cohesive visual language.** All scenes share a color palette, consistent typography sizing, matching animation speeds. A technically correct video where every scene uses random different colors is an aesthetic failure. +**Narration is part of a topic-only explainer.** Unless the user explicitly asks +for silence, generate audible human or synthetic narration, synchronize the +teaching beats to it, and include matching captions. A silent audio file is a +hard failure, not a narration track. + ## Prerequisites Run `scripts/setup.sh` to verify all dependencies. Requires: Python 3.10+, Manim Community Edition v0.20+ (`pip install manim`), LaTeX (`texlive-full` on Linux, `mactex` on macOS), and ffmpeg. Reference docs tested against Manim CE v0.20.1. @@ -30,7 +35,7 @@ Run `scripts/setup.sh` to verify all dependencies. Requires: Python 3.10+, Manim | Mode | Input | Output | Reference | |------|-------|--------|-----------| -| **Concept explainer** | Topic/concept | Animated explanation with geometric intuition | `references/scene-planning.md` | +| **Concept explainer** | Topic/concept | Narrated explanation with geometric intuition | `references/concept-explainer.md` | | **Equation derivation** | Math expressions | Step-by-step animated proof | `references/equations.md` | | **Algorithm visualization** | Algorithm description | Step-by-step execution with data structures | `references/graphs-and-data.md` | | **Data story** | Data/metrics | Animated charts, comparisons, counters | `references/graphs-and-data.md` | @@ -38,6 +43,14 @@ Run `scripts/setup.sh` to verify all dependencies. Requires: Python 3.10+, Manim | **Paper explainer** | Research paper | Key findings and methods animated | `references/scene-planning.md` | | **3D visualization** | 3D concept | Rotating surfaces, parametric curves, spatial geometry | `references/camera-and-3d.md` | +For a narrated concept explainer, read `references/concept-explainer.md` completely +before planning. It is the focused production contract for narration locking, +visual-beat density, independently renderable narrative chapters, derived-value +checks, and targeted draft iteration. Use `assets/teaching.py` for semantic +continuity and the components in `assets/domains/`; retain +`assets/concept_explainer.py` for caption-safe layout, shared-frame composition, +and collision checks. + ## Stack Single Python script per project. No browser, no Node.js, no GPU required. @@ -52,49 +65,51 @@ Single Python script per project. No browser, no Node.js, no GPU required. ## Pipeline ``` -PLAN --> CODE --> RENDER --> STITCH --> AUDIO (optional) --> REVIEW +RESEARCH --> NARRATE --> VISUAL PLAN --> AUTHOR --> PREVIEW --> RENDER --> COMPOSE --> REVIEW ``` -1. **PLAN** — Write `plan.md` with narrative arc, scene list, visual elements, color palette, voiceover script -2. **CODE** — Write `script.py` with one class per scene, each independently renderable -3. **RENDER** — `manim -ql script.py Scene1 Scene2 ...` for draft, `-qh` for production -4. **STITCH** — ffmpeg concat of scene clips into `final.mp4` -5. **AUDIO** (optional) — Add voiceover and/or background music via ffmpeg. See `references/rendering.md` -6. **REVIEW** — Render preview stills, verify against plan, adjust +1. **RESEARCH** — Verify claims, examples, and derived values. +2. **NARRATE** — Write and generate audible narration that progresses from the misconception to a specific visual payoff; omit speech only when the user explicitly requests silence. +3. **VISUAL PLAN** — Complete the required `edit/visual_plan.md` contract. +4. **AUTHOR** — Build one independently renderable class per narrative chapter and mark its internal beats with `next_section()` or `begin_beat()`. +5. **PREVIEW** — Render only the chapters under repair with `scripts/preview_scene.py`. +6. **RENDER** — Render the complete visual base at delivery quality. +7. **COMPOSE** — Add approved footage when it is more informative than illustration, then use the existing EDL workflow for narration and captions. +8. **REVIEW** — Inspect the complete video and verify that its speech track is audible before loudness normalization; the final quality decision is never made from isolated preview clips. ## Project Structure ``` project-name/ - plan.md # Narrative arc, scene breakdown - script.py # All scenes in one file - concat.txt # ffmpeg scene list - final.mp4 # Stitched output - media/ # Auto-generated by Manim - videos/script/480p15/ + edit/ + visual_plan.md # Required semantic teaching plan for original explainers + animations/ # Chapter source and production renders + verify/ # Targeted chapter previews and review evidence + edl.json # Existing composition contract; unchanged + final.mp4 ``` ## Creative Direction -### Color Palettes +### Semantic Theme + +Create one `VisualTheme` for the approved direction. Its background, text, +muted, primary, secondary, accent, warning, and title/body/label font roles keep +meaning consistent without imposing a house palette. Components require the +same theme instance and never embed another creator's palette or branding. -| Palette | Background | Primary | Secondary | Accent | Use case | -|---------|-----------|---------|-----------|--------|----------| -| **Classic 3B1B** | `#1C1C1C` | `#58C4DD` (BLUE) | `#83C167` (GREEN) | `#FFFF00` (YELLOW) | General math/CS | -| **Warm academic** | `#2D2B55` | `#FF6B6B` | `#FFD93D` | `#6BCB77` | Approachable | -| **Neon tech** | `#0A0A0A` | `#00F5FF` | `#FF00FF` | `#39FF14` | Systems, architecture | -| **Monochrome** | `#1A1A2E` | `#EAEAEA` | `#888888` | `#FFFFFF` | Minimalist | +### Animation Timing Starting Points -### Animation Speed +| Context | Typical motion | Hold purpose | +|---------|----------------|--------------| +| Establish a model | 1–2s | orient to its parts | +| Key equation reveal | 1.5–2.5s | connect notation to the model | +| Transform/morph | 1–2s | compare initial and resulting state | +| Supporting label | 0.5–1s | read only when it carries new meaning | +| "Aha moment" reveal | 2–3s | study the final relationship | -| Context | run_time | self.wait() after | -|---------|----------|-------------------| -| Title/intro appear | 1.5s | 1.0s | -| Key equation reveal | 2.0s | 2.0s | -| Transform/morph | 1.5s | 1.5s | -| Supporting label | 0.8s | 0.5s | -| FadeOut cleanup | 0.5s | 0.3s | -| "Aha moment" reveal | 2.5s | 3.0s | +These are starting points. Narration alignment and comprehension decide the +real timing; do not add the listed hold mechanically. ### Typography Scale @@ -108,74 +123,88 @@ project-name/ ### Fonts -**Use monospace fonts for all text.** Manim's Pango renderer produces broken kerning with proportional fonts at all sizes. See `references/visual-design.md` for full recommendations. - -```python -MONO = "Menlo" # define once at top of file - -Text("Fourier Series", font_size=48, font=MONO, weight=BOLD) # titles -Text("n=1: sin(x)", font_size=20, font=MONO) # labels -MathTex(r"\nabla L") # math (uses LaTeX) -``` - -Minimum `font_size=18` for readability. +Typography follows the approved visual direction. Define title, body, and label +roles in `VisualTheme`, confirm the selected fonts exist on the render host, and +inspect their real output at medium quality. Monospace is appropriate for code +or an intentional aesthetic; it is not a universal requirement. Keep ordinary +text at `font_size=18` or larger and retain all width and frame-safety checks. -### Per-Scene Variation +### Continuity -Never use identical config for all scenes. For each scene: -- **Different dominant color** from the palette -- **Different layout** — don't always center everything -- **Different animation entry** — vary between Write, FadeIn, GrowFromCenter, Create -- **Different visual weight** — some scenes dense, others sparse +- Preserve a concept's color role and screen position unless changing either one teaches something. +- Carry important objects into later beats and transform them instead of recreating completed diagrams. +- Update coupled numbers, plots, arrows, and labels from the same underlying value. +- Use animation vocabulary for meaning: construction, transfer, comparison, causality, and state change. ## Workflow -### Step 1: Plan (plan.md) +### Step 1: Plan (`edit/visual_plan.md`) -Before any code, write `plan.md`. See `references/scene-planning.md` for the comprehensive template. +Before any original explainer code, complete `edit/visual_plan.md`. See +`references/concept-explainer.md` and `references/scene-planning.md`. Clip-editing +workflows do not require this artifact. + +When a chapter shares the frame with sourced footage, use +`assets/concept_explainer.py`. Its `composition_regions()`, +`assert_inside_region()`, and `normalized_bounds()` helpers use the same +normalized rectangles as the renderer, so the EDL can protect the teaching +visual from picture-in-picture collisions. ### Step 2: Code (script.py) -One class per scene. Every scene is independently renderable. +Use one class per narrative chapter. A chapter is independently renderable and +contains multiple named teaching beats; it is not a short title-and-card scene. ```python from manim import * +from teaching import TeachingScene, VisualTheme +from domains import MatrixMap -BG = "#1C1C1C" -PRIMARY = "#58C4DD" -SECONDARY = "#83C167" -ACCENT = "#FFFF00" -MONO = "Menlo" +THEME = VisualTheme( + background="#10141C", text="#F6F7FA", muted="#7F8899", + primary="#5E8BFF", secondary="#57C4A5", accent="#F2C14E", + warning="#E56B6F", title_font="Inter", body_font="Inter", + label_font="Inter", +) -class Scene1_Introduction(Scene): +class CompositionChapter(TeachingScene): def construct(self): - self.camera.background_color = BG - title = Text("Why Does This Work?", font_size=48, color=PRIMARY, weight=BOLD, font=MONO) - self.add_subcaption("Why does this work?", duration=2) - self.play(Write(title), run_time=1.5) - self.wait(1.0) - self.play(FadeOut(title), run_time=0.5) + self.theme = THEME + self.camera.background_color = THEME.background + transformation = self.remember("transformation", MatrixMap(THEME)) + self.begin_beat("first transformation") + self.play(FadeIn(transformation)) + self.begin_beat("composition payoff") + self.play(transformation.apply_to((1, 1))) + self.hold(2, purpose="compare input and composed output") ``` Key patterns: -- **Subtitles** on every animation: `self.add_subcaption("text", duration=N)` or `subcaption="text"` on `self.play()` -- **Shared color constants** at file top for cross-scene consistency -- **`self.camera.background_color`** set in every scene -- **Clean exits** — FadeOut all mobjects at scene end: `self.play(FadeOut(Group(*self.mobjects)))` +- **Narration mapping** for every significant visual action; captions remain composed last through the existing EDL renderer +- **One shared `VisualTheme`** for semantic color and typography roles +- **Named objects and beats** through `remember()` and `begin_beat()` +- **Portable easing imports** — import `ease_out_cubic` and + `ease_in_out_cubic` from the copied concept-explainer asset; do not assume + unqualified easing names are exported by `from manim import *` +- **Continuity at boundaries** — hold or transform the payoff frame; fade only objects whose departure has narrative meaning +- **Frame-safe endings** — call `assert_inside_frame()` and + `assert_no_overlap()` on each chapter's critical teaching states, including + the payoff frame. ### Step 3: Render ```bash -manim -ql script.py Scene1_Introduction Scene2_CoreConcept # draft -manim -qh script.py Scene1_Introduction Scene2_CoreConcept # production +python skills/manim-video/scripts/preview_scene.py \ + edit/animations/script.py CompositionChapter +manim -qh edit/animations/script.py CompositionChapter # production ``` ### Step 4: Stitch ```bash cat > concat.txt << 'EOF' -file 'media/videos/script/480p15/Scene1_Introduction.mp4' -file 'media/videos/script/480p15/Scene2_CoreConcept.mp4' +file 'media/videos/script/480p15/CompositionChapter.mp4' +file 'media/videos/script/480p15/SystemsChapter.mp4' EOF ffmpeg -y -f concat -safe 0 -i concat.txt -c copy final.mp4 ``` @@ -183,9 +212,15 @@ ffmpeg -y -f concat -safe 0 -i concat.txt -c copy final.mp4 ### Step 5: Review ```bash -manim -ql --format=png -s script.py Scene2_CoreConcept # preview still +python skills/manim-video/scripts/preview_scene.py \ + edit/animations/script.py CompositionChapter SystemsChapter ``` +The preview command writes only below `edit/verify/` and produces a low-quality +chapter video, first and final frames, a five-frame contact sheet, media +metadata, render status, and saved `next_section()` metadata. It selects whole +scene classes; Manim does not support arbitrary section-only rendering here. + ## Critical Implementation Notes ### Raw Strings for LaTeX @@ -225,13 +260,15 @@ Always iterate at `-ql`. Only render `-qh` for final output. | File | Contents | |------|----------| +| `references/concept-explainer.md` | Required narrated-explainer contract: audio lock, visual beats, timing, correctness, and efficient iteration | +| `references/teaching-api.md` | Semantic scene API, linked values, continuity helpers, and domain component index | | `references/animations.md` | Core animations, rate functions, composition, `.animate` syntax, timing patterns | | `references/mobjects.md` | Text, shapes, VGroup/Group, positioning, styling, custom mobjects | -| `references/visual-design.md` | 12 design principles, opacity layering, layout templates, color palettes | +| `references/visual-design.md` | Semantic design principles, attention, layout patterns, themes, and typography | | `references/equations.md` | LaTeX in Manim, TransformMatchingTex, derivation patterns | | `references/graphs-and-data.md` | Axes, plotting, BarChart, animated data, algorithm visualization | | `references/camera-and-3d.md` | MovingCameraScene, ThreeDScene, 3D surfaces, camera control | -| `references/scene-planning.md` | Narrative arcs, layout templates, scene transitions, planning template | +| `references/scene-planning.md` | Narrative arcs, chapter continuity, beat planning, and visual-plan template | | `references/rendering.md` | CLI reference, quality presets, ffmpeg, voiceover workflow, GIF export | | `references/troubleshooting.md` | LaTeX errors, animation errors, common mistakes, debugging | | `references/animation-design-thinking.md` | When to animate vs show static, decomposition, pacing, narration sync | diff --git a/skills/manim-video/assets/__init__.py b/skills/manim-video/assets/__init__.py new file mode 100644 index 00000000..d8df1d77 --- /dev/null +++ b/skills/manim-video/assets/__init__.py @@ -0,0 +1,21 @@ +"""Reusable style-neutral Manim explainer assets.""" + +from .teaching import ( + LinkedValue, + SemanticMobject, + TeachingScene, + TeachingThreeDScene, + VisualTheme, + fit_to_frame, + theme_text, +) + +__all__ = [ + "LinkedValue", + "SemanticMobject", + "TeachingScene", + "TeachingThreeDScene", + "VisualTheme", + "fit_to_frame", + "theme_text", +] diff --git a/skills/manim-video/assets/concept_explainer.py b/skills/manim-video/assets/concept_explainer.py new file mode 100644 index 00000000..c4dca82c --- /dev/null +++ b/skills/manim-video/assets/concept_explainer.py @@ -0,0 +1,223 @@ +"""Small layout guards for narrated 16:9 Manim explainers. + +Import these helpers into a project's ``script.py``. They keep authored visuals +above the caption rail and make text fit its container before rendering. +""" + +from __future__ import annotations + +from itertools import combinations + +from manim import DOWN, UP, Mobject, Rectangle, RoundedRectangle, Text, VGroup, config + + +CAPTION_RAIL_FRACTION = 0.16 + +COMPOSITION_REGIONS = { + "split_left": { + "media": {"x": 0.04, "y": 0.17, "width": 0.44, "height": 0.57}, + "illustration": {"x": 0.52, "y": 0.17, "width": 0.44, "height": 0.57}, + }, + "split_right": { + "media": {"x": 0.52, "y": 0.17, "width": 0.44, "height": 0.57}, + "illustration": {"x": 0.04, "y": 0.17, "width": 0.44, "height": 0.57}, + }, +} + + +def normalized_region(rect: dict[str, float]) -> Rectangle: + """Convert a top-left normalized video rectangle into a Manim guide.""" + try: + x, y, width, height = (float(rect[key]) for key in ("x", "y", "width", "height")) + except (KeyError, TypeError, ValueError) as exc: + raise ValueError("region requires numeric x, y, width, and height") from exc + if x < 0 or y < 0 or width <= 0 or height <= 0: + raise ValueError("region must use non-negative x/y and positive width/height") + if x + width > 1.000001 or y + height > 1.000001: + raise ValueError("region must stay inside the normalized frame") + guide = Rectangle( + width=config.frame_width * width, + height=config.frame_height * height, + stroke_opacity=0, + fill_opacity=0, + ) + center_x = (x + width / 2 - 0.5) * config.frame_width + center_y = (0.5 - y - height / 2) * config.frame_height + guide.move_to((center_x, center_y, 0)) + return guide + + +def composition_regions(mode: str) -> dict[str, Rectangle]: + """Return non-overlapping media/illustration guides for a split scene.""" + if mode not in COMPOSITION_REGIONS: + choices = ", ".join(sorted(COMPOSITION_REGIONS)) + raise ValueError(f"unknown composition '{mode}'; choose {choices}") + return { + name: normalized_region(rect) + for name, rect in COMPOSITION_REGIONS[mode].items() + } + + +def fit_mobject(mobject: Mobject, max_width: float, max_height: float) -> Mobject: + """Scale down, never up, until a mobject fits the requested box.""" + if max_width <= 0 or max_height <= 0: + raise ValueError("max_width and max_height must be positive") + scale = min(1.0, max_width / mobject.width, max_height / mobject.height) + mobject.scale(scale) + return mobject + + +def fitted_text( + text: str, + *, + max_width: float, + max_height: float, + font: str = "Menlo", + font_size: int = 48, + **kwargs, +) -> Text: + """Create text at the desired size, then shrink it to fit if needed.""" + return fit_mobject( + Text(text, font=font, font_size=font_size, **kwargs), + max_width, + max_height, + ) + + +def safe_panel( + content: Mobject, + *, + width: float, + height: float, + padding_x: float = 0.35, + padding_y: float = 0.28, + corner_radius: float = 0.18, + stroke_color: str = "#3B4B68", + fill_color: str = "#101827", + fill_opacity: float = 0.92, +) -> VGroup: + """Return a panel whose content is guaranteed to fit its inner bounds.""" + panel = RoundedRectangle( + width=width, + height=height, + corner_radius=corner_radius, + stroke_color=stroke_color, + stroke_width=2, + fill_color=fill_color, + fill_opacity=fill_opacity, + ) + fit_mobject(content, width - 2 * padding_x, height - 2 * padding_y) + content.move_to(panel) + return VGroup(panel, content) + + +def content_frame( + *, + caption_rail_fraction: float = CAPTION_RAIL_FRACTION, + margin: float = 0.28, +) -> Rectangle: + """Create an invisible guide for the portion of frame above captions.""" + if not 0 <= caption_rail_fraction < 0.5: + raise ValueError("caption_rail_fraction must be between 0 and 0.5") + rail_height = config.frame_height * caption_rail_fraction + guide = Rectangle( + width=config.frame_width - 2 * margin, + height=config.frame_height - rail_height - 2 * margin, + stroke_opacity=0, + fill_opacity=0, + ) + guide.shift(UP * rail_height / 2) + return guide + + +def assert_inside_frame( + mobject: Mobject, + *, + caption_safe: bool = True, + caption_rail_fraction: float = CAPTION_RAIL_FRACTION, + margin: float = 0.18, +) -> Mobject: + """Raise before render if a mobject leaves the frame or caption-safe area.""" + left = -config.frame_width / 2 + margin + right = config.frame_width / 2 - margin + top = config.frame_height / 2 - margin + rail_height = config.frame_height * caption_rail_fraction if caption_safe else 0 + bottom = -config.frame_height / 2 + rail_height + margin + if ( + mobject.get_left()[0] < left + or mobject.get_right()[0] > right + or mobject.get_top()[1] > top + or mobject.get_bottom()[1] < bottom + ): + raise ValueError("mobject leaves the frame or enters the reserved caption rail") + return mobject + + +def assert_inside_region( + mobject: Mobject, + region: Rectangle, + *, + margin: float = 0.08, +) -> Mobject: + """Raise when a mobject leaves its assigned composition region.""" + if ( + mobject.get_left()[0] < region.get_left()[0] + margin + or mobject.get_right()[0] > region.get_right()[0] - margin + or mobject.get_top()[1] > region.get_top()[1] - margin + or mobject.get_bottom()[1] < region.get_bottom()[1] + margin + ): + raise ValueError("mobject leaves its assigned composition region") + return mobject + + +def normalized_bounds(mobject: Mobject, *, padding: float = 0.0) -> dict[str, float]: + """Return an EDL-ready protected rectangle for a final-state mobject.""" + if padding < 0: + raise ValueError("padding must be non-negative") + left = max(-config.frame_width / 2, mobject.get_left()[0] - padding) + right = min(config.frame_width / 2, mobject.get_right()[0] + padding) + top = min(config.frame_height / 2, mobject.get_top()[1] + padding) + bottom = max(-config.frame_height / 2, mobject.get_bottom()[1] - padding) + return { + "x": (left + config.frame_width / 2) / config.frame_width, + "y": (config.frame_height / 2 - top) / config.frame_height, + "width": (right - left) / config.frame_width, + "height": (top - bottom) / config.frame_height, + } + + +def mobjects_overlap(first: Mobject, second: Mobject, *, gap: float = 0.05) -> bool: + """Return whether two axis-aligned Manim bounds overlap, including a gap.""" + return not ( + first.get_right()[0] + gap <= second.get_left()[0] + or second.get_right()[0] + gap <= first.get_left()[0] + or first.get_top()[1] + gap <= second.get_bottom()[1] + or second.get_top()[1] + gap <= first.get_bottom()[1] + ) + + +def assert_no_overlap(*mobjects: Mobject, gap: float = 0.05) -> None: + """Raise when any pair of final-state mobjects overlap.""" + for first, second in combinations(mobjects, 2): + if mobjects_overlap(first, second, gap=gap): + raise ValueError("final-state mobjects overlap; resize or reposition them") + + +def source_footer( + text: str, + *, + color: str = "#8E9BB5", + font: str = "Menlo", +) -> Text: + """Create a restrained source note just above the caption rail.""" + footer = fitted_text( + text, + max_width=config.frame_width - 1.0, + max_height=0.28, + font=font, + font_size=18, + color=color, + ) + rail_height = config.frame_height * CAPTION_RAIL_FRACTION + footer.to_edge(DOWN, buff=rail_height + 0.12) + return assert_inside_frame(footer) diff --git a/skills/manim-video/assets/domains/__init__.py b/skills/manim-video/assets/domains/__init__.py new file mode 100644 index 00000000..3e439595 --- /dev/null +++ b/skills/manim-video/assets/domains/__init__.py @@ -0,0 +1,37 @@ +"""Broad semantic component kit for original Manim explainers.""" + +from .biology import CellProcess, PopulationFlow, SequenceProcess +from .computing import ArrayModel, GraphModel, NeuralLayer, StateMachine, TokenFlow +from .finance import CashFlow, CompoundTimeline, FeedbackLoop, Funnel, ResourceFlow +from .math import LinkedPlot, MatrixMap, NumberLineModel, ProbabilityMass, VectorMap +from .physics import BodySystem, CircuitFlow, ForceVector, WaveField +from .systems import DataPipeline, QueueModel, RequestFlow, ServiceGraph + +__all__ = [ + "ArrayModel", + "BodySystem", + "CashFlow", + "CellProcess", + "CircuitFlow", + "CompoundTimeline", + "DataPipeline", + "FeedbackLoop", + "ForceVector", + "Funnel", + "GraphModel", + "LinkedPlot", + "MatrixMap", + "NeuralLayer", + "NumberLineModel", + "PopulationFlow", + "ProbabilityMass", + "QueueModel", + "RequestFlow", + "ResourceFlow", + "SequenceProcess", + "ServiceGraph", + "StateMachine", + "TokenFlow", + "VectorMap", + "WaveField", +] diff --git a/skills/manim-video/assets/domains/_common.py b/skills/manim-video/assets/domains/_common.py new file mode 100644 index 00000000..42905065 --- /dev/null +++ b/skills/manim-video/assets/domains/_common.py @@ -0,0 +1,227 @@ +"""Shared construction helpers for the style-neutral semantic domain kit.""" + +from __future__ import annotations + +from collections.abc import Iterable, Sequence + +import numpy as np +from manim import ( + Arrow, + Circle, + DOWN, + Mobject, + RIGHT, + RoundedRectangle, + UP, + VGroup, +) + +try: + from ..teaching import VisualTheme, fit_to_frame, theme_text +except ImportError: # Supports copying ``teaching.py`` and ``domains/`` together. + from teaching import VisualTheme, fit_to_frame, theme_text + + +def require_theme(theme: VisualTheme) -> VisualTheme: + if not isinstance(theme, VisualTheme): + raise TypeError("domain components require a shared VisualTheme") + return theme + + +def normalized_labels(labels: Iterable[object] | None, *, fallback: Sequence[str]) -> list[str]: + values = [str(value) for value in labels] if labels is not None else list(fallback) + return values or [""] + + +def label_for( + value: object, + theme: VisualTheme, + *, + size: float = 22, + color: str | None = None, + width: float = 1.55, +) -> Mobject: + return theme_text( + value, + theme, + role="label", + font_size=size, + color=color, + max_width=max(width, 0.05), + ) + + +def labeled_box( + label: object, + theme: VisualTheme, + *, + width: float = 1.8, + height: float = 0.78, + color: str | None = None, + fill_opacity: float = 0.12, + font_size: float = 22, +) -> VGroup: + stroke = color or theme.primary + box = RoundedRectangle( + width=width, + height=height, + corner_radius=min(0.14, height / 4), + color=stroke, + stroke_width=2, + fill_color=stroke, + fill_opacity=fill_opacity, + ) + text = label_for(label, theme, size=font_size, width=width - 0.18) + return VGroup(box, text) + + +def circle_node( + label: object, + theme: VisualTheme, + *, + radius: float = 0.38, + color: str | None = None, +) -> VGroup: + stroke = color or theme.primary + circle = Circle( + radius=radius, + color=stroke, + stroke_width=2, + fill_color=stroke, + fill_opacity=0.12, + ) + text = label_for(label, theme, size=19, width=radius * 1.55) + return VGroup(circle, text) + + +def arrow_between( + start: Mobject | Sequence[float], + end: Mobject | Sequence[float], + theme: VisualTheme, + *, + color: str | None = None, + buff: float = 0.12, +) -> Arrow: + start_point = start.get_center() if isinstance(start, Mobject) else np.asarray(start, dtype=float) + end_point = end.get_center() if isinstance(end, Mobject) else np.asarray(end, dtype=float) + return Arrow( + start_point, + end_point, + buff=buff, + color=color or theme.muted, + stroke_width=2.4, + max_tip_length_to_length_ratio=0.16, + ) + + +def row_of_boxes( + labels: Iterable[object] | None, + theme: VisualTheme, + *, + fallback: Sequence[str], + color: str | None = None, + box_width: float = 1.65, + box_height: float = 0.74, + buff: float = 0.42, +) -> VGroup: + values = normalized_labels(labels, fallback=fallback) + boxes = VGroup( + *[ + labeled_box( + value, + theme, + width=box_width, + height=box_height, + color=color, + ) + for value in values + ] + ).arrange(RIGHT, buff=buff) + return boxes + + +def arrows_for_row(nodes: VGroup, theme: VisualTheme, *, color: str | None = None) -> VGroup: + return VGroup( + *[ + Arrow( + nodes[index].get_right(), + nodes[index + 1].get_left(), + buff=0.08, + color=color or theme.muted, + stroke_width=2.2, + max_tip_length_to_length_ratio=0.22, + ) + for index in range(max(0, len(nodes) - 1)) + ] + ) + + +def title_above(label: object, body: Mobject, theme: VisualTheme) -> Mobject: + title = theme_text(label, theme, role="body", font_size=26, max_width=body.width) + title.next_to(body, UP, buff=0.28) + return title + + +def frame_safe(component: Mobject) -> Mobject: + return fit_to_frame(component, margin=0.45) + + +def ensure_index(index: int, size: int, *, name: str = "index") -> int: + if not 0 <= int(index) < size: + raise IndexError(f"{name} {index} is outside 0..{size - 1}") + return int(index) + + +def ensure_amount(amount: float, *, available: float | None = None) -> float: + value = float(amount) + if value < 0: + raise ValueError("amount cannot be negative") + if available is not None and value > available + 1e-9: + raise ValueError(f"amount {value} exceeds available value {available}") + return value + + +def two_row_flow( + top_labels: Iterable[object], + bottom_labels: Iterable[object], + theme: VisualTheme, +) -> tuple[VGroup, VGroup, VGroup]: + top = row_of_boxes(top_labels, theme, fallback=[""], color=theme.primary) + bottom = row_of_boxes(bottom_labels, theme, fallback=[""], color=theme.secondary) + width = max(top.width, bottom.width) + if top.width: + top.scale_to_fit_width(width) + if bottom.width: + bottom.scale_to_fit_width(width) + VGroup(top, bottom).arrange(DOWN, buff=1.0) + count = min(len(top), len(bottom)) + links = VGroup( + *[ + Arrow( + top[index].get_bottom(), + bottom[index].get_top(), + buff=0.08, + color=theme.accent, + stroke_width=2, + ) + for index in range(count) + ] + ) + return top, bottom, links + + +__all__ = [ + "arrow_between", + "arrows_for_row", + "circle_node", + "ensure_amount", + "ensure_index", + "frame_safe", + "label_for", + "labeled_box", + "normalized_labels", + "require_theme", + "row_of_boxes", + "title_above", + "two_row_flow", +] diff --git a/skills/manim-video/assets/domains/biology.py b/skills/manim-video/assets/domains/biology.py new file mode 100644 index 00000000..4eea6c30 --- /dev/null +++ b/skills/manim-video/assets/domains/biology.py @@ -0,0 +1,195 @@ +"""Semantic components for cellular, sequence, and population processes.""" + +from __future__ import annotations + +from collections.abc import Iterable, Mapping, Sequence +from typing import Any + +from manim import ( + Circle, + Dot, + DOWN, + RIGHT, + Transform, + UP, + VGroup, +) + +try: + from ..teaching import SemanticMobject, VisualTheme +except ImportError: + from teaching import SemanticMobject, VisualTheme + +from ._common import ( + arrows_for_row, + ensure_amount, + ensure_index, + frame_safe, + label_for, + labeled_box, + normalized_labels, + title_above, +) + + +class CellProcess(SemanticMobject): + def __init__( + self, + theme: VisualTheme, + stages: Iterable[object] | None = None, + *, + label: object = "", + ) -> None: + super().__init__(theme) + self.stages = normalized_labels(stages, fallback=["signal", "nucleus", "protein"]) + membrane = Circle( + radius=2.35, + color=theme.secondary, + fill_color=theme.secondary, + fill_opacity=0.05, + stroke_width=3, + ) + nodes = VGroup( + *[ + labeled_box(stage, theme, width=1.35, height=0.66, color=theme.primary) + for stage in self.stages + ] + ).arrange(RIGHT, buff=0.34) + if nodes.width > 4.1: + nodes.scale_to_fit_width(4.1) + links = arrows_for_row(nodes, theme, color=theme.accent) + cargo = Dot(nodes[0].get_top() + UP * 0.25, color=theme.warning, radius=0.09) + caption = title_above(label, membrane, theme) + self.stage = 0 + self.register_part("membrane", membrane) + self.register_part("stages", nodes) + self.register_part("links", links) + self.register_part("cargo", cargo) + self.register_part("label", caption) + for index, stage in enumerate(nodes): + self.register_anchor(f"stage_{index}", stage) + frame_safe(self) + + def advance(self, destination: int | None = None) -> Any: + target = self.stage + 1 if destination is None else int(destination) + target = ensure_index(target, len(self.stages), name="cell-process destination") + self.stage = target + return self.part("cargo").animate.move_to( + self.part("stages")[target].get_top() + UP * 0.25 + ) + + +class SequenceProcess(SemanticMobject): + _TRANSCRIPTION = {"A": "U", "T": "A", "C": "G", "G": "C", "U": "A"} + + def __init__( + self, + theme: VisualTheme, + sequence: str = "ATGCGT", + *, + label: object = "", + ) -> None: + super().__init__(theme) + self.dna = str(sequence).upper() + invalid = sorted(set(self.dna) - {"A", "T", "C", "G"}) + if invalid: + raise ValueError(f"DNA sequence contains unsupported bases: {', '.join(invalid)}") + dna_row = self._row(self.dna, color=theme.primary) + rna_row = self._row("" if self.dna else "", color=theme.secondary) + protein_row = self._row("", color=theme.accent) + labels = VGroup( + label_for("DNA", theme, size=18, color=theme.primary, width=0.8).next_to(dna_row, direction=[-1, 0, 0], buff=0.25), + label_for("RNA", theme, size=18, color=theme.secondary, width=0.8).next_to(rna_row, direction=[-1, 0, 0], buff=0.25), + label_for("protein", theme, size=18, color=theme.accent, width=1.0).next_to(protein_row, direction=[-1, 0, 0], buff=0.25), + ) + rows = VGroup(dna_row, rna_row, protein_row).arrange(DOWN, buff=0.58) + # Reposition labels after arranging the rows. + for text, row in zip(labels, rows): + text.next_to(row, direction=[-1, 0, 0], buff=0.25) + caption = title_above(label, VGroup(rows, labels), theme) + self.rna = "" + self.protein: list[str] = [] + self.register_part("dna", dna_row) + self.register_part("rna", rna_row) + self.register_part("protein", protein_row) + self.register_part("row_labels", labels) + self.register_part("label", caption) + frame_safe(self) + + def _row(self, sequence: str | Sequence[str], *, color: str) -> VGroup: + values = list(sequence) or [""] + return VGroup( + *[ + labeled_box(base, self.theme, width=0.58, height=0.55, color=color, font_size=18) + for base in values + ] + ).arrange(RIGHT, buff=0.07) + + def transcribe(self) -> Transform: + self.rna = "".join(self._TRANSCRIPTION[base] for base in self.dna) + replacement = self._row(self.rna, color=self.theme.secondary).move_to(self.part("rna")) + return Transform(self.part("rna"), replacement) + + def translate(self) -> Transform: + if not self.rna: + raise RuntimeError("transcribe the sequence before translating it") + codons = [self.rna[index:index + 3] for index in range(0, len(self.rna), 3)] + self.protein = [f"aa{index + 1}" for index, codon in enumerate(codons) if len(codon) == 3] + replacement = self._row(self.protein, color=self.theme.accent).move_to(self.part("protein")) + return Transform(self.part("protein"), replacement) + + +class PopulationFlow(SemanticMobject): + def __init__( + self, + theme: VisualTheme, + populations: Mapping[object, float] | None = None, + *, + label: object = "", + ) -> None: + super().__init__(theme) + source = populations if populations is not None else {"susceptible": 80, "infected": 20} + self.names = [str(name) for name in source] + self.counts = [float(value) for value in source.values()] + if not self.names: + self.names = [""] + self.counts = [0.0] + if any(value < 0 for value in self.counts): + raise ValueError("population counts cannot be negative") + compartments = self._compartments() + links = arrows_for_row(compartments, theme, color=theme.secondary) + caption = title_above(label, VGroup(compartments, links), theme) + self.register_part("compartments", compartments) + self.register_part("links", links) + self.register_part("label", caption) + for index, compartment in enumerate(compartments): + self.register_anchor(f"population_{index}", compartment) + frame_safe(self) + + def _compartments(self) -> VGroup: + return VGroup( + *[ + labeled_box( + f"{name}\n{count:g}", + self.theme, + width=1.65, + height=1.0, + color=self.theme.primary if index % 2 == 0 else self.theme.secondary, + font_size=18, + ) + for index, (name, count) in enumerate(zip(self.names, self.counts)) + ] + ).arrange(RIGHT, buff=0.65) + + def transfer(self, source: int, target: int, amount: float) -> Transform: + source_index = ensure_index(source, len(self.counts), name="source population") + target_index = ensure_index(target, len(self.counts), name="target population") + if source_index == target_index: + raise ValueError("population source and target must differ") + value = ensure_amount(amount, available=self.counts[source_index]) + self.counts[source_index] -= value + self.counts[target_index] += value + return Transform(self.part("compartments"), self._compartments()) + + +__all__ = ["CellProcess", "PopulationFlow", "SequenceProcess"] diff --git a/skills/manim-video/assets/domains/computing.py b/skills/manim-video/assets/domains/computing.py new file mode 100644 index 00000000..1ac4b64d --- /dev/null +++ b/skills/manim-video/assets/domains/computing.py @@ -0,0 +1,328 @@ +"""Semantic components for algorithms, machine learning, and AI.""" + +from __future__ import annotations + +from collections.abc import Iterable, Sequence +from typing import Any + +import numpy as np +from manim import ( + AnimationGroup, + Arrow, + CurvedArrow, + DOWN, + RIGHT, + SurroundingRectangle, + Transform, + VGroup, +) + +try: + from ..teaching import SemanticMobject, VisualTheme +except ImportError: + from teaching import SemanticMobject, VisualTheme + +from ._common import ( + circle_node, + ensure_index, + frame_safe, + label_for, + labeled_box, + normalized_labels, + title_above, +) + + +class ArrayModel(SemanticMobject): + def __init__( + self, + theme: VisualTheme, + values: Iterable[object] | None = None, + *, + label: object = "", + ) -> None: + super().__init__(theme) + self.values = normalized_labels(values, fallback=["4", "1", "3", "2"]) + cells = self._make_cells() + indices = VGroup( + *[label_for(index, theme, size=16, color=theme.muted, width=0.7) for index in range(len(self.values))] + ).arrange(RIGHT, buff=0.1) + for index, cell in enumerate(cells): + indices[index].next_to(cell, DOWN, buff=0.1) + caption = title_above(label, cells, theme) + self.register_part("cells", cells) + self.register_part("indices", indices) + self.register_part("label", caption) + for index, cell in enumerate(cells): + self.register_anchor(f"index_{index}", cell) + frame_safe(self) + + def _make_cells(self) -> VGroup: + cells = VGroup( + *[ + labeled_box(value, self.theme, width=0.82, height=0.78, color=self.theme.primary) + for value in self.values + ] + ).arrange(RIGHT, buff=0.1) + return cells + + def highlight_index(self, index: int) -> Any: + selected = ensure_index(index, len(self.values)) + return self.part("cells")[selected].animate.set_color(self.theme.accent) + + def swap(self, first: int, second: int) -> AnimationGroup: + left = ensure_index(first, len(self.values), name="first index") + right = ensure_index(second, len(self.values), name="second index") + if left == right: + raise ValueError("swap indices must differ") + cells = self.part("cells") + left_position = cells[left].get_center().copy() + right_position = cells[right].get_center().copy() + self.values[left], self.values[right] = self.values[right], self.values[left] + return AnimationGroup( + cells[left].animate.move_to(right_position), + cells[right].animate.move_to(left_position), + lag_ratio=0, + ) + + def set_value(self, index: int, value: object) -> Transform: + selected = ensure_index(index, len(self.values)) + self.values[selected] = str(value) + replacement = labeled_box( + value, + self.theme, + width=0.82, + height=0.78, + color=self.theme.secondary, + ).move_to(self.part("cells")[selected]) + return Transform(self.part("cells")[selected], replacement) + + +class GraphModel(SemanticMobject): + def __init__( + self, + theme: VisualTheme, + nodes: Iterable[object] | None = None, + edges: Iterable[Sequence[object]] | None = None, + *, + label: object = "", + ) -> None: + super().__init__(theme) + self.node_names = normalized_labels(nodes, fallback=["A", "B", "C", "D"]) + self.node_index = {name: index for index, name in enumerate(self.node_names)} + if len(self.node_index) != len(self.node_names): + raise ValueError("graph node labels must be unique") + default_edges = list(zip(self.node_names, self.node_names[1:])) + self.edges = [(str(start), str(end)) for start, end in (edges if edges is not None else default_edges)] + count = len(self.node_names) + radius = min(2.25, 0.7 + count * 0.25) + node_group = VGroup() + for index, name in enumerate(self.node_names): + angle = np.pi / 2 - 2 * np.pi * index / max(count, 1) + node = circle_node(name, theme, radius=0.39) + node.move_to([radius * np.cos(angle), radius * np.sin(angle), 0]) + node_group.add(node) + edge_group = VGroup() + for start, end in self.edges: + if start not in self.node_index or end not in self.node_index: + raise ValueError(f"edge {start!r}->{end!r} names an unknown graph node") + source = node_group[self.node_index[start]] + target = node_group[self.node_index[end]] + edge_group.add( + Arrow( + source.get_center(), + target.get_center(), + buff=0.45, + color=theme.muted, + stroke_width=2, + max_tip_length_to_length_ratio=0.16, + ) + ) + caption = title_above(label, VGroup(edge_group, node_group), theme) + self.visited: set[str] = set() + self.register_part("edges", edge_group) + self.register_part("nodes", node_group) + self.register_part("label", caption) + for name, node in zip(self.node_names, node_group): + self.register_anchor(f"node_{name}", node) + frame_safe(self) + + def visit_node(self, name: object) -> Any: + key = str(name) + if key not in self.node_index: + raise KeyError(f"unknown graph node {key!r}") + self.visited.add(key) + return self.part("nodes")[self.node_index[key]].animate.set_color(self.theme.accent) + + def traverse(self, start: object, end: object) -> Any: + edge = (str(start), str(end)) + try: + index = self.edges.index(edge) + except ValueError as exc: + raise KeyError(f"unknown directed graph edge {edge[0]!r}->{edge[1]!r}") from exc + return self.part("edges")[index].animate.set_color(self.theme.secondary).set_stroke(width=4) + + +class StateMachine(SemanticMobject): + def __init__( + self, + theme: VisualTheme, + states: Iterable[object] | None = None, + transitions: Iterable[Sequence[object]] | None = None, + *, + initial: object | None = None, + label: object = "", + ) -> None: + super().__init__(theme) + self.states = normalized_labels(states, fallback=["idle", "work", "done"]) + self.state_index = {state: index for index, state in enumerate(self.states)} + if len(self.state_index) != len(self.states): + raise ValueError("state names must be unique") + self.transitions = [ + (str(start), str(end)) + for start, end in ( + transitions if transitions is not None else zip(self.states, self.states[1:]) + ) + ] + nodes = VGroup(*[circle_node(state, theme, radius=0.48) for state in self.states]).arrange(RIGHT, buff=1.0) + arrows = VGroup() + for start, end in self.transitions: + if start not in self.state_index or end not in self.state_index: + raise ValueError(f"transition {start!r}->{end!r} names an unknown state") + arrows.add( + Arrow( + nodes[self.state_index[start]].get_right(), + nodes[self.state_index[end]].get_left(), + buff=0.08, + color=theme.muted, + stroke_width=2, + ) + ) + first = str(initial) if initial is not None else self.states[0] + if first not in self.state_index: + raise ValueError(f"unknown initial state {first!r}") + self.current_state = first + marker = SurroundingRectangle( + nodes[self.state_index[first]], + color=theme.accent, + buff=0.10, + corner_radius=0.16, + ) + caption = title_above(label, VGroup(nodes, arrows), theme) + self.register_part("transitions", arrows) + self.register_part("states", nodes) + self.register_part("current", marker) + self.register_part("label", caption) + frame_safe(self) + + def transition_to(self, state: object) -> Transform: + destination = str(state) + if destination not in self.state_index: + raise KeyError(f"unknown state {destination!r}") + if (self.current_state, destination) not in self.transitions: + raise ValueError(f"no transition from {self.current_state!r} to {destination!r}") + self.current_state = destination + marker = SurroundingRectangle( + self.part("states")[self.state_index[destination]], + color=self.theme.accent, + buff=0.10, + corner_radius=0.16, + ) + return Transform(self.part("current"), marker) + + +class TokenFlow(SemanticMobject): + def __init__( + self, + theme: VisualTheme, + tokens: Iterable[object] | None = None, + *, + label: object = "", + ) -> None: + super().__init__(theme) + self.tokens = normalized_labels(tokens, fallback=["The", "cat", "sat"]) + token_nodes = VGroup( + *[labeled_box(token, theme, width=1.25, height=0.7, color=theme.primary) for token in self.tokens] + ).arrange(RIGHT, buff=0.38) + links = VGroup() + caption = title_above(label, token_nodes, theme) + self.register_part("tokens", token_nodes) + self.register_part("context_links", links) + self.register_part("label", caption) + for index, token in enumerate(token_nodes): + self.register_anchor(f"token_{index}", token) + frame_safe(self) + + def mix_context(self, target: int, weights: Sequence[float]) -> Transform: + target_index = ensure_index(target, len(self.tokens), name="target token") + if len(weights) != len(self.tokens): + raise ValueError("attention weights must match the token count") + values = [float(weight) for weight in weights] + if any(weight < 0 for weight in values) or not np.isclose(sum(values), 1.0, atol=1e-8): + raise ValueError("attention weights must be non-negative and sum to one") + tokens = self.part("tokens") + links = VGroup() + for index, weight in enumerate(values): + if index == target_index or weight <= 0: + continue + links.add( + CurvedArrow( + tokens[index].get_top(), + tokens[target_index].get_top(), + angle=-0.45 if index < target_index else 0.45, + color=self.theme.accent, + stroke_width=1.5 + 6 * weight, + stroke_opacity=0.25 + 0.75 * weight, + ) + ) + self.context_weights = values + return Transform(self.part("context_links"), links) + + +class NeuralLayer(SemanticMobject): + def __init__( + self, + theme: VisualTheme, + size: int = 5, + *, + labels: Iterable[object] | None = None, + label: object = "", + ) -> None: + super().__init__(theme) + if size < 1: + raise ValueError("neural layer size must be positive") + names = [str(value) for value in labels] if labels is not None else [""] * size + if len(names) != size: + raise ValueError("neuron labels must match layer size") + neurons = VGroup( + *[circle_node(name, theme, radius=0.30, color=theme.primary) for name in names] + ).arrange(DOWN, buff=0.28) + caption = title_above(label, neurons, theme) + self.activations = [0.0] * size + self.register_part("neurons", neurons) + self.register_part("label", caption) + for index, neuron in enumerate(neurons): + self.register_anchor(f"neuron_{index}", neuron) + frame_safe(self) + + def activate(self, values: Sequence[float]) -> AnimationGroup: + if len(values) != len(self.activations): + raise ValueError("activation values must match layer size") + normalized = [float(value) for value in values] + if any(not 0 <= value <= 1 for value in normalized): + raise ValueError("activation values must be between zero and one") + self.activations = normalized + animations = [] + for neuron, value in zip(self.part("neurons"), normalized): + color = self.theme.accent if value >= 0.5 else self.theme.primary + animations.append(neuron.animate.set_color(color).set_opacity(0.25 + 0.75 * value)) + return AnimationGroup(*animations, lag_ratio=0.05) + + +__all__ = [ + "ArrayModel", + "GraphModel", + "NeuralLayer", + "StateMachine", + "TokenFlow", +] diff --git a/skills/manim-video/assets/domains/finance.py b/skills/manim-video/assets/domains/finance.py new file mode 100644 index 00000000..6f83b108 --- /dev/null +++ b/skills/manim-video/assets/domains/finance.py @@ -0,0 +1,309 @@ +"""Semantic components for finance, business, and resource explanations.""" + +from __future__ import annotations + +from collections.abc import Iterable, Mapping +from typing import Any + +import numpy as np +from manim import ( + Arrow, + Dot, + DOWN, + LEFT, + Line, + Polygon, + RIGHT, + Transform, + UP, + VGroup, +) + +try: + from ..teaching import SemanticMobject, VisualTheme +except ImportError: + from teaching import SemanticMobject, VisualTheme + +from ._common import ( + arrows_for_row, + ensure_amount, + ensure_index, + frame_safe, + label_for, + labeled_box, + normalized_labels, + title_above, +) + + +class CashFlow(SemanticMobject): + def __init__( + self, + theme: VisualTheme, + accounts: Mapping[object, float] | None = None, + *, + label: object = "", + ) -> None: + super().__init__(theme) + source = accounts if accounts is not None else {"revenue": 100, "costs": 40, "cash": 60} + self.names = [str(name) for name in source] + self.amounts = [float(amount) for amount in source.values()] + if not self.names: + self.names, self.amounts = [""], [0.0] + if any(amount < 0 for amount in self.amounts): + raise ValueError("cash balances cannot be negative") + accounts_group = self._accounts() + links = arrows_for_row(accounts_group, theme, color=theme.secondary) + caption = title_above(label, VGroup(accounts_group, links), theme) + self.register_part("accounts", accounts_group) + self.register_part("links", links) + self.register_part("label", caption) + for index, account in enumerate(accounts_group): + self.register_anchor(f"account_{index}", account) + frame_safe(self) + + def _accounts(self) -> VGroup: + return VGroup( + *[ + labeled_box( + f"{name}\n${amount:,.0f}", + self.theme, + width=1.7, + height=1.0, + color=self.theme.primary if index % 2 == 0 else self.theme.secondary, + font_size=18, + ) + for index, (name, amount) in enumerate(zip(self.names, self.amounts)) + ] + ).arrange(RIGHT, buff=0.65) + + def transfer(self, source: int, target: int, amount: float) -> Transform: + source_index = ensure_index(source, len(self.amounts), name="source account") + target_index = ensure_index(target, len(self.amounts), name="target account") + if source_index == target_index: + raise ValueError("cash source and target must differ") + value = ensure_amount(amount, available=self.amounts[source_index]) + self.amounts[source_index] -= value + self.amounts[target_index] += value + return Transform(self.part("accounts"), self._accounts()) + + +class CompoundTimeline(SemanticMobject): + def __init__( + self, + theme: VisualTheme, + *, + principal: float = 100.0, + rate: float = 0.10, + periods: int = 5, + label: object = "", + ) -> None: + super().__init__(theme) + if principal < 0 or rate <= -1 or periods < 0: + raise ValueError("principal, rate, and periods describe an invalid compound timeline") + self.principal = float(principal) + self.rate = float(rate) + self.periods = int(periods) + self.visible_periods = 0 + baseline = Line(LEFT * 4.5, RIGHT * 4.5, color=theme.muted) + ticks = VGroup( + *[ + Line(UP * 0.10, DOWN * 0.10, color=theme.muted).move_to( + baseline.point_from_proportion(index / max(self.periods, 1)) + ) + for index in range(self.periods + 1) + ] + ) + balances = self._balances(0, baseline) + caption = title_above(label, VGroup(baseline, balances), theme) + self.register_part("baseline", baseline) + self.register_part("ticks", ticks) + self.register_part("balances", balances) + self.register_part("label", caption) + self.register_anchor("start", lambda: baseline.get_start()) + self.register_anchor("end", lambda: baseline.get_end()) + frame_safe(self) + + def balance_at(self, period: int) -> float: + if not 0 <= int(period) <= self.periods: + raise IndexError(f"period {period} is outside 0..{self.periods}") + return self.principal * (1 + self.rate) ** int(period) + + def _balances(self, through: int, baseline: Line) -> VGroup: + values = VGroup() + maximum = max(self.balance_at(self.periods), self.principal, 1e-9) + for period in range(through + 1): + point = baseline.point_from_proportion(period / max(self.periods, 1)) + height = 0.25 + 1.8 * self.balance_at(period) / maximum + stem = Line(point, point + UP * height, color=self.theme.primary, stroke_width=5) + amount = label_for( + f"${self.balance_at(period):,.0f}", + self.theme, + size=16, + width=1.15, + ).next_to(stem, UP, buff=0.08) + values.add(VGroup(stem, amount)) + return values + + def compound(self, periods: int = 1) -> Transform: + if periods < 0: + raise ValueError("compound step cannot be negative") + self.visible_periods = min(self.periods, self.visible_periods + int(periods)) + replacement = self._balances(self.visible_periods, self.part("baseline")) + return Transform(self.part("balances"), replacement) + + +class Funnel(SemanticMobject): + def __init__( + self, + theme: VisualTheme, + stages: Mapping[object, float] | None = None, + *, + label: object = "", + ) -> None: + super().__init__(theme) + source = stages if stages is not None else {"visit": 1000, "trial": 300, "buy": 90} + self.names = [str(name) for name in source] + self.counts = [float(value) for value in source.values()] + if not self.names: + self.names, self.counts = [""], [0.0] + if any(value < 0 for value in self.counts): + raise ValueError("funnel counts cannot be negative") + levels = self._levels() + caption = title_above(label, levels, theme) + self.register_part("stages", levels) + self.register_part("label", caption) + for index, stage in enumerate(levels): + self.register_anchor(f"stage_{index}", stage) + frame_safe(self) + + def _levels(self) -> VGroup: + maximum = max(max(self.counts), 1e-9) + levels = VGroup() + for index, (name, count) in enumerate(zip(self.names, self.counts)): + width = 1.4 + 5.2 * count / maximum + shape = Polygon( + [-width / 2, 0.42, 0], + [width / 2, 0.42, 0], + [width * 0.43, -0.42, 0], + [-width * 0.43, -0.42, 0], + color=self.theme.primary if index % 2 == 0 else self.theme.secondary, + fill_color=self.theme.primary if index % 2 == 0 else self.theme.secondary, + fill_opacity=0.16 + index * 0.06, + ) + text = label_for(f"{name} {count:g}", self.theme, size=18, width=width - 0.2) + levels.add(VGroup(shape, text)) + levels.arrange(DOWN, buff=0.08) + return levels + + def convert(self, source: int, target: int, amount: float) -> Transform: + source_index = ensure_index(source, len(self.counts), name="source funnel stage") + target_index = ensure_index(target, len(self.counts), name="target funnel stage") + if source_index == target_index: + raise ValueError("funnel source and target must differ") + value = ensure_amount(amount, available=self.counts[source_index]) + self.counts[source_index] -= value + self.counts[target_index] += value + return Transform(self.part("stages"), self._levels()) + + +class FeedbackLoop(SemanticMobject): + def __init__( + self, + theme: VisualTheme, + factors: Iterable[object] | None = None, + *, + label: object = "", + ) -> None: + super().__init__(theme) + self.factors = normalized_labels(factors, fallback=["users", "data", "quality"]) + count = len(self.factors) + nodes = VGroup() + radius = min(2.4, 1.1 + count * 0.22) + for index, factor in enumerate(self.factors): + angle = np.pi / 2 - 2 * np.pi * index / max(count, 1) + node = labeled_box(factor, theme, width=1.35, height=0.66, color=theme.primary) + node.move_to([radius * np.cos(angle), radius * np.sin(angle), 0]) + nodes.add(node) + links = VGroup( + *[ + Arrow( + nodes[index].get_center(), + nodes[(index + 1) % count].get_center(), + buff=0.78, + color=theme.secondary, + stroke_width=2.4, + ) + for index in range(count) + if count > 1 + ] + ) + pulse = Dot(nodes[0].get_top() + UP * 0.18, color=theme.accent, radius=0.09) + caption = title_above(label, VGroup(nodes, links), theme) + self.step = 0 + self.register_part("nodes", nodes) + self.register_part("links", links) + self.register_part("pulse", pulse) + self.register_part("label", caption) + frame_safe(self) + + def reinforce(self) -> Any: + self.step = (self.step + 1) % len(self.factors) + return self.part("pulse").animate.move_to( + self.part("nodes")[self.step].get_top() + UP * 0.18 + ) + + +class ResourceFlow(SemanticMobject): + def __init__( + self, + theme: VisualTheme, + resources: Mapping[object, float] | None = None, + *, + label: object = "", + ) -> None: + super().__init__(theme) + source = resources if resources is not None else {"input": 100, "work": 20, "output": 80} + self.names = [str(name) for name in source] + self.amounts = [float(value) for value in source.values()] + if not self.names: + self.names, self.amounts = [""], [0.0] + if any(value < 0 for value in self.amounts): + raise ValueError("resource amounts cannot be negative") + stocks = self._stocks() + links = arrows_for_row(stocks, theme, color=theme.accent) + caption = title_above(label, VGroup(stocks, links), theme) + self.register_part("stocks", stocks) + self.register_part("links", links) + self.register_part("label", caption) + for index, stock in enumerate(stocks): + self.register_anchor(f"stock_{index}", stock) + frame_safe(self) + + def _stocks(self) -> VGroup: + return VGroup( + *[ + labeled_box( + f"{name}\n{amount:g}", + self.theme, + width=1.65, + height=1.0, + color=self.theme.secondary if index % 2 else self.theme.primary, + font_size=18, + ) + for index, (name, amount) in enumerate(zip(self.names, self.amounts)) + ] + ).arrange(RIGHT, buff=0.65) + + def transfer(self, source: int, target: int, amount: float) -> Transform: + source_index = ensure_index(source, len(self.amounts), name="source stock") + target_index = ensure_index(target, len(self.amounts), name="target stock") + if source_index == target_index: + raise ValueError("resource source and target must differ") + value = ensure_amount(amount, available=self.amounts[source_index]) + self.amounts[source_index] -= value + self.amounts[target_index] += value + return Transform(self.part("stocks"), self._stocks()) + + +__all__ = ["CashFlow", "CompoundTimeline", "FeedbackLoop", "Funnel", "ResourceFlow"] diff --git a/skills/manim-video/assets/domains/math.py b/skills/manim-video/assets/domains/math.py new file mode 100644 index 00000000..e8d4282c --- /dev/null +++ b/skills/manim-video/assets/domains/math.py @@ -0,0 +1,317 @@ +"""Semantic components for mathematical explanations.""" + +from __future__ import annotations + +from collections.abc import Callable, Iterable, Sequence +from typing import Any + +import numpy as np +from manim import ( + AnimationGroup, + Arrow, + Axes, + DashedLine, + Dot, + DOWN, + LEFT, + NumberLine, + Rectangle, + RIGHT, + Transform, + UP, + VGroup, +) + +try: + from ..teaching import SemanticMobject, VisualTheme +except ImportError: + from teaching import SemanticMobject, VisualTheme + +from ._common import ensure_amount, ensure_index, frame_safe, label_for, title_above + + +class NumberLineModel(SemanticMobject): + def __init__( + self, + theme: VisualTheme, + *, + x_range: Sequence[float] = (-5, 5, 1), + value: float = 0.0, + label: object = "", + length: float = 8.4, + ) -> None: + super().__init__(theme) + if len(x_range) not in {2, 3} or float(x_range[1]) <= float(x_range[0]): + raise ValueError("x_range must have increasing start and end values") + line = NumberLine(x_range=list(x_range), length=length, color=theme.muted) + self.minimum = float(x_range[0]) + self.maximum = float(x_range[1]) + self.value = float(value) + if not self.minimum <= self.value <= self.maximum: + raise ValueError("initial value is outside the number line") + marker = Dot(line.n2p(self.value), color=theme.accent, radius=0.10) + caption = title_above(label, line, theme) + self.register_part("line", line) + self.register_part("marker", marker) + self.register_part("label", caption) + self.register_anchor("minimum", lambda: line.n2p(self.minimum)) + self.register_anchor("maximum", lambda: line.n2p(self.maximum)) + self.register_anchor("value", marker) + frame_safe(self) + + def move_value(self, value: float) -> Any: + target = float(value) + if not self.minimum <= target <= self.maximum: + raise ValueError("target value is outside the number line") + self.value = target + return self.part("marker").animate.move_to(self.part("line").n2p(target)) + + +class VectorMap(SemanticMobject): + def __init__( + self, + theme: VisualTheme, + vectors: Iterable[Sequence[float]] | None = None, + *, + label: object = "", + ) -> None: + super().__init__(theme) + raw_vectors = list(vectors) if vectors is not None else [(1.5, 0.5), (-0.5, 1.4)] + self.vectors = [np.asarray((*vector[:2], 0.0), dtype=float) for vector in raw_vectors] + axes = Axes( + x_range=[-3, 3, 1], + y_range=[-2.3, 2.3, 1], + x_length=7, + y_length=4.6, + axis_config={"color": theme.muted, "stroke_opacity": 0.5}, + tips=False, + ) + arrows = VGroup( + *[ + Arrow( + axes.c2p(0, 0), + axes.c2p(vector[0], vector[1]), + buff=0, + color=theme.primary if index % 2 == 0 else theme.secondary, + stroke_width=3, + ) + for index, vector in enumerate(self.vectors) + ] + ) + caption = title_above(label, axes, theme) + self.register_part("axes", axes) + self.register_part("vectors", arrows) + self.register_part("label", caption) + self.register_anchor("origin", lambda: axes.c2p(0, 0)) + frame_safe(self) + + def apply_matrix(self, matrix: Sequence[Sequence[float]]) -> AnimationGroup: + transform = np.asarray(matrix, dtype=float) + if transform.shape != (2, 2): + raise ValueError("vector map transformations require a 2 by 2 matrix") + axes = self.part("axes") + new_vectors = [transform @ vector[:2] for vector in self.vectors] + animations = [] + for index, (arrow, vector) in enumerate(zip(self.part("vectors"), new_vectors)): + target = Arrow( + axes.c2p(0, 0), + axes.c2p(vector[0], vector[1]), + buff=0, + color=theme_color(self.theme, index), + stroke_width=3, + ) + animations.append(Transform(arrow, target)) + self.vectors = [np.array([vector[0], vector[1], 0.0]) for vector in new_vectors] + return AnimationGroup(*animations, lag_ratio=0) + + +def theme_color(theme: VisualTheme, index: int) -> str: + return theme.primary if index % 2 == 0 else theme.secondary + + +class MatrixMap(SemanticMobject): + def __init__( + self, + theme: VisualTheme, + matrix: Sequence[Sequence[float]] = ((1, 0), (0, 1)), + *, + label: object = "", + ) -> None: + super().__init__(theme) + values = np.asarray(matrix, dtype=float) + if values.shape != (2, 2): + raise ValueError("MatrixMap currently visualizes 2 by 2 transformations") + self.matrix = values + cells = VGroup( + *[ + VGroup( + Rectangle( + width=0.82, + height=0.68, + color=theme.primary, + fill_color=theme.primary, + fill_opacity=0.10, + ), + label_for(f"{value:g}", theme, size=20, width=0.64), + ) + for value in values.flat + ] + ).arrange_in_grid(rows=2, cols=2, buff=0.08) + axes = Axes( + x_range=[-2.5, 2.5, 1], + y_range=[-2, 2, 1], + x_length=4.5, + y_length=3.6, + tips=False, + axis_config={"color": theme.muted, "stroke_opacity": 0.45}, + ) + cells.next_to(axes, LEFT, buff=0.65) + input_arrow = Arrow(axes.c2p(0, 0), axes.c2p(1, 0.75), buff=0, color=theme.secondary) + output_arrow = Arrow(axes.c2p(0, 0), axes.c2p(1, 0.75), buff=0, color=theme.accent) + caption = title_above(label, VGroup(cells, axes), theme) + self.register_part("matrix", cells) + self.register_part("axes", axes) + self.register_part("input", input_arrow) + self.register_part("output", output_arrow) + self.register_part("label", caption) + self.register_anchor("input", input_arrow) + self.register_anchor("output", output_arrow) + frame_safe(self) + + def apply_to(self, vector: Sequence[float]) -> AnimationGroup: + raw = np.asarray(vector, dtype=float) + if raw.shape != (2,): + raise ValueError("matrix input must contain exactly two values") + result = self.matrix @ raw + axes = self.part("axes") + input_target = Arrow(axes.c2p(0, 0), axes.c2p(*raw), buff=0, color=self.theme.secondary) + output_target = Arrow(axes.c2p(0, 0), axes.c2p(*result), buff=0, color=self.theme.accent) + self.input_vector = raw + self.output_vector = result + return AnimationGroup( + Transform(self.part("input"), input_target), + Transform(self.part("output"), output_target), + lag_ratio=0.15, + ) + + +class LinkedPlot(SemanticMobject): + def __init__( + self, + theme: VisualTheme, + function: Callable[[float], float] = lambda x: x, + *, + x_range: Sequence[float] = (-3, 3), + y_range: Sequence[float] = (-2, 2), + x_value: float = 0.0, + label: object = "", + ) -> None: + super().__init__(theme) + self.function = function + self.x_range = (float(x_range[0]), float(x_range[1])) + if self.x_range[1] <= self.x_range[0]: + raise ValueError("plot x range must increase") + axes = Axes( + x_range=[*self.x_range, 1], + y_range=[float(y_range[0]), float(y_range[1]), 1], + x_length=7, + y_length=4, + tips=False, + axis_config={"color": theme.muted, "stroke_opacity": 0.5}, + ) + graph = axes.plot(function, x_range=self.x_range, color=theme.primary) + self.x_value = float(x_value) + point = Dot(axes.c2p(self.x_value, function(self.x_value)), color=theme.accent) + guide = DashedLine( + axes.c2p(self.x_value, 0), + point.get_center(), + color=theme.secondary, + ) + caption = title_above(label, axes, theme) + self.register_part("axes", axes) + self.register_part("graph", graph) + self.register_part("point", point) + self.register_part("guide", guide) + self.register_part("label", caption) + self.register_anchor("point", point) + frame_safe(self) + + def set_x(self, x_value: float) -> AnimationGroup: + value = float(x_value) + if not self.x_range[0] <= value <= self.x_range[1]: + raise ValueError("x value is outside the plot range") + axes = self.part("axes") + point = Dot(axes.c2p(value, self.function(value)), color=self.theme.accent) + guide = DashedLine(axes.c2p(value, 0), point.get_center(), color=self.theme.secondary) + self.x_value = value + return AnimationGroup( + Transform(self.part("point"), point), + Transform(self.part("guide"), guide), + lag_ratio=0, + ) + + +class ProbabilityMass(SemanticMobject): + def __init__( + self, + theme: VisualTheme, + probabilities: Sequence[float] = (0.25, 0.5, 0.25), + *, + labels: Iterable[object] | None = None, + label: object = "", + ) -> None: + super().__init__(theme) + self.probabilities = [float(value) for value in probabilities] + if not self.probabilities or any(value < 0 for value in self.probabilities): + raise ValueError("probabilities must be a non-empty list of non-negative values") + total = sum(self.probabilities) + if not np.isclose(total, 1.0, atol=1e-8): + raise ValueError(f"probabilities must sum to one, got {total:g}") + self.labels = [str(value) for value in labels] if labels is not None else [str(i) for i in range(len(self.probabilities))] + if len(self.labels) != len(self.probabilities): + raise ValueError("probability labels must match the number of values") + bars = self._bars() + caption = title_above(label, bars, theme) + self.register_part("bars", bars) + self.register_part("label", caption) + for index, bar in enumerate(bars): + self.register_anchor(f"mass_{index}", bar) + frame_safe(self) + + def _bars(self) -> VGroup: + bars = VGroup() + for index, (name, value) in enumerate(zip(self.labels, self.probabilities)): + height = max(0.02, value * 3.4) + bar = Rectangle( + width=0.86, + height=height, + color=theme_color(self.theme, index), + fill_color=theme_color(self.theme, index), + fill_opacity=0.55, + ) + value_label = label_for(f"{value:.0%}", self.theme, size=19, width=0.8) + name_label = label_for(name, self.theme, size=18, width=0.86) + value_label.next_to(bar, UP, buff=0.1) + name_label.next_to(bar, DOWN, buff=0.12) + bars.add(VGroup(bar, value_label, name_label)) + bars.arrange(RIGHT, buff=0.32, aligned_edge=DOWN) + return bars + + def transfer_probability(self, source: int, target: int, amount: float) -> Transform: + source_index = ensure_index(source, len(self.probabilities), name="source") + target_index = ensure_index(target, len(self.probabilities), name="target") + if source_index == target_index: + raise ValueError("probability source and target must differ") + value = ensure_amount(amount, available=self.probabilities[source_index]) + self.probabilities[source_index] -= value + self.probabilities[target_index] += value + return Transform(self.part("bars"), self._bars()) + + +__all__ = [ + "LinkedPlot", + "MatrixMap", + "NumberLineModel", + "ProbabilityMass", + "VectorMap", +] diff --git a/skills/manim-video/assets/domains/physics.py b/skills/manim-video/assets/domains/physics.py new file mode 100644 index 00000000..bf63d9b0 --- /dev/null +++ b/skills/manim-video/assets/domains/physics.py @@ -0,0 +1,250 @@ +"""Semantic components for mechanics, waves, and circuits.""" + +from __future__ import annotations + +from collections.abc import Iterable, Sequence +from typing import Any + +import numpy as np +from manim import ( + AnimationGroup, + Arrow, + Axes, + Circle, + Dot, + Line, + ORIGIN, + Rectangle, + RIGHT, + Transform, + UP, + VGroup, +) + +try: + from ..teaching import SemanticMobject, VisualTheme +except ImportError: + from teaching import SemanticMobject, VisualTheme + +from ._common import ensure_index, frame_safe, label_for, normalized_labels, title_above + + +class BodySystem(SemanticMobject): + def __init__( + self, + theme: VisualTheme, + bodies: Iterable[object] | None = None, + *, + positions: Sequence[Sequence[float]] | None = None, + label: object = "", + ) -> None: + super().__init__(theme) + self.body_names = normalized_labels(bodies, fallback=["m1", "m2"]) + if positions is None: + positions = [ + ((index - (len(self.body_names) - 1) / 2) * 2.2, 0, 0) + for index in range(len(self.body_names)) + ] + if len(positions) != len(self.body_names): + raise ValueError("body positions must match the body count") + body_group = VGroup() + for index, (name, position) in enumerate(zip(self.body_names, positions)): + body = Circle( + radius=0.48, + color=theme.primary if index % 2 == 0 else theme.secondary, + fill_color=theme.primary if index % 2 == 0 else theme.secondary, + fill_opacity=0.18, + ).move_to(position) + body_group.add(VGroup(body, label_for(name, theme, size=20, width=0.7).move_to(body))) + connections = VGroup( + *[ + Line(body_group[index].get_center(), body_group[index + 1].get_center(), color=theme.muted) + for index in range(max(0, len(body_group) - 1)) + ] + ) + force_group = VGroup() + caption = title_above(label, VGroup(connections, body_group), theme) + self.register_part("connections", connections) + self.register_part("bodies", body_group) + self.register_part("forces", force_group) + self.register_part("label", caption) + for index, body in enumerate(body_group): + self.register_anchor(f"body_{index}", body) + frame_safe(self) + + def move_body(self, index: int, position: Sequence[float]) -> Any: + selected = ensure_index(index, len(self.body_names), name="body") + return self.part("bodies")[selected].animate.move_to(position) + + def apply_force(self, index: int, vector: Sequence[float]) -> Arrow: + selected = ensure_index(index, len(self.body_names), name="body") + raw = np.asarray(vector, dtype=float) + if raw.shape == (2,): + raw = np.append(raw, 0.0) + if raw.shape != (3,) or np.linalg.norm(raw) == 0: + raise ValueError("force vector must be a non-zero 2D or 3D vector") + body = self.part("bodies")[selected] + arrow = Arrow( + body.get_center(), + body.get_center() + raw, + buff=0.08, + color=self.theme.warning, + stroke_width=3, + ) + self.part("forces").add(arrow) + return arrow + + +class ForceVector(SemanticMobject): + def __init__( + self, + theme: VisualTheme, + vector: Sequence[float] = (2, 1), + *, + label: object = "F", + ) -> None: + super().__init__(theme) + self.vector = self._vector(vector) + origin = Dot(ORIGIN, color=theme.text, radius=0.07) + arrow = Arrow(ORIGIN, self.vector, buff=0, color=theme.warning, stroke_width=4) + caption = label_for(label, theme, size=24, color=theme.warning, width=1.4) + caption.next_to(arrow.get_end(), UP, buff=0.12) + self.register_part("origin", origin) + self.register_part("vector", arrow) + self.register_part("label", caption) + self.register_anchor("tail", origin) + self.register_anchor("tip", lambda: arrow.get_end()) + frame_safe(self) + + @staticmethod + def _vector(vector: Sequence[float]) -> np.ndarray: + raw = np.asarray(vector, dtype=float) + if raw.shape == (2,): + raw = np.append(raw, 0.0) + if raw.shape != (3,) or np.linalg.norm(raw) == 0: + raise ValueError("force vector must be a non-zero 2D or 3D vector") + return raw + + def set_vector(self, vector: Sequence[float]) -> Transform: + self.vector = self._vector(vector) + return Transform( + self.part("vector"), + Arrow(ORIGIN, self.vector, buff=0, color=self.theme.warning, stroke_width=4), + ) + + +class WaveField(SemanticMobject): + def __init__( + self, + theme: VisualTheme, + *, + amplitude: float = 1.0, + wavelength: float = 2.5, + phase: float = 0.0, + label: object = "", + ) -> None: + super().__init__(theme) + if amplitude <= 0 or wavelength <= 0: + raise ValueError("wave amplitude and wavelength must be positive") + self.amplitude = float(amplitude) + self.wavelength = float(wavelength) + self.phase = float(phase) + axes = Axes( + x_range=[-5, 5, 1], + y_range=[-2, 2, 1], + x_length=9, + y_length=4, + tips=False, + axis_config={"color": theme.muted, "stroke_opacity": 0.35}, + ) + wave = self._wave(axes) + source = Dot(axes.c2p(-5, self._value(-5)), color=theme.accent, radius=0.09) + caption = title_above(label, axes, theme) + self.register_part("axes", axes) + self.register_part("wave", wave) + self.register_part("source", source) + self.register_part("label", caption) + self.register_anchor("source", source) + self.register_anchor("crest", lambda: axes.c2p(self.wavelength / 4 + self.phase, self.amplitude)) + frame_safe(self) + + def _value(self, x: float) -> float: + return self.amplitude * np.sin(2 * np.pi * (x - self.phase) / self.wavelength) + + def _wave(self, axes: Axes) -> Any: + return axes.plot(lambda x: self._value(x), x_range=(-5, 5), color=self.theme.primary) + + def propagate(self, distance: float) -> AnimationGroup: + self.phase += float(distance) + axes = self.part("axes") + new_wave = self._wave(axes) + new_source = Dot(axes.c2p(-5, self._value(-5)), color=self.theme.accent, radius=0.09) + return AnimationGroup( + Transform(self.part("wave"), new_wave), + Transform(self.part("source"), new_source), + lag_ratio=0, + ) + + +class CircuitFlow(SemanticMobject): + def __init__( + self, + theme: VisualTheme, + components: Iterable[object] | None = None, + *, + charge_count: int = 6, + label: object = "", + ) -> None: + super().__init__(theme) + self.components = normalized_labels(components, fallback=["source", "load"]) + if charge_count < 1: + raise ValueError("circuit needs at least one visible charge") + left_x, right_x, top_y, bottom_y = -3.2, 3.2, 1.5, -1.5 + wire = VGroup( + Line([left_x, top_y, 0], [right_x, top_y, 0], color=theme.muted), + Line([right_x, top_y, 0], [right_x, bottom_y, 0], color=theme.muted), + Line([right_x, bottom_y, 0], [left_x, bottom_y, 0], color=theme.muted), + Line([left_x, bottom_y, 0], [left_x, top_y, 0], color=theme.muted), + ) + component_group = VGroup() + positions = [[left_x, 0, 0], [right_x, 0, 0], [0, top_y, 0], [0, bottom_y, 0]] + for index, name in enumerate(self.components): + box = Rectangle( + width=1.25, + height=0.58, + color=theme.primary if index % 2 == 0 else theme.secondary, + fill_color=theme.background, + fill_opacity=1, + ).move_to(positions[index % len(positions)]) + component_group.add(VGroup(box, label_for(name, theme, size=18, width=1.05).move_to(box))) + path_points = [ + [-3.2 + 6.4 * index / charge_count, top_y, 0] + for index in range(charge_count) + ] + charges = VGroup(*[Dot(point, color=theme.accent, radius=0.07) for point in path_points]) + caption = title_above(label, VGroup(wire, component_group), theme) + self.current = 0.0 + self.register_part("wire", wire) + self.register_part("components", component_group) + self.register_part("charges", charges) + self.register_part("label", caption) + frame_safe(self) + + def set_current(self, current: float) -> AnimationGroup: + self.current = float(current) + strength = min(1.0, abs(self.current)) + color = self.theme.accent if self.current >= 0 else self.theme.warning + return AnimationGroup( + *(charge.animate.set_color(color).set_opacity(0.25 + 0.75 * strength) for charge in self.part("charges")), + lag_ratio=0.03, + ) + + def flow(self, distance: float = 0.45) -> AnimationGroup: + direction = 1 if self.current >= 0 else -1 + return AnimationGroup( + *(charge.animate.shift(RIGHT * distance * direction) for charge in self.part("charges")), + lag_ratio=0.04, + ) + + +__all__ = ["BodySystem", "CircuitFlow", "ForceVector", "WaveField"] diff --git a/skills/manim-video/assets/domains/systems.py b/skills/manim-video/assets/domains/systems.py new file mode 100644 index 00000000..d3872332 --- /dev/null +++ b/skills/manim-video/assets/domains/systems.py @@ -0,0 +1,252 @@ +"""Semantic components for software systems and data architecture.""" + +from __future__ import annotations + +from collections.abc import Iterable, Sequence +from typing import Any + +from manim import ( + AnimationGroup, + Arrow, + Circle, + Dot, + DOWN, + FadeIn, + MoveAlongPath, + RIGHT, + Transform, + UP, + VGroup, +) + +try: + from ..teaching import SemanticMobject, VisualTheme +except ImportError: + from teaching import SemanticMobject, VisualTheme + +from ._common import ( + arrows_for_row, + ensure_index, + frame_safe, + label_for, + labeled_box, + normalized_labels, + row_of_boxes, + title_above, +) + + +class RequestFlow(SemanticMobject): + def __init__( + self, + theme: VisualTheme, + stages: Iterable[object] | None = None, + *, + label: object = "", + ) -> None: + super().__init__(theme) + self.stages = normalized_labels(stages, fallback=["client", "service", "database"]) + nodes = row_of_boxes( + self.stages, + theme, + fallback=[""], + color=theme.primary, + box_width=1.65, + ) + links = arrows_for_row(nodes, theme) + request = Dot(nodes[0].get_top() + UP * 0.30, color=theme.accent, radius=0.10) + caption = title_above(label, VGroup(nodes, links), theme) + self.request_stage = 0 + self.register_part("stages", nodes) + self.register_part("links", links) + self.register_part("request", request) + self.register_part("label", caption) + for index, stage in enumerate(nodes): + self.register_anchor(f"stage_{index}", stage) + frame_safe(self) + + def advance_request(self, destination: int | None = None) -> Any: + target = self.request_stage + 1 if destination is None else int(destination) + target = ensure_index(target, len(self.stages), name="request destination") + self.request_stage = target + return self.part("request").animate.move_to( + self.part("stages")[target].get_top() + UP * 0.30 + ) + + def reset_request(self) -> Any: + self.request_stage = 0 + return self.part("request").animate.move_to( + self.part("stages")[0].get_top() + UP * 0.30 + ) + + +class ServiceGraph(SemanticMobject): + def __init__( + self, + theme: VisualTheme, + services: Iterable[object] | None = None, + connections: Iterable[Sequence[object]] | None = None, + *, + label: object = "", + ) -> None: + super().__init__(theme) + self.services = normalized_labels(services, fallback=["api", "cache", "db"]) + self.service_index = {name: index for index, name in enumerate(self.services)} + if len(self.service_index) != len(self.services): + raise ValueError("service names must be unique") + nodes = VGroup( + *[labeled_box(service, theme, width=1.5, color=theme.primary) for service in self.services] + ) + if len(nodes) == 1: + nodes[0].move_to([0, 0, 0]) + else: + import numpy as np + + for index, node in enumerate(nodes): + angle = np.pi / 2 - 2 * np.pi * index / len(nodes) + node.move_to([2.2 * np.cos(angle), 1.55 * np.sin(angle), 0]) + default_connections = list(zip(self.services, self.services[1:])) + self.connections = [ + (str(start), str(end)) + for start, end in (connections if connections is not None else default_connections) + ] + links = VGroup() + for start, end in self.connections: + if start not in self.service_index or end not in self.service_index: + raise ValueError(f"connection {start!r}->{end!r} names an unknown service") + links.add( + Arrow( + nodes[self.service_index[start]].get_center(), + nodes[self.service_index[end]].get_center(), + buff=0.82, + color=theme.muted, + stroke_width=2, + ) + ) + requests = VGroup() + caption = title_above(label, VGroup(nodes, links), theme) + self.register_part("links", links) + self.register_part("services", nodes) + self.register_part("requests", requests) + self.register_part("label", caption) + for name, service in zip(self.services, nodes): + self.register_anchor(f"service_{name}", service) + frame_safe(self) + + def send_request(self, source: object, target: object) -> AnimationGroup: + connection = (str(source), str(target)) + try: + link_index = self.connections.index(connection) + except ValueError as exc: + raise KeyError(f"unknown service connection {connection[0]!r}->{connection[1]!r}") from exc + link = self.part("links")[link_index] + request = Dot(link.get_start(), color=self.theme.accent, radius=0.09) + self.part("requests").add(request) + return AnimationGroup( + FadeIn(request, run_time=0.12), + MoveAlongPath(request, link), + lag_ratio=0.15, + ) + + +class QueueModel(SemanticMobject): + def __init__( + self, + theme: VisualTheme, + items: Iterable[object] | None = None, + *, + capacity: int = 6, + label: object = "", + ) -> None: + super().__init__(theme) + if capacity < 1: + raise ValueError("queue capacity must be positive") + self.capacity = int(capacity) + self.items = [str(value) for value in items] if items is not None else ["r1", "r2", "r3"] + if len(self.items) > self.capacity: + raise ValueError("initial queue exceeds its capacity") + slots = self._slots() + front = label_for("front", theme, size=17, color=theme.muted, width=0.8) + back = label_for("back", theme, size=17, color=theme.muted, width=0.8) + front.next_to(slots, DOWN, buff=0.16).align_to(slots, direction=[-1, 0, 0]) + back.next_to(slots, DOWN, buff=0.16).align_to(slots, direction=[1, 0, 0]) + markers = VGroup(front, back) + caption = title_above(label, slots, theme) + self.register_part("slots", slots) + self.register_part("markers", markers) + self.register_part("label", caption) + self.register_anchor("front", lambda: slots[0].get_center()) + self.register_anchor("back", lambda: slots[-1].get_center()) + frame_safe(self) + + def _slots(self) -> VGroup: + values = [*self.items, *([""] * (self.capacity - len(self.items)))] + return VGroup( + *[ + labeled_box( + value, + self.theme, + width=0.95, + height=0.72, + color=self.theme.secondary if index < len(self.items) else self.theme.muted, + fill_opacity=0.18 if index < len(self.items) else 0.03, + ) + for index, value in enumerate(values) + ] + ).arrange(RIGHT, buff=0.08) + + def enqueue(self, request: object) -> Transform: + if len(self.items) >= self.capacity: + raise OverflowError("cannot enqueue into a full queue") + self.items.append(str(request)) + return Transform(self.part("slots"), self._slots()) + + def dequeue(self) -> tuple[str, Transform]: + if not self.items: + raise IndexError("cannot dequeue from an empty queue") + request = self.items.pop(0) + return request, Transform(self.part("slots"), self._slots()) + + +class DataPipeline(SemanticMobject): + def __init__( + self, + theme: VisualTheme, + stages: Iterable[object] | None = None, + *, + label: object = "", + ) -> None: + super().__init__(theme) + self.stages = normalized_labels(stages, fallback=["ingest", "clean", "model", "serve"]) + nodes = row_of_boxes( + self.stages, + theme, + fallback=[""], + color=theme.secondary, + box_width=1.45, + ) + links = arrows_for_row(nodes, theme, color=theme.primary) + packet = Circle( + radius=0.10, + color=theme.accent, + fill_color=theme.accent, + fill_opacity=1, + ).move_to(nodes[0].get_top() + UP * 0.28) + caption = title_above(label, VGroup(nodes, links), theme) + self.packet_stage = 0 + self.register_part("stages", nodes) + self.register_part("links", links) + self.register_part("packet", packet) + self.register_part("label", caption) + frame_safe(self) + + def propagate(self, destination: int | None = None) -> Any: + target = self.packet_stage + 1 if destination is None else int(destination) + target = ensure_index(target, len(self.stages), name="pipeline destination") + self.packet_stage = target + return self.part("packet").animate.move_to( + self.part("stages")[target].get_top() + UP * 0.28 + ) + + +__all__ = ["DataPipeline", "QueueModel", "RequestFlow", "ServiceGraph"] diff --git a/skills/manim-video/assets/teaching.py b/skills/manim-video/assets/teaching.py new file mode 100644 index 00000000..1dca6377 --- /dev/null +++ b/skills/manim-video/assets/teaching.py @@ -0,0 +1,658 @@ +"""Style-neutral semantic authoring primitives for Manim Community explainers. + +The classes in this module manage meaning, continuity, and attention. They do +not replace ordinary Manim mobjects or animations; authors can drop to raw +Manim whenever a custom visual argument needs it. +""" + +from __future__ import annotations + +from copy import deepcopy +from dataclasses import dataclass +from typing import Any, Callable, Iterable, Sequence + +import numpy as np +from manim import ( + AnimationGroup, + DefaultSectionType, + Group, + Mobject, + MovingCameraScene, + Text, + ThreeDScene, + Transform, + VGroup, + ValueTracker, + config, +) + + +@dataclass(frozen=True, slots=True) +class VisualTheme: + """One explicit set of semantic color and typography roles. + + Components require a theme instance so color meaning remains stable across + a whole explanation. The defaults are deliberately neutral rather than a + recreation of any existing studio palette. + """ + + background: str = "#111318" + text: str = "#F4F5F7" + muted: str = "#9298A3" + primary: str = "#5B8DEF" + secondary: str = "#5FC49C" + accent: str = "#F2C14E" + warning: str = "#E86A6A" + title_font: str = "" + body_font: str = "" + label_font: str = "" + + def font_for(self, role: str) -> str: + """Resolve a semantic font role and reject accidental one-off roles.""" + fonts = { + "title": self.title_font, + "body": self.body_font, + "label": self.label_font, + } + if role not in fonts: + raise KeyError(f"unknown font role {role!r}; choose title, body, or label") + return fonts[role] + + +def theme_text( + value: object, + theme: VisualTheme, + *, + role: str = "label", + font_size: float = 24, + color: str | None = None, + max_width: float | None = None, +) -> Text: + """Create resilient themed text, including a harmless empty-label object.""" + text = str(value) + rendered = Text( + text if text else " ", + font=theme.font_for(role), + font_size=font_size, + color=color or theme.text, + ) + if not text: + rendered.set_opacity(0.0) + if max_width is not None and rendered.width > max_width: + rendered.scale_to_fit_width(max_width) + return rendered + + +def fit_to_frame(mobject: Mobject, *, margin: float = 0.45) -> Mobject: + """Scale a component down only when it would exceed the active frame.""" + if margin < 0: + raise ValueError("frame margin cannot be negative") + maximum_width = float(config.frame_width) - 2 * margin + maximum_height = float(config.frame_height) - 2 * margin + if maximum_width <= 0 or maximum_height <= 0: + raise ValueError("frame margin leaves no drawable area") + scale = min( + 1.0, + maximum_width / max(float(mobject.width), 1e-9), + maximum_height / max(float(mobject.height), 1e-9), + ) + if scale < 1.0: + mobject.scale(scale) + return mobject + + +def _family_with_style(mobject: Mobject) -> list[Mobject]: + return [ + member + for member in mobject.get_family() + if hasattr(member, "get_style") and hasattr(member, "set_style") + ] + + +def _style_snapshot(mobjects: Iterable[Mobject]) -> list[tuple[Mobject, dict[str, Any]]]: + seen: set[int] = set() + snapshot: list[tuple[Mobject, dict[str, Any]]] = [] + for mobject in mobjects: + for member in _family_with_style(mobject): + if id(member) in seen: + continue + seen.add(id(member)) + snapshot.append((member, deepcopy(member.get_style()))) + return snapshot + + +def _restore_snapshot(snapshot: Iterable[tuple[Mobject, dict[str, Any]]]) -> None: + for member, style in snapshot: + member.set_style(**deepcopy(style)) + + +def _path_to(root: Mobject, target: Mobject) -> tuple[int, ...] | None: + if root is target: + return () + for index, child in enumerate(root.submobjects): + child_path = _path_to(child, target) + if child_path is not None: + return (index, *child_path) + return None + + +def _at_path(root: Mobject, path: Sequence[int]) -> Mobject: + current = root + for index in path: + current = current.submobjects[index] + return current + + +class SemanticMobject(VGroup): + """A ``VGroup`` whose authored parts and anchors have stable semantic names.""" + + def __init__(self, theme: VisualTheme, *mobjects: Mobject, **kwargs: Any) -> None: + if not isinstance(theme, VisualTheme): + raise TypeError("SemanticMobject requires a shared VisualTheme") + super().__init__(*mobjects, **kwargs) + self.theme = theme + self._semantic_parts: dict[str, Mobject] = {} + self._semantic_anchors: dict[ + str, Mobject | Sequence[float] | Callable[[], Sequence[float]] + ] = {} + self._opacity_snapshot: list[tuple[Mobject, dict[str, Any]]] | None = None + + def register_part(self, name: str, mobject: Mobject, *, add: bool = True) -> Mobject: + if not name: + raise ValueError("part name cannot be empty") + if name in self._semantic_parts: + raise ValueError(f"part {name!r} is already registered") + if not isinstance(mobject, Mobject): + raise TypeError("registered parts must be Manim mobjects") + self._semantic_parts[name] = mobject + if add and not any(child is mobject for child in self.submobjects): + self.add(mobject) + return mobject + + def part(self, name: str) -> Mobject: + try: + return self._semantic_parts[name] + except KeyError as exc: + known = ", ".join(sorted(self._semantic_parts)) or "none" + raise KeyError(f"unknown part {name!r}; registered parts: {known}") from exc + + @property + def part_names(self) -> tuple[str, ...]: + return tuple(self._semantic_parts) + + def register_anchor( + self, + name: str, + anchor: Mobject | Sequence[float] | Callable[[], Sequence[float]], + ) -> None: + if not name: + raise ValueError("anchor name cannot be empty") + if name in self._semantic_anchors: + raise ValueError(f"anchor {name!r} is already registered") + if not isinstance(anchor, Mobject) and not callable(anchor): + point = np.asarray(anchor, dtype=float) + if point.shape not in {(2,), (3,)}: + raise ValueError("anchor coordinates must contain two or three values") + self._semantic_anchors[name] = anchor + + def anchor(self, name: str) -> np.ndarray: + try: + anchor = self._semantic_anchors[name] + except KeyError as exc: + known = ", ".join(sorted(self._semantic_anchors)) or "none" + raise KeyError(f"unknown anchor {name!r}; registered anchors: {known}") from exc + if isinstance(anchor, Mobject): + point = np.asarray(anchor.get_center(), dtype=float) + else: + point = np.asarray(anchor() if callable(anchor) else anchor, dtype=float) + if point.shape == (2,): + point = np.append(point, 0.0) + if point.shape != (3,): + raise ValueError(f"anchor {name!r} did not resolve to a 2D or 3D point") + return point.copy() + + @property + def anchor_names(self) -> tuple[str, ...]: + return tuple(self._semantic_anchors) + + def highlight( + self, + *part_names: str, + dim_opacity: float = 0.18, + ) -> "SemanticMobject": + """Dim every named part except the requested semantic focus.""" + if not 0 <= dim_opacity <= 1: + raise ValueError("dim opacity must be between zero and one") + selected = {self.part(name) for name in part_names} + if not selected: + raise ValueError("highlight requires at least one registered part") + if self._opacity_snapshot is None: + self._opacity_snapshot = _style_snapshot(self._semantic_parts.values()) + for part in self._semantic_parts.values(): + if part not in selected: + part.set_opacity(dim_opacity) + return self + + def restore_opacity(self) -> "SemanticMobject": + if self._opacity_snapshot is not None: + _restore_snapshot(self._opacity_snapshot) + self._opacity_snapshot = None + return self + + def _adopt_semantics_from(self, target: "SemanticMobject") -> None: + """Rebind semantic parts after this object has become ``target``.""" + part_paths = { + name: _path_to(target, part) + for name, part in target._semantic_parts.items() + } + self._semantic_parts = { + name: _at_path(self, path) + for name, path in part_paths.items() + if path is not None + } + anchors: dict[str, Mobject | Sequence[float] | Callable[[], Sequence[float]]] = {} + for name, anchor in target._semantic_anchors.items(): + if isinstance(anchor, Mobject): + path = _path_to(target, anchor) + anchors[name] = _at_path(self, path) if path is not None else anchor.get_center() + elif callable(anchor): + anchors[name] = np.asarray(anchor(), dtype=float) + else: + anchors[name] = np.asarray(anchor, dtype=float) + self._semantic_anchors = anchors + + +class LinkedValue: + """One ``ValueTracker`` driving any number of named representations.""" + + def __init__(self, value: float = 0.0) -> None: + self.tracker = ValueTracker(float(value)) + self._bindings: dict[ + str, tuple[Mobject, Callable[[Mobject, float], Mobject | None], Callable[[Mobject], None]] + ] = {} + self._suspended = False + + @property + def value(self) -> float: + return float(self.tracker.get_value()) + + def register( + self, + name: str | Mobject, + mobject: Mobject | Callable[[Mobject, float], Mobject | None], + update: Callable[[Mobject, float], Mobject | None] | None = None, + ) -> Mobject: + """Register a dependent by name, or pass ``(mobject, update)`` for an auto-name.""" + if isinstance(name, Mobject): + dependent = name + callback = mobject + binding_name = f"dependent_{len(self._bindings)}" + else: + binding_name = name + dependent = mobject + callback = update + if not binding_name: + raise ValueError("linked value binding name cannot be empty") + if binding_name in self._bindings: + raise ValueError(f"linked value binding {binding_name!r} already exists") + if not isinstance(dependent, Mobject) or not callable(callback): + raise TypeError("register requires a mobject and an update callback") + + def updater(current: Mobject, _dt: float = 0.0) -> None: + if self._suspended: + return + replacement = callback(current, self.value) + if isinstance(replacement, Mobject) and replacement is not current: + current.become(replacement) + + dependent.add_updater(updater) + self._bindings[binding_name] = (dependent, callback, updater) + updater(dependent) + return dependent + + def dependent(self, name: str) -> Mobject: + try: + return self._bindings[name][0] + except KeyError as exc: + raise KeyError(f"unknown linked value binding {name!r}") from exc + + def set_value(self, value: float) -> "LinkedValue": + self.tracker.set_value(float(value)) + self.update() + return self + + def update(self) -> "LinkedValue": + if not self._suspended: + for dependent, _callback, updater in self._bindings.values(): + updater(dependent) + return self + + def suspend(self) -> "LinkedValue": + self._suspended = True + return self + + def resume(self) -> "LinkedValue": + self._suspended = False + return self.update() + + def clear(self, name: str | None = None) -> "LinkedValue": + names = list(self._bindings) if name is None else [name] + for binding_name in names: + try: + dependent, _callback, updater = self._bindings.pop(binding_name) + except KeyError as exc: + raise KeyError(f"unknown linked value binding {binding_name!r}") from exc + dependent.remove_updater(updater) + return self + + def animate_to(self, value: float, *, run_time: float | None = None) -> Any: + builder = self.tracker.animate + if run_time is not None: + if run_time < 0: + raise ValueError("animation duration cannot be negative") + builder = builder(run_time=run_time) + return builder.set_value(float(value)) + + +class _TeachingSceneMixin: + max_static_hold_s = 3.0 + + def _ensure_teaching_state(self) -> None: + if not hasattr(self, "_remembered_objects"): + self._remembered_objects: dict[str, Mobject] = {} + self._context_snapshot: list[tuple[Mobject, dict[str, Any]]] | None = None + self._context_roots: list[Mobject] = [] + self._highlight_snapshot: list[tuple[Mobject, dict[str, Any]]] | None = None + self._highlight_roots: list[Mobject] = [] + self._camera_context: Any = None + + def setup(self) -> None: + super().setup() + self._ensure_teaching_state() + + def remember(self, name: str, mobject: Mobject, *, add: bool = True) -> Mobject: + self._ensure_teaching_state() + if not name: + raise ValueError("remembered object name cannot be empty") + if name in self._remembered_objects: + raise ValueError(f"object {name!r} is already remembered") + self._remembered_objects[name] = mobject + if add and not any(existing is mobject for existing in self.mobjects): + self.add(mobject) + return mobject + + def recall(self, name: str) -> Mobject: + self._ensure_teaching_state() + try: + return self._remembered_objects[name] + except KeyError as exc: + known = ", ".join(sorted(self._remembered_objects)) or "none" + raise KeyError(f"unknown remembered object {name!r}; known objects: {known}") from exc + + def begin_beat( + self, + name: str, + *, + section_type: DefaultSectionType = DefaultSectionType.NORMAL, + skip_animations: bool = False, + ) -> None: + if not name: + raise ValueError("beat name cannot be empty") + # Positional arguments support both Manim CE 0.20's ``type`` name and + # 0.21's renamed ``section_type`` parameter. + self.next_section(name, section_type, skip_animations) + + def _resolve_targets( + self, target: str | Mobject | Iterable[str | Mobject] + ) -> list[Mobject]: + if isinstance(target, (str, Mobject)): + values: Iterable[str | Mobject] = [target] + else: + values = target + resolved = [self.recall(value) if isinstance(value, str) else value for value in values] + if not resolved or not all(isinstance(value, Mobject) for value in resolved): + raise ValueError("focus target must contain at least one mobject") + return resolved + + @staticmethod + def _unrelated_roots(roots: Iterable[Mobject], targets: Iterable[Mobject]) -> list[Mobject]: + target_ids = { + id(member) + for target in targets + for member in target.get_family() + } + return [ + root + for root in roots + if not any(id(member) in target_ids for member in root.get_family()) + ] + + def _play_focus( + self, + targets: list[Mobject], + dim_animations: list[Any], + *, + margin: float, + run_time: float, + ) -> None: + self.play(*dim_animations, run_time=run_time) + + def _restore_focus_camera(self, *, run_time: float, animate: bool) -> None: + return None + + def focus_on( + self, + target: str | Mobject | Iterable[str | Mobject], + *, + dim_opacity: float = 0.18, + margin: float = 0.8, + run_time: float = 0.6, + animate: bool = True, + ) -> None: + self._ensure_teaching_state() + if self._context_snapshot is not None: + raise RuntimeError("restore the current focus context before starting another") + if not 0 <= dim_opacity <= 1: + raise ValueError("dim opacity must be between zero and one") + targets = self._resolve_targets(target) + unrelated = self._unrelated_roots(self.mobjects, targets) + self._context_roots = unrelated + self._context_snapshot = _style_snapshot(unrelated) + if animate: + animations = [root.animate.set_opacity(dim_opacity) for root in unrelated] + self._play_focus(targets, animations, margin=margin, run_time=run_time) + else: + for root in unrelated: + root.set_opacity(dim_opacity) + self._set_focus_camera_immediately(targets, margin=margin) + + def _set_focus_camera_immediately(self, targets: list[Mobject], *, margin: float) -> None: + return None + + def restore_context(self, *, run_time: float = 0.6, animate: bool = True) -> None: + self._ensure_teaching_state() + if self._context_snapshot is None: + return + snapshot = self._context_snapshot + if animate: + animations = [ + member.animate.set_style(**deepcopy(style)) + for member, style in snapshot + ] + if animations: + self.play(AnimationGroup(*animations, lag_ratio=0), run_time=run_time) + _restore_snapshot(snapshot) + self._restore_focus_camera(run_time=run_time, animate=animate) + self._context_snapshot = None + self._context_roots = [] + + def highlight( + self, + target: str | Mobject | Iterable[str | Mobject], + *, + color: str | None = None, + run_time: float = 0.35, + animate: bool = True, + ) -> None: + self._ensure_teaching_state() + if self._highlight_snapshot is not None: + raise RuntimeError("restore the current highlight before starting another") + targets = self._resolve_targets(target) + self._highlight_roots = targets + self._highlight_snapshot = _style_snapshot(targets) + highlight_color = color or getattr(getattr(self, "theme", None), "accent", "#F2C14E") + if animate: + self.play(*(target.animate.set_color(highlight_color) for target in targets), run_time=run_time) + else: + for focused in targets: + focused.set_color(highlight_color) + + def restore_highlight(self, *, run_time: float = 0.35, animate: bool = True) -> None: + self._ensure_teaching_state() + if self._highlight_snapshot is None: + return + snapshot = self._highlight_snapshot + if animate: + animations = [ + member.animate.set_style(**deepcopy(style)) + for member, style in snapshot + ] + if animations: + self.play(AnimationGroup(*animations, lag_ratio=0), run_time=run_time) + _restore_snapshot(snapshot) + self._highlight_snapshot = None + self._highlight_roots = [] + + def transform_object( + self, + name: str, + target: Mobject, + *, + run_time: float = 0.8, + animate: bool = True, + **animation_kwargs: Any, + ) -> Mobject: + """Morph a remembered object while preserving its registry identity.""" + current = self.recall(name) + if animate: + self.play( + Transform(current, target, **animation_kwargs), + run_time=run_time, + ) + else: + current.become(target) + if isinstance(current, SemanticMobject) and isinstance(target, SemanticMobject): + current._adopt_semantics_from(target) + self._remembered_objects[name] = current + return current + + def hold(self, duration_s: float, *, purpose: str | None = None) -> None: + if duration_s < 0: + raise ValueError("hold duration cannot be negative") + if duration_s > self.max_static_hold_s and not purpose: + raise ValueError( + f"static hold of {duration_s:.2f}s needs an explicit educational purpose" + ) + if duration_s >= 1 / float(config.frame_rate): + self.wait(duration_s) + + +class TeachingScene(_TeachingSceneMixin, MovingCameraScene): + """A moving-camera scene with semantic continuity and attention helpers.""" + + def _play_focus( + self, + targets: list[Mobject], + dim_animations: list[Any], + *, + margin: float, + run_time: float, + ) -> None: + group = Group(*targets) + frame = self.camera.frame + self._camera_context = frame.copy() + target_width = max(group.width + margin * 2, (group.height + margin * 2) * frame.width / frame.height) + camera_animation = frame.animate.move_to(group.get_center()).set(width=target_width) + self.play(*dim_animations, camera_animation, run_time=run_time) + + def _set_focus_camera_immediately(self, targets: list[Mobject], *, margin: float) -> None: + group = Group(*targets) + frame = self.camera.frame + self._camera_context = frame.copy() + target_width = max(group.width + margin * 2, (group.height + margin * 2) * frame.width / frame.height) + frame.move_to(group.get_center()).set(width=target_width) + + def _restore_focus_camera(self, *, run_time: float, animate: bool) -> None: + if self._camera_context is None: + return + if animate: + self.play(Transform(self.camera.frame, self._camera_context), run_time=run_time) + else: + self.camera.frame.become(self._camera_context) + self._camera_context = None + + +class TeachingThreeDScene(_TeachingSceneMixin, ThreeDScene): + """The same semantic authoring contract on top of Manim's ``ThreeDScene``.""" + + def _camera_values(self) -> dict[str, Any]: + return { + "phi": float(self.camera.phi), + "theta": float(self.camera.theta), + "gamma": float(self.camera.gamma), + "zoom": float(self.camera.zoom), + "focal_distance": float(self.camera.focal_distance), + "frame_center": np.asarray(self.camera.frame_center, dtype=float).copy(), + } + + def _play_focus( + self, + targets: list[Mobject], + dim_animations: list[Any], + *, + margin: float, + run_time: float, + ) -> None: + group = Group(*targets) + self._camera_context = self._camera_values() + scale = min( + float(config.frame_width) / max(group.width + 2 * margin, 1e-6), + float(config.frame_height) / max(group.height + 2 * margin, 1e-6), + ) + self.move_camera( + frame_center=group.get_center(), + zoom=min(float(self.camera.zoom) * scale, 8.0), + added_anims=dim_animations, + run_time=run_time, + ) + + def _set_focus_camera_immediately(self, targets: list[Mobject], *, margin: float) -> None: + group = Group(*targets) + self._camera_context = self._camera_values() + scale = min( + float(config.frame_width) / max(group.width + 2 * margin, 1e-6), + float(config.frame_height) / max(group.height + 2 * margin, 1e-6), + ) + self.camera.frame_center = group.get_center() + self.camera.zoom = min(float(self.camera.zoom) * scale, 8.0) + + def _restore_focus_camera(self, *, run_time: float, animate: bool) -> None: + if self._camera_context is None: + return + if animate: + self.move_camera(**self._camera_context, run_time=run_time) + else: + for name, value in self._camera_context.items(): + setattr(self.camera, name, value) + self._camera_context = None + + +__all__ = [ + "LinkedValue", + "SemanticMobject", + "TeachingScene", + "TeachingThreeDScene", + "VisualTheme", + "fit_to_frame", + "theme_text", +] diff --git a/skills/manim-video/references/animation-design-thinking.md b/skills/manim-video/references/animation-design-thinking.md index 2ef3739a..71607748 100644 --- a/skills/manim-video/references/animation-design-thinking.md +++ b/skills/manim-video/references/animation-design-thinking.md @@ -58,7 +58,7 @@ Each beat is one `self.play()` call or a small group of simultaneous animations. ## Pacing: the universal mistake is too fast -### Timing rules +### Timing starting points | Content type | Minimum on-screen time | |-------------|----------------------| @@ -66,16 +66,18 @@ Each beat is one `self.play()` call or a small group of simultaneous animations. | New concept label | 1.0s animation + 1.0s pause | | Key insight ("aha moment") | 2.5s animation + 3.0s pause | | Supporting annotation | 0.8s animation + 0.5s pause | -| Scene transition (FadeOut all) | 0.5s animation + 0.3s pause | +| Deliberate chapter transition | enough time to preserve continuity and orientation | ### Breathing room -After every reveal, add `self.wait()`. The viewer needs time to: +After a meaningful reveal, hold long enough for the viewer to: 1. Read the new text 2. Connect it to what's already on screen 3. Form an expectation about what comes next -**No wait = the viewer is always behind you.** They're still reading the equation when you've already started transforming it. +Do not add a pause mechanically after every animation. Tie the hold to reading, +comparison, prediction, or payoff, and keep related motion continuous when a +pause would break causality. ### Tempo variation diff --git a/skills/manim-video/references/animations.md b/skills/manim-video/references/animations.md index 1bbbc034..d4ab24e3 100644 --- a/skills/manim-video/references/animations.md +++ b/skills/manim-video/references/animations.md @@ -141,9 +141,8 @@ self.wait(2.0) # Dim-and-focus self.play(old_content.animate.set_opacity(0.3), FadeIn(new_content)) -# Clean exit -self.play(FadeOut(Group(*self.mobjects)), run_time=0.5) -self.wait(0.3) +# Deliberate departure: only the obsolete annotation leaves. +self.play(FadeOut(obsolete_annotation), run_time=0.5) ``` ## Reactive Mobjects: always_redraw() diff --git a/skills/manim-video/references/concept-explainer.md b/skills/manim-video/references/concept-explainer.md new file mode 100644 index 00000000..c0c46f53 --- /dev/null +++ b/skills/manim-video/references/concept-explainer.md @@ -0,0 +1,177 @@ +# Original Concept Explainer Contract + +Use this contract when creating an original technical explainer from a topic and +narration. It does not apply when the task is only clipping or treating supplied +footage. The objective is a correct, continuous visual argument rather than a +narrated sequence of cards. + +A topic-only request implies an audible narrated explainer unless the user +explicitly requests silence. Do not create a silent placeholder track: generate +speech, use its measured duration for timing, and verify the assembled audio has +finite loudness and audible samples before normalization. + +## Efficient Production Order + +1. Research the topic and verify every factual claim and worked value. +2. Write narration around one misconception or central question, a progression, + and a concrete final visual payoff. +3. Estimate narration duration locally before making a paid TTS request: + count the words and divide by the voice's words-per-minute rate, then keep a + three-second safety margin below the maximum. After synthesis, read the real + duration from the alignment JSON and re-plan if it misses the window. + + Use the fixed voice's measured rate when known. Otherwise begin near 145 WPM + and treat the result as an estimate. Reserve three seconds below a 55–65 + second ceiling because punctuation and phrasing affect real speech duration. +4. Generate audible narration once, save its alignment sidecar when available, + measure the actual duration, and lock the narration before final animation + timing. Reject or repair a silent source before rendering the EDL. +5. Complete the required `edit/visual_plan.md` below. +6. Choose Manim, Penrose, CeTZ, sourced footage, or another engine beat by beat. + Illustration is the default for original explainers. Footage is optional and + needs an editorial reason plus the existing provenance and rights checks. +7. Copy `assets/teaching.py`, `assets/concept_explainer.py`, and any needed + `assets/domains/` modules into the animation workspace. Build one independently + renderable Manim class per narrative chapter. +8. Mark internal teaching beats with `TeachingScene.begin_beat()` or + `next_section()`. Preview and repair only the affected chapter classes. +9. Render the complete visual base, add approved footage through the existing + overlay contract, and apply narration and captions through the existing EDL + renderer. +10. Review the complete video at 1x and persist run evidence. Isolated previews + speed up authoring but are not the quality gate. + +## Required `edit/visual_plan.md` + +This artifact is advisory rather than a scene compiler: write it, follow it, and +still use custom Manim whenever an abstraction would constrain the teaching. +The plan must contain: + +### Teaching contract + +- **Audience and assumed knowledge** +- **Central question or misconception** +- **Teaching thesis** +- **Final aha moment**: the exact relationship the payoff frame makes visible +- **Approved visual direction**: semantic theme and typography roles + +### Chapter map + +Identify one independently renderable class per narrative chapter. Each chapter +contains multiple teaching beats and should build, transform, compare, or move a +persistent set of objects instead of resetting into a new card. + +### Beat table + +Add one row per meaningful visual beat: + +| Narration span | Named objects | Initial state | Object state change | Relationships updated together | Camera or attention action | Result / what this proves | Carried into next beat | +|---|---|---|---|---|---|---|---| +| 8.2–10.5 “tasks run together” | `queue`, `workers`, `elapsed_time` | tasks wait in one queue | tasks fan into workers | worker status and elapsed-time counter advance from one value | focus on active workers; queue dims | concurrency shortens elapsed time | workers and counter | + +If the action and resulting state are the same, the beat is probably a static +slide. Redesign it as a construction, comparison, transformation, transfer, or +flow. Every spoken claim needs visible evidence, but not every sentence needs a +new object. + +### Payoff frame + +Name every object that remains, its final state and position, the relationship +the viewer should notice, and how long the frame is held through the final +spoken words. + +### Optional footage + +For each footage beat, record the source, intended crop and timing, rights or +license evidence, and why footage communicates that claim better than an +illustration. Omit footage when no beat benefits from it. + +## Semantic Authoring Rules + +- Important objects persist across multiple beats. Preserve their semantic color + role and position unless a deliberate transformation teaches something. +- Related representations share one underlying value. A changed probability, + balance, vector, or request state must update every visible representation. +- Motion reveals causality or structure; it does not exist to vary animation + types mechanically. +- Camera movement and opacity have an attention purpose recorded in the plan. +- Do not reveal a completed diagram first and spend the chapter pointing at its + labels. Build the relevant relationship as the narration earns it. +- A 10–15 second chapter normally contains 3–5 meaningful state changes. Static + holds longer than three seconds need a reading, comparison, prediction, or + payoff purpose. +- The main teaching object must remain readable at phone size. Keep titles, + labels, equations, and payoff copy inside the safe frame. +- Compute derived numbers in code. Matrix products, percentages, counters, chart + values, and equation results must come from a calculation or assertion. +- Use timestamp alignment to land important reveals on their payoff words. Let + the visual lead the word slightly rather than appear after it. +- Hold the last meaningful frame through the final spoken words. Fade only when + departure itself is meaningful; do not create unexplained blank padding. + +## Reusable Assets + +`assets/teaching.py` provides: + +- `VisualTheme` for explicit semantic color and typography roles +- `SemanticMobject` for stable named parts and anchors +- `LinkedValue` for synchronized representations driven by one `ValueTracker` +- `TeachingScene` and `TeachingThreeDScene` for persistent object registries, + beat markers, camera/opacity focus, highlights, transformations, and holds + +`assets/domains/` provides style-neutral math, algorithms/AI, systems, physics, +biology, finance, and business components. They expose concept-level actions and +remain ordinary Manim objects. Raw Manim is always available. + +`assets/concept_explainer.py` provides caption-safe content frames, fitted text +and panels, composition-region guides, final-state overlap checks, and +EDL-ready normalized bounds. Use `assert_inside_frame()` and +`assert_no_overlap()` on every critical state. When footage shares the frame, +use `composition_regions()` and export the teaching object's +`normalized_bounds()` to the EDL `protected_regions` contract. + +Penrose and CeTZ remain available for precise static structures. Import those +assets into Manim when temporal reveal or semantic transformation is needed. + +## Targeted Preview + +```bash +python skills/manim-video/scripts/preview_scene.py \ + /path/to/project/edit/animations/script.py ChapterOne ChapterThree +``` + +The command uses Manim Community low quality and its cache, writes only beneath +`edit/verify/manim_previews/`, and produces each selected chapter's video, +initial/final frames, five-frame contact sheet, duration, dimensions, frame rate, +render status, and available section metadata. Selection is scene-class level; +`next_section()` metadata helps inspect internal beats but does not imply +unsupported arbitrary section-only rendering. + +## Review Before Production + +- Read the narration while watching the complete low-quality assembly at 1x. +- Confirm every beat-table action is visibly demonstrated. +- Confirm named objects persist for the spans promised by the plan. +- Confirm linked representations agree at initial, intermediate, and final state. +- Search for static holds longer than three seconds and justify or replace them. +- Confirm every derived value is calculated or asserted. +- Confirm every narrative chapter renders independently and section markers match + its internal beats. +- Preview text-heavy frames at medium quality and pass safe-frame validation. +- Inspect the complete video for factual errors, clipped teaching content, + caption collisions, broken rendering, missing audio, and delivery properties. + +## Shared-Frame Footage Rules + +Reserve the bottom caption rail defined by the existing explainer asset. Choose +a relationship rather than merely adding a rectangle: `cutaway`, `split_left`, +`split_right`, or `picture_in_picture`. A split frame pairs footage with an +illustration only when the two views teach the same narration sentence. Place +picture-in-picture footage in genuine negative space and declare the teaching +object's occupied bounds so overlay preflight can reject collisions at footage +entry, middle, and exit frames. + +Inspect only a few candidate moments from each source. If a source cannot be +cleared or does not teach the beat better than illustration, omit it rather than +lowering relevance. Keep provenance in a restrained source footer instead of +adding decorative title hierarchy. diff --git a/skills/manim-video/references/mobjects.md b/skills/manim-video/references/mobjects.md index ec68b375..27d0bb24 100644 --- a/skills/manim-video/references/mobjects.md +++ b/skills/manim-video/references/mobjects.md @@ -106,8 +106,8 @@ shapes.set_color(BLUE) labeled_shape = Group(circle, Text("Label").next_to(circle, DOWN)) labeled_shape.move_to(ORIGIN) -# FadeOut everything on screen (may contain mixed types) -self.play(FadeOut(Group(*self.mobjects))) +# A deliberate mixed-type departure can use Group. +self.play(FadeOut(Group(label, circle))) ``` **Rule: if your group contains any `Text()` objects, use `Group`, not `VGroup`.** VGroup will raise a TypeError on Manim CE v0.20+. MathTex and Tex are VMobjects and work with VGroup. diff --git a/skills/manim-video/references/paper-explainer.md b/skills/manim-video/references/paper-explainer.md index 9088ffca..424e2a7b 100644 --- a/skills/manim-video/references/paper-explainer.md +++ b/skills/manim-video/references/paper-explainer.md @@ -69,12 +69,15 @@ But Chinchilla showed us that most models are undertrained..." After the narration, break it into scenes. Each scene is one Manim class. ```markdown -Scene 1: Hook — surprising stat with animated counter -Scene 2: Problem — model size bar chart growing -Scene 3: Key insight — training data vs parameters, animated 2D plot -Scene 4: Method — pipeline diagram building left to right -Scene 5: Results — before/after comparison with animated bars -Scene 6: Closing — implications text +Chapter 1: Establish the persistent model and expose the paper's problem + Beat: measured failure + Beat: failed baseline +Chapter 2: Transform that model with the paper's method + Beat: mechanism + Beat: linked prediction +Chapter 3: Compare measured results and land the implication payoff + Beat: before/after + Beat: final thesis ``` ### Gate 3: Style constants @@ -82,19 +85,27 @@ Scene 6: Closing — implications text Before coding scenes, define the visual language: ```python -# style.py — import in every scene file -BG = "#0D1117" -PRIMARY = "#58C4DD" -SECONDARY = "#83C167" -ACCENT = "#FFFF00" -HIGHLIGHT = "#FF6B6B" -MONO = "Menlo" +# style.py — import in every chapter file +from teaching import VisualTheme + +THEME = VisualTheme( + background="", + text="", + muted="", + primary="", + secondary="", + accent="", + warning="", + title_font="", + body_font="", + label_font="", +) # Color meanings for THIS paper -MODEL_COLOR = PRIMARY # "the model" -DATA_COLOR = SECONDARY # "training data" -BASELINE_COLOR = HIGHLIGHT # "previous approach" -RESULT_COLOR = ACCENT # "our result" +MODEL_COLOR = THEME.primary +DATA_COLOR = THEME.secondary +BASELINE_COLOR = THEME.warning +RESULT_COLOR = THEME.accent ``` ## First-principles equation explanation diff --git a/skills/manim-video/references/production-quality.md b/skills/manim-video/references/production-quality.md index 1b371f89..aa2a869e 100644 --- a/skills/manim-video/references/production-quality.md +++ b/skills/manim-video/references/production-quality.md @@ -1,190 +1,108 @@ # Production Quality Checklist -Standards and checks for ensuring animation output is publication-ready. +Standards for publication-ready original explainers. -## Pre-Code Checklist +## Before Code -Before writing any Manim code: +- [ ] Research is sourced and claims are verified. +- [ ] Narration states a misconception, progression, thesis, and visual payoff. +- [ ] `edit/visual_plan.md` contains the required teaching contract, chapter map, + beat table, payoff frame, and optional-footage rationale. +- [ ] Semantically named objects and carried state are identified. +- [ ] Coupled representations share a calculation or `LinkedValue`. +- [ ] One `VisualTheme` defines color and title/body/label font roles. +- [ ] Target resolution, aspect ratio, caption rail, and protected regions are + known. -- [ ] Narration script written with visual beats marked -- [ ] Scene list with purpose, duration, and layout for each -- [ ] Color palette defined with meaning assignments (`PRIMARY` = main concept, etc.) -- [ ] `MONO = "Menlo"` set as the font constant -- [ ] Target resolution and aspect ratio decided - -## Text Quality - -### Overlap prevention +## Text and Frame Safety ```python -# RULE: buff >= 0.5 for edge text -label.to_edge(DOWN, buff=0.5) # GOOD -label.to_edge(DOWN, buff=0.3) # BAD — may clip +# Keep edge text inside a meaningful safe margin. +label.to_edge(DOWN, buff=0.5) -# RULE: FadeOut previous before adding new at same position -self.play(ReplacementTransform(note1, note2)) # GOOD -self.play(Write(note2)) # BAD — overlaps note1 - -# RULE: Reduce font size for dense scenes -# When > 4 text elements visible, use font_size=20 not 28 -``` +# Transform copy occupying the same semantic role. +self.play(ReplacementTransform(note1, note2)) -### Width enforcement - -Long text strings overflow the frame: - -```python -# RULE: Set max width for any text that might be long -text = Text("This is a potentially long description", font_size=22, font=MONO) +# Fit authored copy before rendering. if text.width > config.frame_width - 1.0: - text.set_width(config.frame_width - 1.0) -``` - -### Font consistency - -```python -# RULE: Define MONO once, use everywhere -MONO = "Menlo" - -# WRONG: mixing fonts -Text("Title", font="Helvetica") -Text("Label", font="Arial") -Text("Code", font="Courier") - -# RIGHT: one font -Text("Title", font=MONO, weight=BOLD, font_size=48) -Text("Label", font=MONO, font_size=20) -Text("Code", font=MONO, font_size=18) -``` - -## Spatial Layout - -### The coordinate budget - -The visible frame is approximately 14.2 wide × 8.0 tall (default 16:9). With mandatory margins: - -``` -Usable area: x ∈ [-6.5, 6.5], y ∈ [-3.5, 3.5] -Top title zone: y ∈ [2.5, 3.5] -Bottom note zone: y ∈ [-3.5, -2.5] -Main content: y ∈ [-2.5, 2.5], x ∈ [-6.0, 6.0] + text.scale_to_fit_width(config.frame_width - 1.0) ``` -### Fill the frame +- Keep ordinary text at `font_size=18` or larger. +- Use the approved typography; monospace is optional, not universal. +- Test selected fonts on the render host. +- Run `assert_inside_frame()` and `assert_no_overlap()` for every chapter ending. +- Treat clipped titles, labels, equations, and payoff copy as failed renders. -Empty scenes look unfinished. If the main content is small, add context: -- A dimmed grid/axes behind the content -- A title/subtitle at the top -- A source citation at the bottom -- Decorative geometry at low opacity +## Spatial and Semantic Continuity -### Maximum simultaneous elements +The default 16:9 frame is about 14.2 by 8 units. Reserve real margins, protected +caption space, and enough negative space for attention shifts. -**Hard limit: 6 actively visible elements.** Beyond that, the viewer can't track everything. If you need more: -- Dim old elements to opacity 0.3 -- Remove elements that have served their purpose -- Split into two scenes +- Important objects persist across beats instead of being recreated. +- A concept keeps its semantic color and approximate position. +- Camera moves only when changing scale or focus teaches something. +- Dim context instead of deleting it when the viewer still needs orientation. +- Remove an object only when completion, replacement, loss, or scope change makes + that departure meaningful. +- Dense diagrams can exceed six total objects when hierarchy and focus keep the + active relationship legible; there is no arbitrary object-count limit. ## Animation Quality -### Variety audit - -Check that no two consecutive scenes use the exact same: -- Animation type (if Scene 3 uses Write for everything, Scene 4 should use FadeIn or Create) -- Color emphasis (rotate through palette colors) -- Layout (center, left-right, grid — alternate) -- Pacing (if Scene 2 was slow and deliberate, Scene 3 can be faster) - -### Tempo curve - -A good video follows a tempo curve: - -``` -Slow ──→ Medium ──→ FAST (climax) ──→ Slow (conclusion) - -Scene 1: Slow (introduction, setup) -Scene 2: Medium (building understanding) -Scene 3: Medium-Fast (core content, lots of animation) -Scene 4: FAST (montage of applications/results) -Scene 5: Slow (conclusion, key takeaway) -``` - -### Transition quality - -Between scenes: -- **Clean exit**: `self.play(FadeOut(Group(*self.mobjects)), run_time=0.5)` -- **Brief pause**: `self.wait(0.3)` after fadeout, before next scene's first animation -- **Never hard-cut**: always animate the transition - -## Color Quality - -### Dimming on dark backgrounds - -Colors that look vibrant on white look muddy on dark backgrounds (#0D1117, #1C1C1C). Test your palette: - -```python -# Colors that work well on dark backgrounds: -# Bright and saturated: #58C4DD, #83C167, #FFFF00, #FF6B6B -# Colors that DON'T work: #666666 (invisible), #2244AA (too dark) - -# RULE: Structural elements (axes, grids) at opacity 0.15 -# Context elements at 0.3-0.4 -# Primary elements at 1.0 -``` - -### Color meaning consistency - -Once a color is assigned a meaning, it keeps that meaning for the entire video: - -```python -# If PRIMARY (#58C4DD) means "the model" in Scene 1, -# it means "the model" in every scene. -# Never reuse PRIMARY for a different concept later. -``` - -## Data Visualization Quality - -### Minimum requirements for charts - -- Axis labels on every axis -- Y-axis range starts at 0 (or has a clear break indicator) -- Bar/line colors match the legend -- Numbers on notable data points (at least the maximum and the comparison point) - -### Animated counters - -When showing a number changing: -```python -# GOOD: DecimalNumber with smooth animation -counter = DecimalNumber(0, font_size=48, num_decimal_places=0, font="Menlo") -self.play(counter.animate.set_value(1000), run_time=3, rate_func=rush_from) - -# BAD: Text that jumps between values -``` - -## Pre-Render Checklist - -Before running `manim -qh`: - -- [ ] All scenes render without errors at `-ql` -- [ ] Preview stills at `-qm` for text-heavy scenes (check kerning) -- [ ] Background color set in every scene (`self.camera.background_color = BG`) -- [ ] `add_subcaption()` or `subcaption=` on every significant animation -- [ ] No text smaller than font_size=18 -- [ ] No text using proportional fonts (use monospace) -- [ ] buff >= 0.5 on all `.to_edge()` calls -- [ ] Clean exit (FadeOut all) at end of every scene -- [ ] `self.wait()` after every reveal -- [ ] Color constants used (no hardcoded hex strings in scene code) -- [ ] All scenes use the same quality flag (don't mix `-ql` and `-qh`) - -## Post-Render Checklist - -After stitching the final video: - -- [ ] Watch the complete video at 1x speed — does it feel rushed anywhere? -- [ ] Is there a moment where two things animate simultaneously and it's confusing? -- [ ] Does every text label have enough time to be read? -- [ ] Are transitions between scenes smooth (no black frames, no jarring cuts)? -- [ ] Is the audio in sync with the visuals (if using voiceover)? -- [ ] Is the Gibbs-like "first impression" good? The first 5 seconds determine if someone keeps watching +- Every animation constructs, transforms, transfers, compares, or reveals a + causal relationship. +- Do not rotate animation types, dominant colors, or layouts merely for variety. +- Simultaneous motion is coordinated around one causal event. +- Holds exist for reading, comparison, prediction, or payoff. Holds longer than + three seconds record their purpose. +- Hard cuts, fades, and transform bridges are all valid when editorially + motivated. Fading every mobject at every boundary is not a cleanup rule. +- The last meaningful state remains visible through the final spoken words. + +## Color and Typography + +- The shared theme has sufficient contrast on its actual background. +- Muted context remains visible but subordinate. +- Accent denotes the same teaching role across chapters. +- Warning is reserved for exceptions, failures, or risk. +- Font roles remain stable and render cleanly at delivery size. +- No external creator's palette, typography, scene code, or branding is copied. + +## Data and Linked Representations + +- Axis scales and breaks are honest and labeled when needed. +- Notable values come from calculations rather than duplicated literals. +- Counters, plots, arrows, matrices, and labels agree at initial, intermediate, + and final states. +- Tests or assertions cover derived matrix products, percentages, balances, and + other authored values. + +## Before Production Render + +- [ ] Every chapter class renders by itself at low quality. +- [ ] `begin_beat()` or `next_section()` names internal teaching beats. +- [ ] Targeted preview artifacts include video, first/final frames, contact sheet, + media metadata, status, and available section metadata. +- [ ] Text-heavy frames have a medium-quality inspection. +- [ ] Safe-frame checks pass. +- [ ] The payoff frame matches `edit/visual_plan.md`. +- [ ] Sourced footage, if any, has provenance, rights evidence, and a written + reason it is better than illustration. + +## Complete-Video Review + +- [ ] Watch the full video at 1x; isolated chapter previews are not the quality + gate. +- [ ] The teaching thesis and final payoff are clear. +- [ ] Important objects persist across multiple beats. +- [ ] Motion reveals relationships instead of decorating. +- [ ] Linked representations remain consistent. +- [ ] Camera and opacity guide attention. +- [ ] Visual language and pacing remain coherent. +- [ ] The video does not repeatedly reset into title-and-card slides. +- [ ] There are no factual errors, invalid values, clipped teaching content, + caption collisions, broken renders, missing audio, or incorrect delivery + properties. +- [ ] Existing EDL overlay order, protected regions, captions-last behavior, and + delivery validation still pass. diff --git a/skills/manim-video/references/rendering.md b/skills/manim-video/references/rendering.md index 882eb19d..eb4a99d4 100644 --- a/skills/manim-video/references/rendering.md +++ b/skills/manim-video/references/rendering.md @@ -128,8 +128,11 @@ class LongVideo(Scene): # ... closing ... ``` -Render individual sections: `manim --save_sections script.py LongVideo` -This outputs separate video files per section — useful for long videos where you want to re-render only one part. +`manim --save_sections script.py LongVideo` saves section videos and metadata for +inspection. Targeted authoring still selects the whole scene class; do not +promise unsupported arbitrary section-only rendering. Use +`scripts/preview_scene.py` to preview selected chapter classes and inspect their +saved section metadata. ## manim-voiceover Plugin (Recommended for Narrated Videos) diff --git a/skills/manim-video/references/scene-planning.md b/skills/manim-video/references/scene-planning.md index f42b78f3..0d4f31d5 100644 --- a/skills/manim-video/references/scene-planning.md +++ b/skills/manim-video/references/scene-planning.md @@ -1,118 +1,130 @@ -# Scene Planning Reference +# Semantic Chapter Planning Reference -## Narrative Arc Structures - -### Discovery Arc (most common) -1. Hook -- pose a question or surprising result -2. Intuition -- build visual understanding -3. Formalize -- introduce the equation/algorithm -4. Reveal -- the "aha moment" -5. Extend -- implications or generalizations +Use this reference with the required `edit/visual_plan.md` contract in +`concept-explainer.md`. The artifact guides custom authoring; it is not a rigid +scene compiler. -### Problem-Solution Arc -1. Problem -- what's broken -2. Failed attempt -- obvious approach fails -3. Key insight -- the idea that works -4. Solution -- implement it -5. Result -- show improvement +## Narrative Arc Structures -### Comparison Arc -1. Setup -- introduce two approaches -2. Approach A -- how it works -3. Approach B -- how it works -4. Contrast -- differences -5. Verdict -- which is better - -### Build-Up Arc (architecture/systems) -1. Component A -- first piece -2. Component B -- second piece -3. Connection -- how they interact -4. Scale -- add more pieces -5. Full picture -- zoom out - -## Scene Transitions - -### Clean Break (default) -```python -self.play(FadeOut(Group(*self.mobjects)), run_time=0.5) -self.wait(0.3) -``` +### Discovery Arc -### Carry-Forward -Keep one element, fade the rest. Next scene starts with it still on screen. +1. Hook — pose the central question or expose the misconception. +2. Intuition — construct a visual model the audience can manipulate mentally. +3. Formalize — connect the model to notation, an algorithm, or a mechanism. +4. Reveal — coordinate the representations into the final aha moment. +5. Extend — show one implication without diluting the thesis. -### Transform Bridge -End scene with a shape, start next scene by transforming it. +### Problem-Solution Arc -## Cross-Scene Consistency +1. Problem — make the failure visible in a persistent system. +2. Failed attempt — transform the same system and show why it still fails. +3. Key insight — introduce the missing relationship. +4. Solution — update the system causally. +5. Result — compare the original and final states. -```python -# Shared constants at file top -BG = "#1C1C1C" -PRIMARY = "#58C4DD" -SECONDARY = "#83C167" -ACCENT = "#FFFF00" -TITLE_SIZE = 48 -BODY_SIZE = 30 -LABEL_SIZE = 24 -FAST = 0.8; NORMAL = 1.5; SLOW = 2.5 -``` +### Comparison Arc -## Scene Checklist - -- [ ] Background color set -- [ ] Subcaptions on every animation -- [ ] `self.wait()` after every reveal -- [ ] Text buff >= 0.5 for edge positioning -- [ ] No text overlap -- [ ] Color constants used (not hardcoded) -- [ ] Opacity layering applied -- [ ] Clean exit at scene end -- [ ] No more than 5-6 elements visible at once - -## Duration Estimation - -| Content | Duration | -|---------|----------| -| Title card | 3-5s | -| Concept introduction | 10-20s | -| Equation reveal | 15-25s | -| Algorithm step | 5-10s | -| Data comparison | 10-15s | -| "Aha moment" | 15-30s | -| Conclusion | 5-10s | +1. Setup — establish shared inputs and measures. +2. Approach A — transform the first copy. +3. Approach B — transform the second copy. +4. Contrast — link differences to the same underlying values. +5. Verdict — land on a frame that makes the decision legible. + +### Build-Up Arc + +1. Establish the first component. +2. Add the next component without moving the first arbitrarily. +3. Reveal the relationship through transfer or flow. +4. Change scale or conditions while linked representations update. +5. Move the camera only when the full system or a local mechanism is the payoff. + +## Chapter Boundaries + +Create one independently renderable class per narrative chapter. Within it, use +`begin_beat()` or `next_section()` for the teaching beats. A boundary is not an +instruction to clear the frame: + +- **Carry forward** an important object in its current state. +- **Transform bridge** one object into the next representation while preserving + its semantic identity. +- **Context shift** keep the system visible but dim or reframe it. +- **Deliberate departure** remove an object only when its absence communicates + completion, replacement, loss, or a change of scope. + +Hard cuts and fades are valid editorial choices when intentional. They are not +default cleanup operations. + +## Cross-Chapter Consistency + +- One shared `VisualTheme` defines semantic color and font roles. +- The same concept keeps its color role and approximate position. +- Repeated quantities use one `LinkedValue` or one source calculation. +- Motion vocabulary remains coherent; vary it only when the meaning changes. +- A chapter payoff should become the next chapter's context when the narrative + depends on it. + +## Chapter Checklist + +- [ ] The chapter advances one stage of the teaching thesis. +- [ ] Internal beats are named with section markers. +- [ ] Persistent objects have semantic names. +- [ ] Each beat specifies initial state, visible action, and resulting state. +- [ ] Coupled representations update together. +- [ ] Camera movement or dimming has an explicit attention purpose. +- [ ] The final frame proves the planned claim. +- [ ] Carried objects are named for the next beat or chapter. +- [ ] All text and teaching content remain frame-safe. + +## Duration Heuristics + +| Content | Typical duration | +|---|---:| +| Establish a visual model | 5–10s | +| Construct a relationship | 8–15s | +| Transform or compare states | 6–12s | +| Dense payoff study | 3–6s | +| Extend the result | 5–10s | + +These are planning ranges, not pacing presets. Narration alignment and viewer +comprehension decide the real timing. ## Planning Template ```markdown -# [Video Title] - -## Overview -- **Topic**: [Core concept] -- **Hook**: [Opening question] -- **Aha moment**: [Key insight] -- **Target audience**: [Prerequisites] -- **Length**: [seconds/minutes] -- **Resolution**: 480p (draft) / 1080p (final) - -## Color Palette -- Background: #1C1C1C -- Primary: #58C4DD -- [purpose] -- Secondary: #83C167 -- [purpose] -- Accent: #FFFF00 -- [purpose] - -## Arc: [Discovery / Problem-Solution / Comparison / Build-Up] - -## Scene 1: [Name] (~Ns) -**Purpose**: [one sentence] -**Layout**: [FULL_CENTER / LEFT_RIGHT / GRID / PROGRESSIVE] - -### Visual elements -- [Mobject: type, position, color] - -### Animation sequence -1. [Animation] -- [what it reveals] (~Ns) - -### Subtitle -"[text]" +# [Video title] + +## Teaching contract +- Audience and assumed knowledge: +- Central question or misconception: +- Teaching thesis: +- Final aha moment: +- Target duration and delivery: +- Visual direction: + +## Semantic theme +- Background: +- Text: +- Muted/context: +- Primary meaning: +- Secondary meaning: +- Accent meaning: +- Warning meaning: +- Title/body/label font roles: + +## Chapter map +| Chapter class | Purpose | Narration span | Entry state | Payoff state | +|---|---|---|---|---| + +## Beat plan +| Narration span | Named objects | Initial state | Object state change | Relationships updated together | Camera/attention | Result and proof | Carried forward | +|---|---|---|---|---|---|---|---| + +## Payoff frame +- Remaining objects and states: +- Relationship the viewer should see: +- Hold purpose and duration: + +## Optional footage +| Beat | Source/provenance | Crop/timing | Why footage is better than illustration | +|---|---|---|---| ``` diff --git a/skills/manim-video/references/teaching-api.md b/skills/manim-video/references/teaching-api.md new file mode 100644 index 00000000..891adf5a --- /dev/null +++ b/skills/manim-video/references/teaching-api.md @@ -0,0 +1,49 @@ +# Semantic Teaching API + +Copy `assets/teaching.py` and the needed `assets/domains/` modules into an +original explainer workspace. Pass one shared `VisualTheme` to every component. +These APIs manage semantic identity, linked state, attention, and continuity; +ordinary Manim remains available for custom visuals. + +## Foundations + +- `VisualTheme` — background, text, muted, primary, secondary, accent, warning, + and title/body/label font roles. +- `SemanticMobject` — register and retrieve stable named parts and anchors with + `register_part()`, `part()`, `register_anchor()`, and `anchor()`. Use + `highlight()` and `restore_opacity()` for local semantic focus. +- `LinkedValue` — bind named mobjects to one `ValueTracker`; use `set_value()` or + `animate_to()` and suspend, resume, or clear bindings when a representation + deliberately stops following the value. +- `TeachingScene` — a `MovingCameraScene` with `remember()`, `recall()`, + `begin_beat()`, `focus_on()`, `restore_context()`, `highlight()`, + `restore_highlight()`, `transform_object()`, and `hold()`. +- `TeachingThreeDScene` — the same registry, beat, focus, highlight, + transformation, and hold contract on `ThreeDScene`. + +## Domain Components + +| Family | Components | Representative actions | +|---|---|---| +| Math | `NumberLineModel`, `VectorMap`, `MatrixMap`, `LinkedPlot`, `ProbabilityMass` | move a value, apply a matrix, update a plotted point, transfer probability | +| Algorithms and AI | `ArrayModel`, `GraphModel`, `StateMachine`, `TokenFlow`, `NeuralLayer` | swap values, visit/traverse, transition, mix context, activate | +| Software systems | `RequestFlow`, `ServiceGraph`, `QueueModel`, `DataPipeline` | advance/send a request, enqueue/dequeue, propagate data | +| Physics | `BodySystem`, `ForceVector`, `WaveField`, `CircuitFlow` | move a body, apply/set force, propagate a wave, flow current | +| Biology | `CellProcess`, `SequenceProcess`, `PopulationFlow` | advance a process, transcribe/translate, transfer population | +| Finance and business | `CashFlow`, `CompoundTimeline`, `Funnel`, `FeedbackLoop`, `ResourceFlow` | transfer cash/resources, compound, convert, reinforce | + +Every component is frame-fitted at construction, accepts empty or long display +labels, exposes named semantic parts and anchors, and inherits the common focus +and opacity-restoration behavior. Action methods return Manim animations so an +author controls timing and composition with `self.play()`. + +## Identity and State + +- Use `remember()` once for an important object and `recall()` in later beats. +- Use `transform_object()` when its representation changes but its narrative + identity does not. +- Drive multiple representations from `LinkedValue` or one verified source + calculation; never duplicate a changing value in unrelated literals. +- Restore focus and highlights before starting another attention context. The + helpers preserve the previous opacity and style exactly. +- Use `begin_beat()` for inspectable `next_section()` metadata inside a chapter. diff --git a/skills/manim-video/references/troubleshooting.md b/skills/manim-video/references/troubleshooting.md index 98c63fd2..ad4b979d 100644 --- a/skills/manim-video/references/troubleshooting.md +++ b/skills/manim-video/references/troubleshooting.md @@ -41,9 +41,10 @@ equations = VGroup(MathTex(r"a"), MathTex(r"b")) **Rule:** If the group contains any `Text()`, use `Group`. If it's all shapes or all `MathTex`, `VGroup` is fine. -**FadeOut everything:** Always use `Group(*self.mobjects)`, not `VGroup(*self.mobjects)`: +**Mixed-type departure:** When a deliberate departure contains `Text`, use +`Group`, not `VGroup`: ```python -self.play(FadeOut(Group(*self.mobjects))) # safe for mixed types +self.play(FadeOut(Group(label, diagram))) # safe for mixed types ``` ## Group save_state() / restore() Not Supported diff --git a/skills/manim-video/references/visual-design.md b/skills/manim-video/references/visual-design.md index e7dcec01..edf4c2b0 100644 --- a/skills/manim-video/references/visual-design.md +++ b/skills/manim-video/references/visual-design.md @@ -1,124 +1,97 @@ # Visual Design Principles -## 12 Core Principles - -1. **Geometry Before Algebra** — Show the shape first, the equation second. -2. **Opacity Layering** — PRIMARY=1.0, CONTEXT=0.4, GRID=0.15. Direct attention through brightness. -3. **One New Idea Per Scene** — Each scene introduces exactly one concept. -4. **Spatial Consistency** — Same concept occupies the same screen region throughout. -5. **Color = Meaning** — Assign colors to concepts, not mobjects. If velocity is blue, it stays blue. -6. **Progressive Disclosure** — Show simplest version first, add complexity incrementally. -7. **Transform, Don't Replace** — Use Transform/ReplacementTransform to show connections. -8. **Breathing Room** — `self.wait(1.5)` minimum after showing something new. -9. **Visual Weight Balance** — Don't cluster everything on one side. -10. **Consistent Motion Vocabulary** — Pick a small set of animation types and reuse them. -11. **Dark Background, Light Content** — #1C1C1C to #2D2B55 backgrounds maximize contrast. -12. **Intentional Empty Space** — Leave at least 15% of the frame empty. - -## Layout Templates - -### FULL_CENTER -One main element centered, title above, note below. -Best for: single equations, single diagrams, title cards. - -### LEFT_RIGHT -Two elements side by side at x=-3.5 and x=3.5. -Best for: equation + visual, before/after, comparison. - -### TOP_BOTTOM -Main element at y=1.5, supporting content at y=-1.5. -Best for: concept + examples, theorem + cases. - -### GRID -Multiple elements via `arrange_in_grid()`. -Best for: comparison matrices, multi-step processes. - -### PROGRESSIVE -Elements appear one at a time, arranged DOWN with aligned_edge=LEFT. -Best for: algorithms, proofs, step-by-step processes. - -### ANNOTATED_DIAGRAM -Central diagram with floating labels connected by arrows. -Best for: architecture diagrams, annotated figures. - -## Color Palettes - -### Classic 3B1B -```python -BG="#1C1C1C"; PRIMARY=BLUE; SECONDARY=GREEN; ACCENT=YELLOW; HIGHLIGHT=RED -``` - -### Warm Academic -```python -BG="#2D2B55"; PRIMARY="#FF6B6B"; SECONDARY="#FFD93D"; ACCENT="#6BCB77" -``` - -### Neon Tech -```python -BG="#0A0A0A"; PRIMARY="#00F5FF"; SECONDARY="#FF00FF"; ACCENT="#39FF14" -``` - -## Font Selection - -**Use monospace fonts for all text.** Manim's Pango text renderer produces broken kerning with proportional fonts (Helvetica, Inter, SF Pro, Arial) at all sizes and resolutions. Characters overlap and spacing is inconsistent. This is a fundamental Pango limitation, not a Manim bug. - -Monospace fonts have fixed character widths — zero kerning issues by design. - -### Recommended Fonts - -| Use case | Font | Fallback | -|----------|------|----------| -| **All text (default)** | `"Menlo"` | `"Courier New"`, `"DejaVu Sans Mono"` | -| Code, labels | `"JetBrains Mono"`, `"SF Mono"` | `"Menlo"` | -| Math | Use `MathTex` (renders via LaTeX, not Pango) | — | - -```python -MONO = "Menlo" # define once at top of file - -title = Text("Fourier Series", font_size=48, color=PRIMARY, weight=BOLD, font=MONO) -label = Text("n=1: (4/pi) sin(x)", font_size=20, color=BLUE, font=MONO) -note = Text("Convergence at discontinuities", font_size=18, color=DIM, font=MONO) - -# Math — always use MathTex, not Text -equation = MathTex(r"\nabla L = \frac{\partial L}{\partial w}") -``` - -### When Proportional Fonts Are Acceptable - -Large title text (font_size >= 48) with short strings (1-3 words) can use proportional fonts without visible kerning issues. For anything else — labels, descriptions, multi-word text, small sizes — use monospace. - -### Font Availability - -- **macOS**: Menlo (pre-installed), SF Mono -- **Linux**: DejaVu Sans Mono (pre-installed), Liberation Mono -- **Cross-platform**: JetBrains Mono (install from jetbrains.com) - -`"Menlo"` is the safest default — pre-installed on macOS, and Linux systems fall back to DejaVu Sans Mono. - -### Fine-Grained Text Control - -`Text()` does not support `letter_spacing` or kerning parameters. For fine control, use `MarkupText` with Pango attributes: +## Core Principles + +1. **Geometry before algebra** — show the shape or mechanism before notation when + that progression helps the topic. +2. **Opacity directs attention** — keep the active relationship strongest and + preserve context at a lower salience. +3. **Beats change understanding** — a beat introduces a state change or a new + relationship, not merely a new card. +4. **Spatial consistency** — the same concept occupies the same region unless + movement itself explains something. +5. **Color equals meaning** — assign theme roles to concepts rather than rotating + colors between chapters. +6. **Progressive disclosure** — construct the simplest useful state first and add + complexity as the narration earns it. +7. **Transform instead of reset** — maintain object identity when showing how two + states or representations connect. +8. **Deliberate holds** — pause for reading, comparison, prediction, or payoff. +9. **Visual weight balance** — make the central teaching object large enough to + own the content area without filling space decoratively. +10. **Consistent motion vocabulary** — reuse movement that has the same meaning. +11. **Direction-specific contrast** — validate the approved palette on its actual + background rather than assuming every explainer must be dark. +12. **Intentional empty space** — leave enough room for hierarchy, camera focus, + and captions. + +## Layout Patterns + +These are compositional starting points, not fixed templates: + +- **Single model** — one persistent object with local labels and a payoff state. +- **Linked representations** — equation, geometry, chart, or counter driven by + the same value. +- **Before and after** — shared inputs and measures, with a visible causal change. +- **System flow** — stable components connected by a request, force, resource, or + information transfer. +- **Nested focus** — a complete system stays visible while camera and opacity + isolate a mechanism. +- **Progressive construction** — parts appear only as their relationship becomes + relevant. + +## Semantic Theme + +`VisualTheme` defines background, text, muted, primary, secondary, accent, +warning, and title/body/label font roles. Choose those values from the approved +visual direction. Reuse the same theme across chapters and pass it to every +domain component. + +Before production: + +- Check contrast at the real delivery size and background. +- Confirm the accent remains legible when context is dimmed. +- Reserve warning for exceptions, failures, or risk. +- Confirm semantic roles do not change meaning later in the video. +- Avoid copying another creator's branding, palette, or typography. + +## Typography + +Typography follows the approved visual direction. Proportional and monospace +fonts are both valid: + +- Use proportional display or text faces when they suit the tone and render + cleanly on the target host. +- Use monospace for code, fixed-width data, or an intentional technical voice. +- Use `MathTex` for mathematical notation that benefits from LaTeX. +- Keep ordinary text at `font_size=18` or larger. +- Define title, body, and label fonts once in `VisualTheme`. +- Fit long labels to their allotted width and run frame-safety checks. +- Preview text-heavy frames at medium quality because low-quality output can hide + spacing and rasterization defects. ```python -# Letter spacing (Pango units: 1/1024 of a point) -MarkupText('HERMES', font_size=18, font="Menlo") - -# Bold specific words -MarkupText('This is important', font_size=24, font="Menlo") - -# Color specific words -MarkupText('Red warning', font_size=24, font="Menlo") +THEME = VisualTheme( + background="#F6F3EC", + text="#1D2430", + muted="#7A8190", + primary="#315C9B", + secondary="#3F8A78", + accent="#D48C24", + warning="#B84A4A", + title_font="Source Serif 4", + body_font="Inter", + label_font="Inter", +) ``` -### Minimum Font Size - -`font_size=18` is the minimum for readable text at any resolution. Below 18, characters become blurry at `-ql` and barely readable even at `-qh`. +## Attention Checklist -## Visual Hierarchy Checklist +For every planned payoff or dense frame: -For every frame: -1. What is the ONE thing to look at? (brightest/largest) -2. What is context? (dimmed to 0.3-0.4) -3. What is structural? (dimmed to 0.15) -4. Enough empty space? (>15%) -5. All text readable at phone size? +1. What is the one relationship to notice? +2. Which persistent objects are context? +3. Should focus use position, scale, camera, opacity, or a highlight? +4. Do all linked representations agree? +5. Is every label readable and inside the safe frame? +6. Is caption space still clear? diff --git a/skills/manim-video/scripts/preview_scene.py b/skills/manim-video/scripts/preview_scene.py new file mode 100755 index 00000000..5079463c --- /dev/null +++ b/skills/manim-video/scripts/preview_scene.py @@ -0,0 +1,385 @@ +#!/usr/bin/env python3 +"""Render selected Manim chapters and build small authoring-review artifacts.""" + +from __future__ import annotations + +import argparse +import ast +import json +import os +import shutil +import subprocess +import sys +import tempfile +from fractions import Fraction +from pathlib import Path +from typing import Any, Callable, Sequence + +from PIL import Image + + +class PreviewError(RuntimeError): + """A clear, user-actionable preview failure.""" + + +Runner = Callable[..., subprocess.CompletedProcess[str]] + + +def scene_classes(script: Path) -> set[str]: + try: + source = script.read_text(encoding="utf-8") + except OSError as exc: + raise PreviewError(f"could not read Manim script {script}: {exc}") from exc + try: + tree = ast.parse(source, filename=str(script)) + except SyntaxError as exc: + raise PreviewError(f"Manim script has invalid Python syntax: {exc}") from exc + return {node.name for node in tree.body if isinstance(node, ast.ClassDef)} + + +def resolve_edit_dir(script: Path, explicit: Path | None = None) -> Path: + script = script.resolve() + if explicit is not None: + edit_dir = explicit.resolve() + if edit_dir.name != "edit": + raise PreviewError("--edit-dir must point to the project's edit directory") + return edit_dir + for candidate in (script.parent, *script.parents): + if candidate.name == "edit": + return candidate + raise PreviewError( + "the Manim script must live under a project edit directory; " + "otherwise pass --edit-dir /path/to/project/edit" + ) + + +def require_executable(value: str, *, purpose: str) -> str: + path = shutil.which(value) + if path is None: + raise PreviewError(f"{purpose} is unavailable: executable {value!r} was not found") + return path + + +def _run( + command: Sequence[str], + *, + runner: Runner, + cwd: Path, + timeout_s: float, + purpose: str, +) -> subprocess.CompletedProcess[str]: + try: + result = runner( + list(command), + cwd=str(cwd), + capture_output=True, + text=True, + timeout=timeout_s, + env={**os.environ, "PYTHONDONTWRITEBYTECODE": "1"}, + ) + except FileNotFoundError as exc: + raise PreviewError(f"{purpose} is unavailable: {exc}") from exc + except subprocess.TimeoutExpired as exc: + raise PreviewError(f"{purpose} timed out after {timeout_s:g} seconds") from exc + if result.returncode != 0: + detail = (result.stderr or result.stdout or "").strip().splitlines() + message = detail[-1] if detail else f"process exited with status {result.returncode}" + raise PreviewError(f"{purpose} failed: {message}") + return result + + +def probe_video( + video: Path, + *, + ffprobe_bin: str, + runner: Runner, + timeout_s: float, +) -> dict[str, Any]: + result = _run( + [ + ffprobe_bin, + "-v", + "error", + "-show_entries", + "format=duration:stream=codec_type,width,height,avg_frame_rate", + "-of", + "json", + str(video), + ], + runner=runner, + cwd=video.parent, + timeout_s=timeout_s, + purpose="ffprobe inspection", + ) + try: + payload = json.loads(result.stdout) + stream = next( + entry for entry in payload["streams"] if entry.get("codec_type") == "video" + ) + duration = float(payload["format"]["duration"]) + frame_rate = float(Fraction(stream["avg_frame_rate"])) + width = int(stream["width"]) + height = int(stream["height"]) + except (KeyError, StopIteration, TypeError, ValueError, ZeroDivisionError, json.JSONDecodeError) as exc: + raise PreviewError(f"ffprobe returned incomplete metadata for {video}") from exc + if duration <= 0 or width <= 0 or height <= 0 or frame_rate <= 0: + raise PreviewError(f"rendered scene has invalid media metadata: {video}") + return { + "duration_s": duration, + "width": width, + "height": height, + "frame_rate": frame_rate, + } + + +def _extract_frame( + video: Path, + output: Path, + timestamp_s: float, + *, + ffmpeg_bin: str, + runner: Runner, + timeout_s: float, +) -> None: + _run( + [ + ffmpeg_bin, + "-hide_banner", + "-loglevel", + "error", + "-y", + "-ss", + f"{max(0.0, timestamp_s):.6f}", + "-i", + str(video), + "-frames:v", + "1", + str(output), + ], + runner=runner, + cwd=output.parent, + timeout_s=timeout_s, + purpose=f"frame extraction at {timestamp_s:.3f}s", + ) + if not output.is_file() or output.stat().st_size == 0: + raise PreviewError(f"expected frame artifact was not produced: {output}") + + +def build_review_frames( + video: Path, + destination: Path, + metadata: dict[str, Any], + *, + ffmpeg_bin: str, + runner: Runner, + timeout_s: float, +) -> dict[str, str]: + duration = float(metadata["duration_s"]) + final_time = max(0.0, duration - 1 / float(metadata["frame_rate"])) + initial = destination / "initial.png" + final = destination / "final.png" + _extract_frame( + video, + initial, + 0.0, + ffmpeg_bin=ffmpeg_bin, + runner=runner, + timeout_s=timeout_s, + ) + _extract_frame( + video, + final, + final_time, + ffmpeg_bin=ffmpeg_bin, + runner=runner, + timeout_s=timeout_s, + ) + + with tempfile.TemporaryDirectory(prefix="contact-", dir=destination) as temp_name: + temp_dir = Path(temp_name) + frame_paths: list[Path] = [] + for index, proportion in enumerate((0.0, 0.25, 0.5, 0.75, 1.0)): + frame = temp_dir / f"frame_{index}.png" + _extract_frame( + video, + frame, + min(final_time, duration * proportion), + ffmpeg_bin=ffmpeg_bin, + runner=runner, + timeout_s=timeout_s, + ) + frame_paths.append(frame) + images = [Image.open(path).convert("RGB") for path in frame_paths] + thumbnail_width = 320 + thumbnail_height = max(1, round(images[0].height * thumbnail_width / images[0].width)) + sheet = Image.new("RGB", (thumbnail_width * 5, thumbnail_height), "black") + for index, frame in enumerate(images): + frame.thumbnail((thumbnail_width, thumbnail_height), Image.Resampling.LANCZOS) + sheet.paste(frame, (index * thumbnail_width, 0)) + contact_sheet = destination / "contact_sheet.png" + sheet.save(contact_sheet) + for frame in images: + frame.close() + + if not contact_sheet.is_file() or contact_sheet.stat().st_size == 0: + raise PreviewError(f"expected contact sheet was not produced: {contact_sheet}") + return { + "initial_frame": str(initial), + "final_frame": str(final), + "contact_sheet": str(contact_sheet), + } + + +def _find_scene_video(media_dir: Path, scene_name: str) -> Path: + matches = [ + path + for path in media_dir.rglob(f"{scene_name}.mp4") + if "partial_movie_files" not in path.parts and "sections" not in path.parts + ] + if not matches: + raise PreviewError(f"Manim reported success but produced no video for scene {scene_name!r}") + return max(matches, key=lambda path: path.stat().st_mtime_ns) + + +def _section_metadata(media_dir: Path, scene_name: str) -> list[dict[str, Any]]: + candidates = [ + path + for path in media_dir.rglob(f"{scene_name}.json") + if "sections" in path.parts + ] + if not candidates: + return [] + source = max(candidates, key=lambda path: path.stat().st_mtime_ns) + try: + data = json.loads(source.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError): + return [{"metadata_file": str(source), "status": "unreadable"}] + sections = data if isinstance(data, list) else data.get("sections", []) + return [dict(section) for section in sections if isinstance(section, dict)] + + +def render_scene( + script: Path, + scene_name: str, + *, + edit_dir: Path | None = None, + manim_bin: str = "manim", + ffmpeg_bin: str = "ffmpeg", + ffprobe_bin: str = "ffprobe", + runner: Runner = subprocess.run, + timeout_s: float = 900.0, +) -> dict[str, Any]: + script = script.resolve() + if not script.is_file(): + raise PreviewError(f"Manim script does not exist: {script}") + if scene_name not in scene_classes(script): + raise PreviewError(f"scene class {scene_name!r} was not found in {script.name}") + resolved_edit = resolve_edit_dir(script, edit_dir) + verify_root = (resolved_edit / "verify").resolve() + destination = verify_root / "manim_previews" / script.stem / scene_name + destination.mkdir(parents=True, exist_ok=True) + if not destination.resolve().is_relative_to(verify_root): + raise PreviewError("refusing to write preview artifacts outside edit/verify") + + manim = require_executable(manim_bin, purpose="Manim") + ffmpeg = require_executable(ffmpeg_bin, purpose="ffmpeg") + ffprobe = require_executable(ffprobe_bin, purpose="ffprobe") + media_dir = destination / "media" + command = [ + manim, + "render", + "-ql", + "--save_sections", + "--media_dir", + str(media_dir), + "--log_dir", + str(destination / "logs"), + "--output_file", + scene_name, + str(script), + scene_name, + ] + _run( + command, + runner=runner, + cwd=script.parent, + timeout_s=timeout_s, + purpose=f"Manim render for {scene_name}", + ) + rendered = _find_scene_video(media_dir, scene_name) + scene_video = destination / f"{scene_name}.mp4" + shutil.copy2(rendered, scene_video) + if not scene_video.is_file() or scene_video.stat().st_size == 0: + raise PreviewError(f"expected scene video was not produced: {scene_video}") + metadata = probe_video( + scene_video, + ffprobe_bin=ffprobe, + runner=runner, + timeout_s=timeout_s, + ) + frames = build_review_frames( + scene_video, + destination, + metadata, + ffmpeg_bin=ffmpeg, + runner=runner, + timeout_s=timeout_s, + ) + report = { + "scene": scene_name, + "status": "ok", + "video": str(scene_video), + **frames, + **metadata, + "sections": _section_metadata(media_dir, scene_name), + } + report_path = destination / "report.json" + report["report"] = str(report_path) + report_path.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8") + return report + + +def preview_scenes( + script: Path, + scene_names: Sequence[str], + **kwargs: Any, +) -> dict[str, Any]: + if not scene_names: + raise PreviewError("provide at least one scene class name") + reports = [] + for scene_name in scene_names: + reports.append(render_scene(script, scene_name, **kwargs)) + return {"status": "ok", "script": str(script.resolve()), "scenes": reports} + + +def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Render selected Manim scene classes into edit/verify review artifacts." + ) + parser.add_argument("script", type=Path, help="Path to the authored Manim Python script") + parser.add_argument("scenes", nargs="+", help="One or more scene class names") + parser.add_argument("--edit-dir", type=Path, help="Explicit project edit directory") + parser.add_argument("--manim-bin", default="manim", help="Manim executable name or path") + parser.add_argument("--timeout-s", type=float, default=900.0, help="Per-command timeout") + return parser.parse_args(argv) + + +def main(argv: Sequence[str] | None = None) -> int: + args = parse_args(argv) + try: + report = preview_scenes( + args.script, + args.scenes, + edit_dir=args.edit_dir, + manim_bin=args.manim_bin, + timeout_s=args.timeout_s, + ) + except PreviewError as exc: + print(f"preview failed: {exc}", file=sys.stderr) + return 1 + print(json.dumps(report, indent=2, sort_keys=True)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/fixtures/manim_domain_gallery.py b/tests/fixtures/manim_domain_gallery.py new file mode 100644 index 00000000..84724ca7 --- /dev/null +++ b/tests/fixtures/manim_domain_gallery.py @@ -0,0 +1,101 @@ +from __future__ import annotations + +import sys +from pathlib import Path + +from manim import Circle, Cube, Dot, Scene, Sphere, Square + + +ASSETS = Path(__file__).parents[2] / "skills" / "manim-video" / "assets" +sys.path.insert(0, str(ASSETS)) + +from domains import ( # noqa: E402 + ArrayModel, + BodySystem, + CashFlow, + CellProcess, + NumberLineModel, + RequestFlow, +) +from teaching import ( # noqa: E402 + SemanticMobject, + TeachingScene, + TeachingThreeDScene, + VisualTheme, +) + + +THEME = VisualTheme() + + +class MathGallery(Scene): + def construct(self): + self.camera.background_color = THEME.background + self.add(NumberLineModel(THEME, label="math")) + self.wait(0.1) + + +class ComputingGallery(Scene): + def construct(self): + self.camera.background_color = THEME.background + self.add(ArrayModel(THEME, label="algorithms and AI")) + self.wait(0.1) + + +class SystemsGallery(Scene): + def construct(self): + self.camera.background_color = THEME.background + self.add(RequestFlow(THEME, label="software systems")) + self.wait(0.1) + + +class PhysicsGallery(Scene): + def construct(self): + self.camera.background_color = THEME.background + self.add(BodySystem(THEME, label="physics")) + self.wait(0.1) + + +class BiologyGallery(Scene): + def construct(self): + self.camera.background_color = THEME.background + self.add(CellProcess(THEME, label="biology")) + self.wait(0.1) + + +class FinanceGallery(Scene): + def construct(self): + self.camera.background_color = THEME.background + self.add(CashFlow(THEME, label="finance and business")) + self.wait(0.1) + + +class TeachingAPIGallery(TeachingScene): + def construct(self): + self.theme = THEME + self.camera.background_color = THEME.background + focus = self.remember("focus", Dot(color=THEME.primary).shift([-1, 0, 0])) + self.remember("context", Circle(color=THEME.secondary).shift([1, 0, 0])) + self.begin_beat("focus") + self.focus_on(focus, run_time=0.1) + self.restore_context(run_time=0.1) + self.highlight("focus", run_time=0.1) + self.restore_highlight(run_time=0.1) + source = SemanticMobject(THEME) + source.register_part("dot", Dot(color=THEME.primary)) + self.remember("semantic", source) + target = SemanticMobject(THEME) + target.register_part("square", Square(color=THEME.accent)) + self.transform_object("semantic", target, run_time=0.1) + self.hold(0.1, purpose="inspect transformed semantic object") + + +class TeachingThreeDGallery(TeachingThreeDScene): + def construct(self): + self.camera.background_color = THEME.background + focus = self.remember("focus", Sphere(radius=0.25).shift([-0.8, 0, 0])) + self.remember("context", Cube(side_length=0.5).shift([0.8, 0, 0])) + self.begin_beat("three dimensional focus") + self.focus_on(focus, run_time=0.1) + self.restore_context(run_time=0.1) + self.hold(0.1, purpose="inspect restored three-dimensional context") diff --git a/tests/test_manim_domains.py b/tests/test_manim_domains.py new file mode 100644 index 00000000..6121600f --- /dev/null +++ b/tests/test_manim_domains.py @@ -0,0 +1,66 @@ +from __future__ import annotations + +import sys +from pathlib import Path + +import pytest + + +manim = pytest.importorskip("manim") + +ASSETS = Path(__file__).parents[1] / "skills" / "manim-video" / "assets" +sys.path.insert(0, str(ASSETS)) + +import domains # noqa: E402 +from teaching import SemanticMobject, VisualTheme # noqa: E402 + + +COMPONENTS = [getattr(domains, name) for name in domains.__all__] +LABELS = ["", "short", "A label long enough to exercise component width fitting and frame safety"] + + +@pytest.mark.parametrize("component", COMPONENTS, ids=lambda component: component.__name__) +@pytest.mark.parametrize("label", LABELS, ids=["empty", "short", "long"]) +def test_domain_components_are_semantic_and_frame_safe(component: type, label: str) -> None: + model = component(VisualTheme(), label=label) + + assert isinstance(model, SemanticMobject) + assert model.part_names + assert model.width <= float(manim.config.frame_width) - 0.9 + 1e-6 + assert model.height <= float(manim.config.frame_height) - 0.9 + 1e-6 + + +def test_domain_components_require_a_shared_theme() -> None: + for component in COMPONENTS: + with pytest.raises(TypeError, match="VisualTheme"): + component(None) + + +def test_domain_actions_update_semantic_state() -> None: + theme = VisualTheme() + + mass = domains.ProbabilityMass(theme) + mass.transfer_probability(1, 0, 0.1) + assert mass.probabilities == pytest.approx([0.35, 0.4, 0.25]) + + graph = domains.GraphModel(theme) + graph.visit_node("A") + assert "A" in graph.visited + + queue = domains.QueueModel(theme, items=[]) + queue.enqueue("request") + request, _animation = queue.dequeue() + assert request == "request" + + wave = domains.WaveField(theme) + wave.propagate(0.5) + assert wave.phase == pytest.approx(0.5) + + sequence = domains.SequenceProcess(theme, "ATG") + sequence.transcribe() + assert sequence.rna == "UAC" + + timeline = domains.CompoundTimeline(theme, principal=100, rate=0.1, periods=3) + timeline.compound(2) + assert timeline.balance_at(2) == pytest.approx(121) + diff --git a/tests/test_manim_gallery.py b/tests/test_manim_gallery.py new file mode 100644 index 00000000..01d90a48 --- /dev/null +++ b/tests/test_manim_gallery.py @@ -0,0 +1,40 @@ +from __future__ import annotations + +import shutil +import subprocess +from pathlib import Path + +import pytest + + +pytest.importorskip("manim") + + +def test_each_domain_family_renders_a_low_quality_gallery(tmp_path: Path) -> None: + manim = shutil.which("manim") + if manim is None: + pytest.skip("Manim executable is not installed") + script = Path(__file__).parent / "fixtures" / "manim_domain_gallery.py" + scenes = [ + "MathGallery", + "ComputingGallery", + "SystemsGallery", + "PhysicsGallery", + "BiologyGallery", + "FinanceGallery", + "TeachingAPIGallery", + "TeachingThreeDGallery", + ] + result = subprocess.run( + [manim, "render", "-ql", "--media_dir", str(tmp_path), str(script), *scenes], + capture_output=True, + text=True, + timeout=240, + ) + + assert result.returncode == 0, result.stderr + for scene in scenes: + assert any( + "partial_movie_files" not in path.parts + for path in tmp_path.rglob(f"{scene}.mp4") + ) diff --git a/tests/test_manim_teaching.py b/tests/test_manim_teaching.py new file mode 100644 index 00000000..81e5acb8 --- /dev/null +++ b/tests/test_manim_teaching.py @@ -0,0 +1,149 @@ +from __future__ import annotations + +import sys +from pathlib import Path + +import numpy as np +import pytest + + +manim = pytest.importorskip("manim") + +ASSETS = Path(__file__).parents[1] / "skills" / "manim-video" / "assets" +sys.path.insert(0, str(ASSETS)) + +from teaching import ( # noqa: E402 + LinkedValue, + SemanticMobject, + TeachingScene, + TeachingThreeDScene, + VisualTheme, +) + + +@pytest.fixture +def theme() -> VisualTheme: + return VisualTheme() + + +def test_semantic_part_and_anchor_registration(theme: VisualTheme) -> None: + model = SemanticMobject(theme) + dot = manim.Dot([1, 2, 0]) + + assert model.register_part("value", dot) is dot + model.register_anchor("value_center", dot) + + assert model.part("value") is dot + np.testing.assert_allclose(model.anchor("value_center"), [1, 2, 0]) + assert model.part_names == ("value",) + assert model.anchor_names == ("value_center",) + + +def test_semantic_names_reject_duplicates_and_unknowns(theme: VisualTheme) -> None: + model = SemanticMobject(theme) + model.register_part("value", manim.Dot()) + model.register_anchor("center", [0, 0]) + + with pytest.raises(ValueError, match="already registered"): + model.register_part("value", manim.Dot()) + with pytest.raises(ValueError, match="already registered"): + model.register_anchor("center", [1, 0, 0]) + with pytest.raises(KeyError, match="unknown part"): + model.part("missing") + with pytest.raises(KeyError, match="unknown anchor"): + model.anchor("missing") + + +def test_focus_dims_unrelated_objects_and_restores_exact_opacity(theme: VisualTheme) -> None: + scene = TeachingScene() + scene.theme = theme + focus = manim.Dot(fill_opacity=0.83, stroke_opacity=0.61) + context = manim.Circle(fill_opacity=0.27, stroke_opacity=0.74).shift(manim.RIGHT * 2) + scene.remember("focus", focus) + scene.remember("context", context) + before_fill = np.asarray(context.get_fill_opacity()).copy() + before_stroke = np.asarray(context.get_stroke_opacity()).copy() + + scene.focus_on("focus", dim_opacity=0.11, animate=False) + + np.testing.assert_allclose(context.get_fill_opacity(), 0.11) + np.testing.assert_allclose(context.get_stroke_opacity(), 0.11) + scene.restore_context(animate=False) + np.testing.assert_allclose(context.get_fill_opacity(), before_fill) + np.testing.assert_allclose(context.get_stroke_opacity(), before_stroke) + + +def test_transform_preserves_registry_identity_and_adopts_semantics(theme: VisualTheme) -> None: + scene = TeachingScene() + source = SemanticMobject(theme) + source.register_part("dot", manim.Dot()) + target = SemanticMobject(theme) + square = manim.Square() + target.register_part("square", square) + target.register_anchor("center", square) + scene.remember("model", source) + original_id = id(source) + + transformed = scene.transform_object("model", target, animate=False) + + assert id(transformed) == original_id + assert scene.recall("model") is source + assert transformed.part("square") in transformed.get_family() + np.testing.assert_allclose(transformed.anchor("center"), [0, 0, 0]) + + +def test_linked_value_updates_all_representations_at_each_state(theme: VisualTheme) -> None: + linked = LinkedValue(0) + dot = manim.Dot() + bar = manim.Rectangle(width=0.2, height=1) + seen: list[tuple[str, float]] = [] + + linked.register( + "dot", + dot, + lambda mobject, value: ( + seen.append(("dot", value)), + mobject.move_to([value, 0, 0]), + )[1], + ) + linked.register( + "bar", + bar, + lambda mobject, value: ( + seen.append(("bar", value)), + mobject.stretch_to_fit_width(max(0.1, value + 0.1)), + )[1], + ) + + for value in (0.0, 0.5, 1.0): + linked.set_value(value) + assert dot.get_center()[0] == pytest.approx(value) + assert {name for name, recorded in seen[-2:] if recorded == value} == {"dot", "bar"} + + +def test_linked_value_suspend_resume_and_clear() -> None: + linked = LinkedValue(0) + dot = manim.Dot() + linked.register("dot", dot, lambda mobject, value: mobject.move_to([value, 0, 0])) + + linked.suspend().set_value(2) + assert dot.get_center()[0] == pytest.approx(0) + linked.resume() + assert dot.get_center()[0] == pytest.approx(2) + linked.clear("dot").set_value(4) + assert dot.get_center()[0] == pytest.approx(2) + + +def test_three_d_scene_supports_registry_and_immediate_focus(theme: VisualTheme) -> None: + scene = TeachingThreeDScene() + focus = manim.Sphere(radius=0.25) + context = manim.Cube(side_length=0.5).shift(manim.RIGHT) + scene.remember("focus", focus) + scene.remember("context", context) + original_center = np.asarray(scene.camera.frame_center).copy() + + scene.focus_on("focus", animate=False) + scene.restore_context(animate=False) + + assert scene.recall("focus") is focus + np.testing.assert_allclose(scene.camera.frame_center, original_center) diff --git a/tests/test_preview_scene.py b/tests/test_preview_scene.py new file mode 100644 index 00000000..82221535 --- /dev/null +++ b/tests/test_preview_scene.py @@ -0,0 +1,110 @@ +from __future__ import annotations + +import importlib.util +import shutil +import subprocess +from pathlib import Path + +import pytest + + +SCRIPT_PATH = ( + Path(__file__).parents[1] + / "skills" + / "manim-video" + / "scripts" + / "preview_scene.py" +) +SPEC = importlib.util.spec_from_file_location("preview_scene", SCRIPT_PATH) +assert SPEC is not None and SPEC.loader is not None +preview_scene = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(preview_scene) + + +def _scene_script(path: Path, name: str = "Demo") -> Path: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text( + "from manim import *\n" + f"class {name}(Scene):\n" + " def construct(self):\n" + " self.next_section('payoff')\n" + " self.add(Dot())\n" + " self.wait(0.2)\n", + encoding="utf-8", + ) + return path + + +def test_preview_rejects_missing_scene_before_render(tmp_path: Path) -> None: + script = _scene_script(tmp_path / "edit" / "scene.py") + + with pytest.raises(preview_scene.PreviewError, match="was not found"): + preview_scene.render_scene(script, "Missing") + + +def test_preview_fails_clearly_when_manim_is_absent(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + script = _scene_script(tmp_path / "edit" / "scene.py") + monkeypatch.setattr(preview_scene.shutil, "which", lambda _name: None) + + with pytest.raises(preview_scene.PreviewError, match="Manim is unavailable"): + preview_scene.render_scene(script, "Demo") + + +def test_preview_reports_render_failure(tmp_path: Path) -> None: + script = _scene_script(tmp_path / "edit" / "scene.py") + + def failed_runner(*_args, **_kwargs): + return subprocess.CompletedProcess([], 2, stdout="", stderr="render exploded") + + with pytest.raises(preview_scene.PreviewError, match="render exploded"): + preview_scene.render_scene( + script, + "Demo", + manim_bin=shutil.which("true") or "/usr/bin/true", + ffmpeg_bin=shutil.which("true") or "/usr/bin/true", + ffprobe_bin=shutil.which("true") or "/usr/bin/true", + runner=failed_runner, + ) + + +def test_preview_rejects_success_without_expected_artifacts(tmp_path: Path) -> None: + script = _scene_script(tmp_path / "edit" / "scene.py") + + def successful_runner(*_args, **_kwargs): + return subprocess.CompletedProcess([], 0, stdout="", stderr="") + + with pytest.raises(preview_scene.PreviewError, match="produced no video"): + preview_scene.render_scene( + script, + "Demo", + manim_bin=shutil.which("true") or "/usr/bin/true", + ffmpeg_bin=shutil.which("true") or "/usr/bin/true", + ffprobe_bin=shutil.which("true") or "/usr/bin/true", + runner=successful_runner, + ) + + +def test_preview_real_manim_render_stays_under_edit_verify(tmp_path: Path) -> None: + pytest.importorskip("manim") + manim = shutil.which("manim") + if manim is None: + pytest.skip("Manim executable is not installed") + edit_dir = tmp_path / "project" / "edit" + script = _scene_script(edit_dir / "animations" / "scene.py", "PreviewSmoke") + + report = preview_scene.render_scene( + script, + "PreviewSmoke", + manim_bin=manim, + timeout_s=180, + ) + + assert report["status"] == "ok" + assert report["width"] == 854 + assert report["height"] == 480 + assert report["frame_rate"] == pytest.approx(15) + assert report["sections"] + for key in ("video", "initial_frame", "final_frame", "contact_sheet", "report"): + artifact = Path(report[key]).resolve() + assert artifact.is_file() + assert artifact.is_relative_to((edit_dir / "verify").resolve()) From d3762011d4824552eac840bb8136db5e6f4179ea Mon Sep 17 00:00:00 2001 From: Ismael Date: Tue, 1 Sep 2026 22:38:01 -0700 Subject: [PATCH 11/19] add plain language comments to manim teaching assets and scripts --- skills/manim-video/assets/__init__.py | 3 + .../manim-video/assets/concept_explainer.py | 20 + skills/manim-video/assets/domains/__init__.py | 3 + skills/manim-video/assets/domains/_common.py | 18 + skills/manim-video/assets/domains/biology.py | 17 + .../manim-video/assets/domains/computing.py | 25 + skills/manim-video/assets/domains/finance.py | 28 + skills/manim-video/assets/domains/math.py | 21 + skills/manim-video/assets/domains/physics.py | 23 + skills/manim-video/assets/domains/systems.py | 20 + skills/manim-video/assets/teaching.py | 76 + skills/manim-video/scripts/preview_scene.py | 23 + skills/manim-video/scripts/setup.sh | 8 + tests/fixtures/manim_domain_gallery.py | 19 + tests/test_manim_domains.py | 6 + tests/test_manim_gallery.py | 4 + tests/test_manim_teaching.py | 11 + tests/test_preview_scene.py | 12 + uv.lock | 2751 +++++++++++++++++ 19 files changed, 3088 insertions(+) create mode 100644 uv.lock diff --git a/skills/manim-video/assets/__init__.py b/skills/manim-video/assets/__init__.py index d8df1d77..6bc5ff2a 100644 --- a/skills/manim-video/assets/__init__.py +++ b/skills/manim-video/assets/__init__.py @@ -1,3 +1,6 @@ +# public entry point for the reusable manim explainer assets +# it re exports the teaching primitives so scripts can import them from one place + """Reusable style-neutral Manim explainer assets.""" from .teaching import ( diff --git a/skills/manim-video/assets/concept_explainer.py b/skills/manim-video/assets/concept_explainer.py index c4dca82c..4ff13b46 100644 --- a/skills/manim-video/assets/concept_explainer.py +++ b/skills/manim-video/assets/concept_explainer.py @@ -1,3 +1,7 @@ +# layout guards for narrated 16 by 9 manim explainers that share the frame with captions +# it defines split composition regions and helpers that fit text and panels and assert nothing leaves its bounds +# a script imports these so authored visuals stay above the caption rail before rendering + """Small layout guards for narrated 16:9 Manim explainers. Import these helpers into a project's ``script.py``. They keep authored visuals @@ -25,6 +29,7 @@ } +# turn a top left normalized rectangle into an invisible manim guide centered in frame space def normalized_region(rect: dict[str, float]) -> Rectangle: """Convert a top-left normalized video rectangle into a Manim guide.""" try: @@ -41,12 +46,14 @@ def normalized_region(rect: dict[str, float]) -> Rectangle: stroke_opacity=0, fill_opacity=0, ) + # convert normalized top left origin into manim center coordinates where y grows upward center_x = (x + width / 2 - 0.5) * config.frame_width center_y = (0.5 - y - height / 2) * config.frame_height guide.move_to((center_x, center_y, 0)) return guide +# build the media and illustration guides for a named split layout def composition_regions(mode: str) -> dict[str, Rectangle]: """Return non-overlapping media/illustration guides for a split scene.""" if mode not in COMPOSITION_REGIONS: @@ -58,6 +65,7 @@ def composition_regions(mode: str) -> dict[str, Rectangle]: } +# scale a mobject down but never up so it fits the given box def fit_mobject(mobject: Mobject, max_width: float, max_height: float) -> Mobject: """Scale down, never up, until a mobject fits the requested box.""" if max_width <= 0 or max_height <= 0: @@ -67,6 +75,7 @@ def fit_mobject(mobject: Mobject, max_width: float, max_height: float) -> Mobjec return mobject +# create text at the requested size and shrink it to fit the box if needed def fitted_text( text: str, *, @@ -84,6 +93,7 @@ def fitted_text( ) +# wrap content in a rounded panel and shrink the content to fit inside the padding def safe_panel( content: Mobject, *, @@ -111,6 +121,7 @@ def safe_panel( return VGroup(panel, content) +# create an invisible guide for the drawable area above the caption rail def content_frame( *, caption_rail_fraction: float = CAPTION_RAIL_FRACTION, @@ -126,10 +137,12 @@ def content_frame( stroke_opacity=0, fill_opacity=0, ) + # shift the guide up by half the rail so it sits centered in the remaining area guide.shift(UP * rail_height / 2) return guide +# raise before render if a mobject leaves the frame or enters the caption rail def assert_inside_frame( mobject: Mobject, *, @@ -138,6 +151,7 @@ def assert_inside_frame( margin: float = 0.18, ) -> Mobject: """Raise before render if a mobject leaves the frame or caption-safe area.""" + # compute the allowed bounds with the caption rail folded into the bottom edge left = -config.frame_width / 2 + margin right = config.frame_width / 2 - margin top = config.frame_height / 2 - margin @@ -153,6 +167,7 @@ def assert_inside_frame( return mobject +# raise when a mobject leaves its assigned composition region by more than the margin def assert_inside_region( mobject: Mobject, region: Rectangle, @@ -170,10 +185,12 @@ def assert_inside_region( return mobject +# describe a final state mobject as a normalized rectangle the edl can protect def normalized_bounds(mobject: Mobject, *, padding: float = 0.0) -> dict[str, float]: """Return an EDL-ready protected rectangle for a final-state mobject.""" if padding < 0: raise ValueError("padding must be non-negative") + # clamp the padded bounds to the frame then convert to top left normalized units left = max(-config.frame_width / 2, mobject.get_left()[0] - padding) right = min(config.frame_width / 2, mobject.get_right()[0] + padding) top = min(config.frame_height / 2, mobject.get_top()[1] + padding) @@ -186,6 +203,7 @@ def normalized_bounds(mobject: Mobject, *, padding: float = 0.0) -> dict[str, fl } +# report whether two axis aligned bounds overlap once a gap is added def mobjects_overlap(first: Mobject, second: Mobject, *, gap: float = 0.05) -> bool: """Return whether two axis-aligned Manim bounds overlap, including a gap.""" return not ( @@ -196,6 +214,7 @@ def mobjects_overlap(first: Mobject, second: Mobject, *, gap: float = 0.05) -> b ) +# raise when any pair of final state mobjects overlap def assert_no_overlap(*mobjects: Mobject, gap: float = 0.05) -> None: """Raise when any pair of final-state mobjects overlap.""" for first, second in combinations(mobjects, 2): @@ -203,6 +222,7 @@ def assert_no_overlap(*mobjects: Mobject, gap: float = 0.05) -> None: raise ValueError("final-state mobjects overlap; resize or reposition them") +# create a small source note that sits just above the caption rail def source_footer( text: str, *, diff --git a/skills/manim-video/assets/domains/__init__.py b/skills/manim-video/assets/domains/__init__.py index 3e439595..2146ef3e 100644 --- a/skills/manim-video/assets/domains/__init__.py +++ b/skills/manim-video/assets/domains/__init__.py @@ -1,3 +1,6 @@ +# package entry point for the semantic domain component kit +# it gathers every domain class from biology computing finance math physics and systems into one namespace + """Broad semantic component kit for original Manim explainers.""" from .biology import CellProcess, PopulationFlow, SequenceProcess diff --git a/skills/manim-video/assets/domains/_common.py b/skills/manim-video/assets/domains/_common.py index 42905065..1498f318 100644 --- a/skills/manim-video/assets/domains/_common.py +++ b/skills/manim-video/assets/domains/_common.py @@ -1,3 +1,7 @@ +# shared construction helpers for the semantic domain kit +# it builds labeled boxes circle nodes arrows and rows and validates indices amounts and themes +# every domain module imports from here so the components share one look + """Shared construction helpers for the style-neutral semantic domain kit.""" from __future__ import annotations @@ -22,17 +26,20 @@ from teaching import VisualTheme, fit_to_frame, theme_text +# raise unless the value is a real VisualTheme def require_theme(theme: VisualTheme) -> VisualTheme: if not isinstance(theme, VisualTheme): raise TypeError("domain components require a shared VisualTheme") return theme +# turn labels into strings or fall back to defaults and never return an empty list def normalized_labels(labels: Iterable[object] | None, *, fallback: Sequence[str]) -> list[str]: values = [str(value) for value in labels] if labels is not None else list(fallback) return values or [""] +# make a themed label that shrinks to a max width def label_for( value: object, theme: VisualTheme, @@ -51,6 +58,7 @@ def label_for( ) +# make a rounded box with a centered label that fits inside it def labeled_box( label: object, theme: VisualTheme, @@ -75,6 +83,7 @@ def labeled_box( return VGroup(box, text) +# make a circle with a centered label for graph style nodes def circle_node( label: object, theme: VisualTheme, @@ -94,6 +103,7 @@ def circle_node( return VGroup(circle, text) +# draw an arrow between two mobjects or two points def arrow_between( start: Mobject | Sequence[float], end: Mobject | Sequence[float], @@ -114,6 +124,7 @@ def arrow_between( ) +# build a horizontal row of labeled boxes from labels or fallbacks def row_of_boxes( labels: Iterable[object] | None, theme: VisualTheme, @@ -140,6 +151,7 @@ def row_of_boxes( return boxes +# draw a short arrow from each box in a row to the next one def arrows_for_row(nodes: VGroup, theme: VisualTheme, *, color: str | None = None) -> VGroup: return VGroup( *[ @@ -156,22 +168,26 @@ def arrows_for_row(nodes: VGroup, theme: VisualTheme, *, color: str | None = Non ) +# place a themed title just above a body mobject and no wider than it def title_above(label: object, body: Mobject, theme: VisualTheme) -> Mobject: title = theme_text(label, theme, role="body", font_size=26, max_width=body.width) title.next_to(body, UP, buff=0.28) return title +# shrink a component so it fits inside the frame with a standard margin def frame_safe(component: Mobject) -> Mobject: return fit_to_frame(component, margin=0.45) +# validate that an index falls inside zero to size minus one def ensure_index(index: int, size: int, *, name: str = "index") -> int: if not 0 <= int(index) < size: raise IndexError(f"{name} {index} is outside 0..{size - 1}") return int(index) +# validate a non negative amount and optionally that it does not exceed what is available def ensure_amount(amount: float, *, available: float | None = None) -> float: value = float(amount) if value < 0: @@ -181,6 +197,7 @@ def ensure_amount(amount: float, *, available: float | None = None) -> float: return value +# build two stacked rows of boxes with vertical arrows linking matching columns def two_row_flow( top_labels: Iterable[object], bottom_labels: Iterable[object], @@ -188,6 +205,7 @@ def two_row_flow( ) -> tuple[VGroup, VGroup, VGroup]: top = row_of_boxes(top_labels, theme, fallback=[""], color=theme.primary) bottom = row_of_boxes(bottom_labels, theme, fallback=[""], color=theme.secondary) + # stretch the narrower row so both rows share the same width before stacking them width = max(top.width, bottom.width) if top.width: top.scale_to_fit_width(width) diff --git a/skills/manim-video/assets/domains/biology.py b/skills/manim-video/assets/domains/biology.py index 4eea6c30..9e806825 100644 --- a/skills/manim-video/assets/domains/biology.py +++ b/skills/manim-video/assets/domains/biology.py @@ -1,3 +1,6 @@ +# semantic components for cellular sequence and population processes +# it defines CellProcess SequenceProcess and PopulationFlow built on SemanticMobject + """Semantic components for cellular, sequence, and population processes.""" from __future__ import annotations @@ -32,7 +35,9 @@ ) +# a cell membrane with a row of stages and a cargo dot that advances between them class CellProcess(SemanticMobject): + # lay out the membrane the stage boxes the links and the cargo and register them as parts def __init__( self, theme: VisualTheme, @@ -55,6 +60,7 @@ def __init__( for stage in self.stages ] ).arrange(RIGHT, buff=0.34) + # keep the stage row narrower than the membrane if nodes.width > 4.1: nodes.scale_to_fit_width(4.1) links = arrows_for_row(nodes, theme, color=theme.accent) @@ -70,6 +76,7 @@ def __init__( self.register_anchor(f"stage_{index}", stage) frame_safe(self) + # move the cargo to the next stage or an explicit destination and return the animation def advance(self, destination: int | None = None) -> Any: target = self.stage + 1 if destination is None else int(destination) target = ensure_index(target, len(self.stages), name="cell-process destination") @@ -79,9 +86,11 @@ def advance(self, destination: int | None = None) -> Any: ) +# three rows showing dna transcribed to rna and translated to protein class SequenceProcess(SemanticMobject): _TRANSCRIPTION = {"A": "U", "T": "A", "C": "G", "G": "C", "U": "A"} + # validate the dna bases and build the labeled rows for dna rna and protein def __init__( self, theme: VisualTheme, @@ -116,6 +125,7 @@ def __init__( self.register_part("label", caption) frame_safe(self) + # build a row of small base boxes in the given color def _row(self, sequence: str | Sequence[str], *, color: str) -> VGroup: values = list(sequence) or [""] return VGroup( @@ -125,21 +135,26 @@ def _row(self, sequence: str | Sequence[str], *, color: str) -> VGroup: ] ).arrange(RIGHT, buff=0.07) + # fill the rna row with the complement of each dna base def transcribe(self) -> Transform: self.rna = "".join(self._TRANSCRIPTION[base] for base in self.dna) replacement = self._row(self.rna, color=self.theme.secondary).move_to(self.part("rna")) return Transform(self.part("rna"), replacement) + # group the rna into codons and fill the protein row with one placeholder per full codon def translate(self) -> Transform: if not self.rna: raise RuntimeError("transcribe the sequence before translating it") + # split into three base codons and drop any trailing partial codon codons = [self.rna[index:index + 3] for index in range(0, len(self.rna), 3)] self.protein = [f"aa{index + 1}" for index, codon in enumerate(codons) if len(codon) == 3] replacement = self._row(self.protein, color=self.theme.accent).move_to(self.part("protein")) return Transform(self.part("protein"), replacement) +# labeled compartments holding population counts with transfers between them class PopulationFlow(SemanticMobject): + # read the population mapping and build the compartments and links def __init__( self, theme: VisualTheme, @@ -166,6 +181,7 @@ def __init__( self.register_anchor(f"population_{index}", compartment) frame_safe(self) + # build one box per population showing its name and current count def _compartments(self) -> VGroup: return VGroup( *[ @@ -181,6 +197,7 @@ def _compartments(self) -> VGroup: ] ).arrange(RIGHT, buff=0.65) + # move an amount from one compartment to another and return the transform that redraws them def transfer(self, source: int, target: int, amount: float) -> Transform: source_index = ensure_index(source, len(self.counts), name="source population") target_index = ensure_index(target, len(self.counts), name="target population") diff --git a/skills/manim-video/assets/domains/computing.py b/skills/manim-video/assets/domains/computing.py index 1ac4b64d..5ff42538 100644 --- a/skills/manim-video/assets/domains/computing.py +++ b/skills/manim-video/assets/domains/computing.py @@ -1,3 +1,6 @@ +# semantic components for algorithms machine learning and ai +# it defines ArrayModel GraphModel StateMachine TokenFlow and NeuralLayer built on SemanticMobject + """Semantic components for algorithms, machine learning, and AI.""" from __future__ import annotations @@ -33,7 +36,9 @@ ) +# a row of value cells with index labels that supports highlighting swapping and setting class ArrayModel(SemanticMobject): + # build the cells and the index labels under them and register anchors per index def __init__( self, theme: VisualTheme, @@ -57,6 +62,7 @@ def __init__( self.register_anchor(f"index_{index}", cell) frame_safe(self) + # build one labeled box per value in a tight row def _make_cells(self) -> VGroup: cells = VGroup( *[ @@ -66,16 +72,19 @@ def _make_cells(self) -> VGroup: ).arrange(RIGHT, buff=0.1) return cells + # return an animation that recolors one cell with the accent color def highlight_index(self, index: int) -> Any: selected = ensure_index(index, len(self.values)) return self.part("cells")[selected].animate.set_color(self.theme.accent) + # swap two values and return an animation that slides their cells past each other def swap(self, first: int, second: int) -> AnimationGroup: left = ensure_index(first, len(self.values), name="first index") right = ensure_index(second, len(self.values), name="second index") if left == right: raise ValueError("swap indices must differ") cells = self.part("cells") + # copy the centers before the swap so both moves use the original positions left_position = cells[left].get_center().copy() right_position = cells[right].get_center().copy() self.values[left], self.values[right] = self.values[right], self.values[left] @@ -85,6 +94,7 @@ def swap(self, first: int, second: int) -> AnimationGroup: lag_ratio=0, ) + # replace one value and return a transform into a fresh cell in the secondary color def set_value(self, index: int, value: object) -> Transform: selected = ensure_index(index, len(self.values)) self.values[selected] = str(value) @@ -98,7 +108,9 @@ def set_value(self, index: int, value: object) -> Transform: return Transform(self.part("cells")[selected], replacement) +# nodes arranged on a circle with directed edges between them class GraphModel(SemanticMobject): + # validate node names and edges then place the nodes on a circle and draw the edges def __init__( self, theme: VisualTheme, @@ -114,6 +126,7 @@ def __init__( raise ValueError("graph node labels must be unique") default_edges = list(zip(self.node_names, self.node_names[1:])) self.edges = [(str(start), str(end)) for start, end in (edges if edges is not None else default_edges)] + # space the nodes evenly around a circle whose radius grows with the node count count = len(self.node_names) radius = min(2.25, 0.7 + count * 0.25) node_group = VGroup() @@ -147,6 +160,7 @@ def __init__( self.register_anchor(f"node_{name}", node) frame_safe(self) + # mark a node as visited and return an animation that recolors it def visit_node(self, name: object) -> Any: key = str(name) if key not in self.node_index: @@ -154,6 +168,7 @@ def visit_node(self, name: object) -> Any: self.visited.add(key) return self.part("nodes")[self.node_index[key]].animate.set_color(self.theme.accent) + # return an animation that emphasizes one directed edge def traverse(self, start: object, end: object) -> Any: edge = (str(start), str(end)) try: @@ -163,7 +178,9 @@ def traverse(self, start: object, end: object) -> Any: return self.part("edges")[index].animate.set_color(self.theme.secondary).set_stroke(width=4) +# a row of states with transition arrows and a marker on the current state class StateMachine(SemanticMobject): + # validate states and transitions then build the nodes arrows and current marker def __init__( self, theme: VisualTheme, @@ -215,6 +232,7 @@ def __init__( self.register_part("label", caption) frame_safe(self) + # follow a valid transition and return a transform that moves the marker def transition_to(self, state: object) -> Transform: destination = str(state) if destination not in self.state_index: @@ -231,7 +249,9 @@ def transition_to(self, state: object) -> Transform: return Transform(self.part("current"), marker) +# a row of tokens with curved attention links drawn on demand class TokenFlow(SemanticMobject): + # build the token boxes and an empty link group def __init__( self, theme: VisualTheme, @@ -253,6 +273,7 @@ def __init__( self.register_anchor(f"token_{index}", token) frame_safe(self) + # validate attention weights and return a transform that draws links weighted by attention def mix_context(self, target: int, weights: Sequence[float]) -> Transform: target_index = ensure_index(target, len(self.tokens), name="target token") if len(weights) != len(self.tokens): @@ -262,6 +283,7 @@ def mix_context(self, target: int, weights: Sequence[float]) -> Transform: raise ValueError("attention weights must be non-negative and sum to one") tokens = self.part("tokens") links = VGroup() + # draw a curved arrow from each other token whose weight is positive with thickness and opacity from the weight for index, weight in enumerate(values): if index == target_index or weight <= 0: continue @@ -279,7 +301,9 @@ def mix_context(self, target: int, weights: Sequence[float]) -> Transform: return Transform(self.part("context_links"), links) +# a vertical column of neurons whose color and opacity follow their activation class NeuralLayer(SemanticMobject): + # build the neuron circles with optional labels def __init__( self, theme: VisualTheme, @@ -305,6 +329,7 @@ def __init__( self.register_anchor(f"neuron_{index}", neuron) frame_safe(self) + # validate activations and return a staggered animation that recolors each neuron def activate(self, values: Sequence[float]) -> AnimationGroup: if len(values) != len(self.activations): raise ValueError("activation values must match layer size") diff --git a/skills/manim-video/assets/domains/finance.py b/skills/manim-video/assets/domains/finance.py index 6f83b108..d992c971 100644 --- a/skills/manim-video/assets/domains/finance.py +++ b/skills/manim-video/assets/domains/finance.py @@ -1,3 +1,6 @@ +# semantic components for finance business and resource explanations +# it defines CashFlow CompoundTimeline Funnel FeedbackLoop and ResourceFlow built on SemanticMobject + """Semantic components for finance, business, and resource explanations.""" from __future__ import annotations @@ -36,7 +39,9 @@ ) +# account boxes showing balances with transfers between them class CashFlow(SemanticMobject): + # read the account mapping and build the account boxes and links def __init__( self, theme: VisualTheme, @@ -62,6 +67,7 @@ def __init__( self.register_anchor(f"account_{index}", account) frame_safe(self) + # build one box per account showing its name and balance def _accounts(self) -> VGroup: return VGroup( *[ @@ -77,6 +83,7 @@ def _accounts(self) -> VGroup: ] ).arrange(RIGHT, buff=0.65) + # move an amount between accounts and return the transform that redraws them def transfer(self, source: int, target: int, amount: float) -> Transform: source_index = ensure_index(source, len(self.amounts), name="source account") target_index = ensure_index(target, len(self.amounts), name="target account") @@ -88,7 +95,9 @@ def transfer(self, source: int, target: int, amount: float) -> Transform: return Transform(self.part("accounts"), self._accounts()) +# a baseline with stems that grow as compound interest accumulates each period class CompoundTimeline(SemanticMobject): + # validate the inputs and build the baseline ticks and the starting balance stem def __init__( self, theme: VisualTheme, @@ -106,6 +115,7 @@ def __init__( self.periods = int(periods) self.visible_periods = 0 baseline = Line(LEFT * 4.5, RIGHT * 4.5, color=theme.muted) + # one tick per period spread evenly along the baseline ticks = VGroup( *[ Line(UP * 0.10, DOWN * 0.10, color=theme.muted).move_to( @@ -124,15 +134,18 @@ def __init__( self.register_anchor("end", lambda: baseline.get_end()) frame_safe(self) + # compute the compounded balance after a given period def balance_at(self, period: int) -> float: if not 0 <= int(period) <= self.periods: raise IndexError(f"period {period} is outside 0..{self.periods}") return self.principal * (1 + self.rate) ** int(period) + # build a stem and amount label for each period up to through scaled by the final balance def _balances(self, through: int, baseline: Line) -> VGroup: values = VGroup() maximum = max(self.balance_at(self.periods), self.principal, 1e-9) for period in range(through + 1): + # stem height scales between a small floor and the largest balance point = baseline.point_from_proportion(period / max(self.periods, 1)) height = 0.25 + 1.8 * self.balance_at(period) / maximum stem = Line(point, point + UP * height, color=self.theme.primary, stroke_width=5) @@ -145,6 +158,7 @@ def _balances(self, through: int, baseline: Line) -> VGroup: values.add(VGroup(stem, amount)) return values + # reveal more periods and return the transform that redraws the stems def compound(self, periods: int = 1) -> Transform: if periods < 0: raise ValueError("compound step cannot be negative") @@ -153,7 +167,9 @@ def compound(self, periods: int = 1) -> Transform: return Transform(self.part("balances"), replacement) +# stacked trapezoids whose widths follow the count at each stage class Funnel(SemanticMobject): + # read the stage mapping and build the funnel levels def __init__( self, theme: VisualTheme, @@ -177,10 +193,12 @@ def __init__( self.register_anchor(f"stage_{index}", stage) frame_safe(self) + # build one trapezoid per stage with width proportional to its count def _levels(self) -> VGroup: maximum = max(max(self.counts), 1e-9) levels = VGroup() for index, (name, count) in enumerate(zip(self.names, self.counts)): + # width scales with the count relative to the largest stage and the shape narrows toward the bottom width = 1.4 + 5.2 * count / maximum shape = Polygon( [-width / 2, 0.42, 0], @@ -196,6 +214,7 @@ def _levels(self) -> VGroup: levels.arrange(DOWN, buff=0.08) return levels + # move an amount between stages and return the transform that redraws the funnel def convert(self, source: int, target: int, amount: float) -> Transform: source_index = ensure_index(source, len(self.counts), name="source funnel stage") target_index = ensure_index(target, len(self.counts), name="target funnel stage") @@ -207,7 +226,9 @@ def convert(self, source: int, target: int, amount: float) -> Transform: return Transform(self.part("stages"), self._levels()) +# factors arranged in a ring with arrows around it and a pulse that circles them class FeedbackLoop(SemanticMobject): + # place the factor boxes on a circle and draw the ring of arrows and the pulse def __init__( self, theme: VisualTheme, @@ -219,12 +240,14 @@ def __init__( self.factors = normalized_labels(factors, fallback=["users", "data", "quality"]) count = len(self.factors) nodes = VGroup() + # space the factors evenly around a circle whose radius grows with the count radius = min(2.4, 1.1 + count * 0.22) for index, factor in enumerate(self.factors): angle = np.pi / 2 - 2 * np.pi * index / max(count, 1) node = labeled_box(factor, theme, width=1.35, height=0.66, color=theme.primary) node.move_to([radius * np.cos(angle), radius * np.sin(angle), 0]) nodes.add(node) + # link each factor to the next and wrap the last one back to the first links = VGroup( *[ Arrow( @@ -247,6 +270,7 @@ def __init__( self.register_part("label", caption) frame_safe(self) + # advance the pulse to the next factor and return the animation def reinforce(self) -> Any: self.step = (self.step + 1) % len(self.factors) return self.part("pulse").animate.move_to( @@ -254,7 +278,9 @@ def reinforce(self) -> Any: ) +# stock boxes holding amounts with transfers between them class ResourceFlow(SemanticMobject): + # read the resource mapping and build the stocks and links def __init__( self, theme: VisualTheme, @@ -280,6 +306,7 @@ def __init__( self.register_anchor(f"stock_{index}", stock) frame_safe(self) + # build one box per stock showing its name and amount def _stocks(self) -> VGroup: return VGroup( *[ @@ -295,6 +322,7 @@ def _stocks(self) -> VGroup: ] ).arrange(RIGHT, buff=0.65) + # move an amount between stocks and return the transform that redraws them def transfer(self, source: int, target: int, amount: float) -> Transform: source_index = ensure_index(source, len(self.amounts), name="source stock") target_index = ensure_index(target, len(self.amounts), name="target stock") diff --git a/skills/manim-video/assets/domains/math.py b/skills/manim-video/assets/domains/math.py index e8d4282c..d2e3f919 100644 --- a/skills/manim-video/assets/domains/math.py +++ b/skills/manim-video/assets/domains/math.py @@ -1,3 +1,6 @@ +# semantic components for mathematical explanations +# it defines NumberLineModel VectorMap MatrixMap LinkedPlot and ProbabilityMass built on SemanticMobject + """Semantic components for mathematical explanations.""" from __future__ import annotations @@ -30,7 +33,9 @@ from ._common import ensure_amount, ensure_index, frame_safe, label_for, title_above +# a number line with a marker dot that moves between values class NumberLineModel(SemanticMobject): + # validate the range and build the line and marker def __init__( self, theme: VisualTheme, @@ -59,6 +64,7 @@ def __init__( self.register_anchor("value", marker) frame_safe(self) + # validate the target and return an animation that slides the marker to it def move_value(self, value: float) -> Any: target = float(value) if not self.minimum <= target <= self.maximum: @@ -67,7 +73,9 @@ def move_value(self, value: float) -> Any: return self.part("marker").animate.move_to(self.part("line").n2p(target)) +# axes with arrows for a set of vectors that a matrix can transform class VectorMap(SemanticMobject): + # build the axes and one arrow per vector from the origin def __init__( self, theme: VisualTheme, @@ -105,6 +113,7 @@ def __init__( self.register_anchor("origin", lambda: axes.c2p(0, 0)) frame_safe(self) + # multiply every vector by a two by two matrix and return the animation that redraws the arrows def apply_matrix(self, matrix: Sequence[Sequence[float]]) -> AnimationGroup: transform = np.asarray(matrix, dtype=float) if transform.shape != (2, 2): @@ -125,11 +134,14 @@ def apply_matrix(self, matrix: Sequence[Sequence[float]]) -> AnimationGroup: return AnimationGroup(*animations, lag_ratio=0) +# alternate between primary and secondary colors by index def theme_color(theme: VisualTheme, index: int) -> str: return theme.primary if index % 2 == 0 else theme.secondary +# a two by two matrix grid beside axes showing an input arrow and its transformed output class MatrixMap(SemanticMobject): + # validate the matrix and build the grid the axes and the input and output arrows def __init__( self, theme: VisualTheme, @@ -178,6 +190,7 @@ def __init__( self.register_anchor("output", output_arrow) frame_safe(self) + # multiply a vector by the matrix and return the animation that moves both arrows def apply_to(self, vector: Sequence[float]) -> AnimationGroup: raw = np.asarray(vector, dtype=float) if raw.shape != (2,): @@ -195,7 +208,9 @@ def apply_to(self, vector: Sequence[float]) -> AnimationGroup: ) +# a plotted function with a point and a dashed guide that track one x value class LinkedPlot(SemanticMobject): + # validate the range and build the axes graph point and guide def __init__( self, theme: VisualTheme, @@ -236,6 +251,7 @@ def __init__( self.register_anchor("point", point) frame_safe(self) + # validate the x value and return the animation that moves the point and guide def set_x(self, x_value: float) -> AnimationGroup: value = float(x_value) if not self.x_range[0] <= value <= self.x_range[1]: @@ -251,7 +267,9 @@ def set_x(self, x_value: float) -> AnimationGroup: ) +# bars showing a probability distribution that can shift mass between outcomes class ProbabilityMass(SemanticMobject): + # validate probabilities and labels and build the bars def __init__( self, theme: VisualTheme, @@ -278,9 +296,11 @@ def __init__( self.register_anchor(f"mass_{index}", bar) frame_safe(self) + # build one bar per outcome with a percent label above and a name label below def _bars(self) -> VGroup: bars = VGroup() for index, (name, value) in enumerate(zip(self.labels, self.probabilities)): + # bar height scales with probability and keeps a tiny floor so zero bars stay visible height = max(0.02, value * 3.4) bar = Rectangle( width=0.86, @@ -297,6 +317,7 @@ def _bars(self) -> VGroup: bars.arrange(RIGHT, buff=0.32, aligned_edge=DOWN) return bars + # move probability mass between outcomes and return the transform that redraws the bars def transfer_probability(self, source: int, target: int, amount: float) -> Transform: source_index = ensure_index(source, len(self.probabilities), name="source") target_index = ensure_index(target, len(self.probabilities), name="target") diff --git a/skills/manim-video/assets/domains/physics.py b/skills/manim-video/assets/domains/physics.py index bf63d9b0..0d2d200b 100644 --- a/skills/manim-video/assets/domains/physics.py +++ b/skills/manim-video/assets/domains/physics.py @@ -1,3 +1,6 @@ +# semantic components for mechanics waves and circuits +# it defines BodySystem ForceVector WaveField and CircuitFlow built on SemanticMobject + """Semantic components for mechanics, waves, and circuits.""" from __future__ import annotations @@ -29,7 +32,9 @@ from ._common import ensure_index, frame_safe, label_for, normalized_labels, title_above +# labeled bodies joined by lines that can move and receive force arrows class BodySystem(SemanticMobject): + # place the bodies at given or evenly spaced positions and connect them def __init__( self, theme: VisualTheme, @@ -40,6 +45,7 @@ def __init__( ) -> None: super().__init__(theme) self.body_names = normalized_labels(bodies, fallback=["m1", "m2"]) + # spread the bodies evenly along the x axis centered on the origin if positions is None: positions = [ ((index - (len(self.body_names) - 1) / 2) * 2.2, 0, 0) @@ -72,10 +78,12 @@ def __init__( self.register_anchor(f"body_{index}", body) frame_safe(self) + # return an animation that moves one body to a position def move_body(self, index: int, position: Sequence[float]) -> Any: selected = ensure_index(index, len(self.body_names), name="body") return self.part("bodies")[selected].animate.move_to(position) + # add a force arrow from one body and return it def apply_force(self, index: int, vector: Sequence[float]) -> Arrow: selected = ensure_index(index, len(self.body_names), name="body") raw = np.asarray(vector, dtype=float) @@ -95,7 +103,9 @@ def apply_force(self, index: int, vector: Sequence[float]) -> Arrow: return arrow +# a single force arrow from the origin with a label at its tip class ForceVector(SemanticMobject): + # validate the vector and build the origin dot arrow and label def __init__( self, theme: VisualTheme, @@ -116,6 +126,7 @@ def __init__( self.register_anchor("tip", lambda: arrow.get_end()) frame_safe(self) + # coerce a two or three element vector to three d and reject zero vectors @staticmethod def _vector(vector: Sequence[float]) -> np.ndarray: raw = np.asarray(vector, dtype=float) @@ -125,6 +136,7 @@ def _vector(vector: Sequence[float]) -> np.ndarray: raise ValueError("force vector must be a non-zero 2D or 3D vector") return raw + # replace the vector and return a transform into the new arrow def set_vector(self, vector: Sequence[float]) -> Transform: self.vector = self._vector(vector) return Transform( @@ -133,7 +145,9 @@ def set_vector(self, vector: Sequence[float]) -> Transform: ) +# a sine wave on axes with a source dot that propagates by shifting phase class WaveField(SemanticMobject): + # validate amplitude and wavelength then build the axes wave and source def __init__( self, theme: VisualTheme, @@ -168,12 +182,15 @@ def __init__( self.register_anchor("crest", lambda: axes.c2p(self.wavelength / 4 + self.phase, self.amplitude)) frame_safe(self) + # evaluate the sine wave at x using the current amplitude wavelength and phase def _value(self, x: float) -> float: return self.amplitude * np.sin(2 * np.pi * (x - self.phase) / self.wavelength) + # plot the current wave on the axes def _wave(self, axes: Axes) -> Any: return axes.plot(lambda x: self._value(x), x_range=(-5, 5), color=self.theme.primary) + # advance the phase and return the animation that redraws the wave and source def propagate(self, distance: float) -> AnimationGroup: self.phase += float(distance) axes = self.part("axes") @@ -186,7 +203,9 @@ def propagate(self, distance: float) -> AnimationGroup: ) +# a rectangular wire loop with components on it and charges that flow around the top class CircuitFlow(SemanticMobject): + # build the wire loop place the components and spread the charges along the top wire def __init__( self, theme: VisualTheme, @@ -207,6 +226,7 @@ def __init__( Line([left_x, bottom_y, 0], [left_x, top_y, 0], color=theme.muted), ) component_group = VGroup() + # components sit on the left right top and bottom wires in that order positions = [[left_x, 0, 0], [right_x, 0, 0], [0, top_y, 0], [0, bottom_y, 0]] for index, name in enumerate(self.components): box = Rectangle( @@ -217,6 +237,7 @@ def __init__( fill_opacity=1, ).move_to(positions[index % len(positions)]) component_group.add(VGroup(box, label_for(name, theme, size=18, width=1.05).move_to(box))) + # charges start evenly spaced along the top wire path_points = [ [-3.2 + 6.4 * index / charge_count, top_y, 0] for index in range(charge_count) @@ -230,6 +251,7 @@ def __init__( self.register_part("label", caption) frame_safe(self) + # set the current and return an animation that colors charges by direction and strength def set_current(self, current: float) -> AnimationGroup: self.current = float(current) strength = min(1.0, abs(self.current)) @@ -239,6 +261,7 @@ def set_current(self, current: float) -> AnimationGroup: lag_ratio=0.03, ) + # return a staggered animation that shifts the charges in the current direction def flow(self, distance: float = 0.45) -> AnimationGroup: direction = 1 if self.current >= 0 else -1 return AnimationGroup( diff --git a/skills/manim-video/assets/domains/systems.py b/skills/manim-video/assets/domains/systems.py index d3872332..aa6404ec 100644 --- a/skills/manim-video/assets/domains/systems.py +++ b/skills/manim-video/assets/domains/systems.py @@ -1,3 +1,6 @@ +# semantic components for software systems and data architecture +# it defines RequestFlow ServiceGraph QueueModel and DataPipeline built on SemanticMobject + """Semantic components for software systems and data architecture.""" from __future__ import annotations @@ -36,7 +39,9 @@ ) +# a row of stages with a request dot that advances across them class RequestFlow(SemanticMobject): + # build the stage boxes links and the request dot def __init__( self, theme: VisualTheme, @@ -65,6 +70,7 @@ def __init__( self.register_anchor(f"stage_{index}", stage) frame_safe(self) + # move the request to the next stage or an explicit destination def advance_request(self, destination: int | None = None) -> Any: target = self.request_stage + 1 if destination is None else int(destination) target = ensure_index(target, len(self.stages), name="request destination") @@ -73,6 +79,7 @@ def advance_request(self, destination: int | None = None) -> Any: self.part("stages")[target].get_top() + UP * 0.30 ) + # move the request back to the first stage def reset_request(self) -> Any: self.request_stage = 0 return self.part("request").animate.move_to( @@ -80,7 +87,9 @@ def reset_request(self) -> Any: ) +# services arranged in an ellipse with directed connections and animated requests class ServiceGraph(SemanticMobject): + # validate services and connections then place the nodes and draw the links def __init__( self, theme: VisualTheme, @@ -97,6 +106,7 @@ def __init__( nodes = VGroup( *[labeled_box(service, theme, width=1.5, color=theme.primary) for service in self.services] ) + # a lone service sits at the center and otherwise nodes spread around an ellipse if len(nodes) == 1: nodes[0].move_to([0, 0, 0]) else: @@ -133,6 +143,7 @@ def __init__( self.register_anchor(f"service_{name}", service) frame_safe(self) + # spawn a request dot and return an animation that moves it along the connection def send_request(self, source: object, target: object) -> AnimationGroup: connection = (str(source), str(target)) try: @@ -149,7 +160,9 @@ def send_request(self, source: object, target: object) -> AnimationGroup: ) +# a fixed capacity row of slots with front and back markers class QueueModel(SemanticMobject): + # validate capacity and items then build the slots and markers def __init__( self, theme: VisualTheme, @@ -168,6 +181,7 @@ def __init__( slots = self._slots() front = label_for("front", theme, size=17, color=theme.muted, width=0.8) back = label_for("back", theme, size=17, color=theme.muted, width=0.8) + # pin the markers under the leftmost and rightmost slots front.next_to(slots, DOWN, buff=0.16).align_to(slots, direction=[-1, 0, 0]) back.next_to(slots, DOWN, buff=0.16).align_to(slots, direction=[1, 0, 0]) markers = VGroup(front, back) @@ -179,6 +193,7 @@ def __init__( self.register_anchor("back", lambda: slots[-1].get_center()) frame_safe(self) + # build one box per slot with filled slots colored and empty slots faded def _slots(self) -> VGroup: values = [*self.items, *([""] * (self.capacity - len(self.items)))] return VGroup( @@ -195,12 +210,14 @@ def _slots(self) -> VGroup: ] ).arrange(RIGHT, buff=0.08) + # append an item and return the transform that redraws the slots def enqueue(self, request: object) -> Transform: if len(self.items) >= self.capacity: raise OverflowError("cannot enqueue into a full queue") self.items.append(str(request)) return Transform(self.part("slots"), self._slots()) + # pop the front item and return it with the transform that redraws the slots def dequeue(self) -> tuple[str, Transform]: if not self.items: raise IndexError("cannot dequeue from an empty queue") @@ -208,7 +225,9 @@ def dequeue(self) -> tuple[str, Transform]: return request, Transform(self.part("slots"), self._slots()) +# a row of pipeline stages with a packet that propagates across them class DataPipeline(SemanticMobject): + # build the stage boxes links and the packet def __init__( self, theme: VisualTheme, @@ -240,6 +259,7 @@ def __init__( self.register_part("label", caption) frame_safe(self) + # move the packet to the next stage or an explicit destination def propagate(self, destination: int | None = None) -> Any: target = self.packet_stage + 1 if destination is None else int(destination) target = ensure_index(target, len(self.stages), name="pipeline destination") diff --git a/skills/manim-video/assets/teaching.py b/skills/manim-video/assets/teaching.py index 1dca6377..38005fc8 100644 --- a/skills/manim-video/assets/teaching.py +++ b/skills/manim-video/assets/teaching.py @@ -1,3 +1,7 @@ +# semantic building blocks for teaching scenes built on manim +# it defines themes named parts anchors linked values and the base scene classes chapters extend +# everything here is meant to be reused across the domain kits so scenes stay consistent + """Style-neutral semantic authoring primitives for Manim Community explainers. The classes in this module manage meaning, continuity, and attention. They do @@ -27,6 +31,7 @@ ) +# one explicit set of semantic color and font roles shared by every component @dataclass(frozen=True, slots=True) class VisualTheme: """One explicit set of semantic color and typography roles. @@ -47,6 +52,7 @@ class VisualTheme: body_font: str = "" label_font: str = "" + # resolve a semantic font role and reject unknown roles def font_for(self, role: str) -> str: """Resolve a semantic font role and reject accidental one-off roles.""" fonts = { @@ -59,6 +65,7 @@ def font_for(self, role: str) -> str: return fonts[role] +# create themed text that survives empty strings and shrinks to a max width def theme_text( value: object, theme: VisualTheme, @@ -76,6 +83,7 @@ def theme_text( font_size=font_size, color=color or theme.text, ) + # an empty label becomes an invisible space so layout code never breaks if not text: rendered.set_opacity(0.0) if max_width is not None and rendered.width > max_width: @@ -83,6 +91,7 @@ def theme_text( return rendered +# scale a component down only when it would exceed the frame minus the margin def fit_to_frame(mobject: Mobject, *, margin: float = 0.45) -> Mobject: """Scale a component down only when it would exceed the active frame.""" if margin < 0: @@ -101,6 +110,7 @@ def fit_to_frame(mobject: Mobject, *, margin: float = 0.45) -> Mobject: return mobject +# collect every family member that can report and restore a style def _family_with_style(mobject: Mobject) -> list[Mobject]: return [ member @@ -109,6 +119,7 @@ def _family_with_style(mobject: Mobject) -> list[Mobject]: ] +# capture a deep copy of each unique styled member so it can be restored later def _style_snapshot(mobjects: Iterable[Mobject]) -> list[tuple[Mobject, dict[str, Any]]]: seen: set[int] = set() snapshot: list[tuple[Mobject, dict[str, Any]]] = [] @@ -121,11 +132,13 @@ def _style_snapshot(mobjects: Iterable[Mobject]) -> list[tuple[Mobject, dict[str return snapshot +# put every captured style back onto its member def _restore_snapshot(snapshot: Iterable[tuple[Mobject, dict[str, Any]]]) -> None: for member, style in snapshot: member.set_style(**deepcopy(style)) +# find the submobject index path from root to target or none when target is not inside def _path_to(root: Mobject, target: Mobject) -> tuple[int, ...] | None: if root is target: return () @@ -136,6 +149,7 @@ def _path_to(root: Mobject, target: Mobject) -> tuple[int, ...] | None: return None +# walk a submobject index path from root and return the mobject at the end def _at_path(root: Mobject, path: Sequence[int]) -> Mobject: current = root for index in path: @@ -143,9 +157,11 @@ def _at_path(root: Mobject, path: Sequence[int]) -> Mobject: return current +# a vgroup whose authored parts and anchors have stable semantic names class SemanticMobject(VGroup): """A ``VGroup`` whose authored parts and anchors have stable semantic names.""" + # validate the theme and set up the part and anchor registries def __init__(self, theme: VisualTheme, *mobjects: Mobject, **kwargs: Any) -> None: if not isinstance(theme, VisualTheme): raise TypeError("SemanticMobject requires a shared VisualTheme") @@ -157,6 +173,7 @@ def __init__(self, theme: VisualTheme, *mobjects: Mobject, **kwargs: Any) -> Non ] = {} self._opacity_snapshot: list[tuple[Mobject, dict[str, Any]]] | None = None + # store a named part and add it to the group unless asked not to def register_part(self, name: str, mobject: Mobject, *, add: bool = True) -> Mobject: if not name: raise ValueError("part name cannot be empty") @@ -169,6 +186,7 @@ def register_part(self, name: str, mobject: Mobject, *, add: bool = True) -> Mob self.add(mobject) return mobject + # look up a registered part and list the known names on a miss def part(self, name: str) -> Mobject: try: return self._semantic_parts[name] @@ -176,10 +194,12 @@ def part(self, name: str) -> Mobject: known = ", ".join(sorted(self._semantic_parts)) or "none" raise KeyError(f"unknown part {name!r}; registered parts: {known}") from exc + # names of every registered part in registration order @property def part_names(self) -> tuple[str, ...]: return tuple(self._semantic_parts) + # store a named anchor that may be a mobject a fixed point or a callable def register_anchor( self, name: str, @@ -189,18 +209,21 @@ def register_anchor( raise ValueError("anchor name cannot be empty") if name in self._semantic_anchors: raise ValueError(f"anchor {name!r} is already registered") + # plain coordinates must be two or three numbers if not isinstance(anchor, Mobject) and not callable(anchor): point = np.asarray(anchor, dtype=float) if point.shape not in {(2,), (3,)}: raise ValueError("anchor coordinates must contain two or three values") self._semantic_anchors[name] = anchor + # resolve a named anchor to a three dimensional point copy def anchor(self, name: str) -> np.ndarray: try: anchor = self._semantic_anchors[name] except KeyError as exc: known = ", ".join(sorted(self._semantic_anchors)) or "none" raise KeyError(f"unknown anchor {name!r}; registered anchors: {known}") from exc + # a mobject anchor follows its center while callables and raw points are evaluated now if isinstance(anchor, Mobject): point = np.asarray(anchor.get_center(), dtype=float) else: @@ -211,10 +234,12 @@ def anchor(self, name: str) -> np.ndarray: raise ValueError(f"anchor {name!r} did not resolve to a 2D or 3D point") return point.copy() + # names of every registered anchor in registration order @property def anchor_names(self) -> tuple[str, ...]: return tuple(self._semantic_anchors) + # dim every named part except the requested focus and remember the original styles def highlight( self, *part_names: str, @@ -233,14 +258,17 @@ def highlight( part.set_opacity(dim_opacity) return self + # undo a highlight by restoring the saved styles def restore_opacity(self) -> "SemanticMobject": if self._opacity_snapshot is not None: _restore_snapshot(self._opacity_snapshot) self._opacity_snapshot = None return self + # rebind parts and anchors by structural path after this object has become target def _adopt_semantics_from(self, target: "SemanticMobject") -> None: """Rebind semantic parts after this object has become ``target``.""" + # record where each part lives inside target then find the same path inside self part_paths = { name: _path_to(target, part) for name, part in target._semantic_parts.items() @@ -250,6 +278,7 @@ def _adopt_semantics_from(self, target: "SemanticMobject") -> None: for name, path in part_paths.items() if path is not None } + # mobject anchors are remapped by path and callables are frozen to their current point anchors: dict[str, Mobject | Sequence[float] | Callable[[], Sequence[float]]] = {} for name, anchor in target._semantic_anchors.items(): if isinstance(anchor, Mobject): @@ -262,9 +291,11 @@ def _adopt_semantics_from(self, target: "SemanticMobject") -> None: self._semantic_anchors = anchors +# one value tracker driving any number of named representations class LinkedValue: """One ``ValueTracker`` driving any number of named representations.""" + # create the tracker and an empty binding table def __init__(self, value: float = 0.0) -> None: self.tracker = ValueTracker(float(value)) self._bindings: dict[ @@ -272,10 +303,12 @@ def __init__(self, value: float = 0.0) -> None: ] = {} self._suspended = False + # the current tracked value as a float @property def value(self) -> float: return float(self.tracker.get_value()) + # bind a dependent mobject to an update callback and run it once immediately def register( self, name: str | Mobject, @@ -283,6 +316,7 @@ def register( update: Callable[[Mobject, float], Mobject | None] | None = None, ) -> Mobject: """Register a dependent by name, or pass ``(mobject, update)`` for an auto-name.""" + # support both the named form and the shorthand form where the mobject comes first if isinstance(name, Mobject): dependent = name callback = mobject @@ -298,6 +332,7 @@ def register( if not isinstance(dependent, Mobject) or not callable(callback): raise TypeError("register requires a mobject and an update callback") + # manim updater that skips while suspended and swaps in a replacement when the callback returns one def updater(current: Mobject, _dt: float = 0.0) -> None: if self._suspended: return @@ -310,31 +345,37 @@ def updater(current: Mobject, _dt: float = 0.0) -> None: updater(dependent) return dependent + # return the mobject bound under a name def dependent(self, name: str) -> Mobject: try: return self._bindings[name][0] except KeyError as exc: raise KeyError(f"unknown linked value binding {name!r}") from exc + # set the tracker and push the new value to every dependent def set_value(self, value: float) -> "LinkedValue": self.tracker.set_value(float(value)) self.update() return self + # rerun every updater unless updates are suspended def update(self) -> "LinkedValue": if not self._suspended: for dependent, _callback, updater in self._bindings.values(): updater(dependent) return self + # pause updates so the value can change without redrawing def suspend(self) -> "LinkedValue": self._suspended = True return self + # turn updates back on and refresh every dependent def resume(self) -> "LinkedValue": self._suspended = False return self.update() + # remove one binding or all of them and detach their updaters def clear(self, name: str | None = None) -> "LinkedValue": names = list(self._bindings) if name is None else [name] for binding_name in names: @@ -345,6 +386,7 @@ def clear(self, name: str | None = None) -> "LinkedValue": dependent.remove_updater(updater) return self + # build a manim animation that tweens the tracker to a new value def animate_to(self, value: float, *, run_time: float | None = None) -> Any: builder = self.tracker.animate if run_time is not None: @@ -354,9 +396,11 @@ def animate_to(self, value: float, *, run_time: float | None = None) -> Any: return builder.set_value(float(value)) +# shared registry focus highlight and hold helpers layered onto a manim scene class _TeachingSceneMixin: max_static_hold_s = 3.0 + # lazily create the registry and snapshot attributes so construction order does not matter def _ensure_teaching_state(self) -> None: if not hasattr(self, "_remembered_objects"): self._remembered_objects: dict[str, Mobject] = {} @@ -366,10 +410,12 @@ def _ensure_teaching_state(self) -> None: self._highlight_roots: list[Mobject] = [] self._camera_context: Any = None + # hook manim setup so the teaching state exists before construct runs def setup(self) -> None: super().setup() self._ensure_teaching_state() + # store a named mobject and add it to the scene unless asked not to def remember(self, name: str, mobject: Mobject, *, add: bool = True) -> Mobject: self._ensure_teaching_state() if not name: @@ -381,6 +427,7 @@ def remember(self, name: str, mobject: Mobject, *, add: bool = True) -> Mobject: self.add(mobject) return mobject + # look up a remembered mobject and list the known names on a miss def recall(self, name: str) -> Mobject: self._ensure_teaching_state() try: @@ -389,6 +436,7 @@ def recall(self, name: str) -> Mobject: known = ", ".join(sorted(self._remembered_objects)) or "none" raise KeyError(f"unknown remembered object {name!r}; known objects: {known}") from exc + # start a named manim section so each narrative beat is a separate clip def begin_beat( self, name: str, @@ -402,6 +450,7 @@ def begin_beat( # 0.21's renamed ``section_type`` parameter. self.next_section(name, section_type, skip_animations) + # turn names or mobjects or an iterable of them into a list of mobjects def _resolve_targets( self, target: str | Mobject | Iterable[str | Mobject] ) -> list[Mobject]: @@ -414,6 +463,7 @@ def _resolve_targets( raise ValueError("focus target must contain at least one mobject") return resolved + # keep only the roots that share no family member with any target @staticmethod def _unrelated_roots(roots: Iterable[Mobject], targets: Iterable[Mobject]) -> list[Mobject]: target_ids = { @@ -427,6 +477,7 @@ def _unrelated_roots(roots: Iterable[Mobject], targets: Iterable[Mobject]) -> li if not any(id(member) in target_ids for member in root.get_family()) ] + # play the dim animations and let subclasses move the camera too def _play_focus( self, targets: list[Mobject], @@ -437,9 +488,11 @@ def _play_focus( ) -> None: self.play(*dim_animations, run_time=run_time) + # no camera to restore in the base mixin def _restore_focus_camera(self, *, run_time: float, animate: bool) -> None: return None + # dim everything unrelated to the target and optionally frame it with the camera def focus_on( self, target: str | Mobject | Iterable[str | Mobject], @@ -455,6 +508,7 @@ def focus_on( if not 0 <= dim_opacity <= 1: raise ValueError("dim opacity must be between zero and one") targets = self._resolve_targets(target) + # snapshot the unrelated roots so restore_context can put back exact styles unrelated = self._unrelated_roots(self.mobjects, targets) self._context_roots = unrelated self._context_snapshot = _style_snapshot(unrelated) @@ -466,14 +520,17 @@ def focus_on( root.set_opacity(dim_opacity) self._set_focus_camera_immediately(targets, margin=margin) + # no camera to move in the base mixin def _set_focus_camera_immediately(self, targets: list[Mobject], *, margin: float) -> None: return None + # restore the styles saved by focus_on and return the camera to where it was def restore_context(self, *, run_time: float = 0.6, animate: bool = True) -> None: self._ensure_teaching_state() if self._context_snapshot is None: return snapshot = self._context_snapshot + # animate every member back to its saved style in one group then hard set it to be exact if animate: animations = [ member.animate.set_style(**deepcopy(style)) @@ -486,6 +543,7 @@ def restore_context(self, *, run_time: float = 0.6, animate: bool = True) -> Non self._context_snapshot = None self._context_roots = [] + # recolor the target with the accent color and remember its styles for later def highlight( self, target: str | Mobject | Iterable[str | Mobject], @@ -507,6 +565,7 @@ def highlight( for focused in targets: focused.set_color(highlight_color) + # restore the styles saved by highlight def restore_highlight(self, *, run_time: float = 0.35, animate: bool = True) -> None: self._ensure_teaching_state() if self._highlight_snapshot is None: @@ -523,6 +582,7 @@ def restore_highlight(self, *, run_time: float = 0.35, animate: bool = True) -> self._highlight_snapshot = None self._highlight_roots = [] + # morph a remembered object into target while keeping its registry identity def transform_object( self, name: str, @@ -541,11 +601,13 @@ def transform_object( ) else: current.become(target) + # carry the semantic parts and anchors over when both sides are semantic if isinstance(current, SemanticMobject) and isinstance(target, SemanticMobject): current._adopt_semantics_from(target) self._remembered_objects[name] = current return current + # wait for a duration and demand a purpose for long static holds def hold(self, duration_s: float, *, purpose: str | None = None) -> None: if duration_s < 0: raise ValueError("hold duration cannot be negative") @@ -553,13 +615,16 @@ def hold(self, duration_s: float, *, purpose: str | None = None) -> None: raise ValueError( f"static hold of {duration_s:.2f}s needs an explicit educational purpose" ) + # skip waits shorter than one frame if duration_s >= 1 / float(config.frame_rate): self.wait(duration_s) +# a moving camera scene with semantic continuity and attention helpers class TeachingScene(_TeachingSceneMixin, MovingCameraScene): """A moving-camera scene with semantic continuity and attention helpers.""" + # dim unrelated roots while zooming the camera frame onto the targets def _play_focus( self, targets: list[Mobject], @@ -571,17 +636,21 @@ def _play_focus( group = Group(*targets) frame = self.camera.frame self._camera_context = frame.copy() + # pick the width that fits both dimensions with margin while keeping the frame aspect ratio target_width = max(group.width + margin * 2, (group.height + margin * 2) * frame.width / frame.height) camera_animation = frame.animate.move_to(group.get_center()).set(width=target_width) self.play(*dim_animations, camera_animation, run_time=run_time) + # move the camera frame onto the targets without animating def _set_focus_camera_immediately(self, targets: list[Mobject], *, margin: float) -> None: group = Group(*targets) frame = self.camera.frame self._camera_context = frame.copy() + # pick the width that fits both dimensions with margin while keeping the frame aspect ratio target_width = max(group.width + margin * 2, (group.height + margin * 2) * frame.width / frame.height) frame.move_to(group.get_center()).set(width=target_width) + # bring the camera frame back to its saved copy def _restore_focus_camera(self, *, run_time: float, animate: bool) -> None: if self._camera_context is None: return @@ -592,9 +661,11 @@ def _restore_focus_camera(self, *, run_time: float, animate: bool) -> None: self._camera_context = None +# the same semantic authoring contract on top of the manim three d scene class TeachingThreeDScene(_TeachingSceneMixin, ThreeDScene): """The same semantic authoring contract on top of Manim's ``ThreeDScene``.""" + # capture every camera setting that focus may change def _camera_values(self) -> dict[str, Any]: return { "phi": float(self.camera.phi), @@ -605,6 +676,7 @@ def _camera_values(self) -> dict[str, Any]: "frame_center": np.asarray(self.camera.frame_center, dtype=float).copy(), } + # dim unrelated roots while moving the three d camera onto the targets def _play_focus( self, targets: list[Mobject], @@ -615,6 +687,7 @@ def _play_focus( ) -> None: group = Group(*targets) self._camera_context = self._camera_values() + # zoom by the tighter of the width and height ratios and cap it so the camera never zooms absurdly far scale = min( float(config.frame_width) / max(group.width + 2 * margin, 1e-6), float(config.frame_height) / max(group.height + 2 * margin, 1e-6), @@ -626,9 +699,11 @@ def _play_focus( run_time=run_time, ) + # move the three d camera onto the targets without animating def _set_focus_camera_immediately(self, targets: list[Mobject], *, margin: float) -> None: group = Group(*targets) self._camera_context = self._camera_values() + # zoom by the tighter of the width and height ratios and cap it so the camera never zooms absurdly far scale = min( float(config.frame_width) / max(group.width + 2 * margin, 1e-6), float(config.frame_height) / max(group.height + 2 * margin, 1e-6), @@ -636,6 +711,7 @@ def _set_focus_camera_immediately(self, targets: list[Mobject], *, margin: float self.camera.frame_center = group.get_center() self.camera.zoom = min(float(self.camera.zoom) * scale, 8.0) + # restore the saved three d camera settings by animation or direct assignment def _restore_focus_camera(self, *, run_time: float, animate: bool) -> None: if self._camera_context is None: return diff --git a/skills/manim-video/scripts/preview_scene.py b/skills/manim-video/scripts/preview_scene.py index 5079463c..5b49e109 100755 --- a/skills/manim-video/scripts/preview_scene.py +++ b/skills/manim-video/scripts/preview_scene.py @@ -1,4 +1,8 @@ #!/usr/bin/env python3 +# command line tool that renders chosen manim scene classes at low quality into a project edit verify folder +# it probes the output with ffprobe and extracts first last and contact sheet frames with ffmpeg +# every step raises PreviewError with a clear message so failures are easy to act on + """Render selected Manim chapters and build small authoring-review artifacts.""" from __future__ import annotations @@ -18,6 +22,7 @@ from PIL import Image +# a clear user actionable preview failure class PreviewError(RuntimeError): """A clear, user-actionable preview failure.""" @@ -25,6 +30,7 @@ class PreviewError(RuntimeError): Runner = Callable[..., subprocess.CompletedProcess[str]] +# parse the script and return the names of every top level class def scene_classes(script: Path) -> set[str]: try: source = script.read_text(encoding="utf-8") @@ -37,6 +43,7 @@ def scene_classes(script: Path) -> set[str]: return {node.name for node in tree.body if isinstance(node, ast.ClassDef)} +# find the project edit directory from an explicit flag or by walking up from the script def resolve_edit_dir(script: Path, explicit: Path | None = None) -> Path: script = script.resolve() if explicit is not None: @@ -53,6 +60,7 @@ def resolve_edit_dir(script: Path, explicit: Path | None = None) -> Path: ) +# resolve an executable on the path or raise a clear error def require_executable(value: str, *, purpose: str) -> str: path = shutil.which(value) if path is None: @@ -60,6 +68,7 @@ def require_executable(value: str, *, purpose: str) -> str: return path +# run a command through the injected runner and turn failures into PreviewError def _run( command: Sequence[str], *, @@ -81,6 +90,7 @@ def _run( raise PreviewError(f"{purpose} is unavailable: {exc}") from exc except subprocess.TimeoutExpired as exc: raise PreviewError(f"{purpose} timed out after {timeout_s:g} seconds") from exc + # surface the last line of stderr or stdout as the failure message if result.returncode != 0: detail = (result.stderr or result.stdout or "").strip().splitlines() message = detail[-1] if detail else f"process exited with status {result.returncode}" @@ -88,6 +98,7 @@ def _run( return result +# read duration size and frame rate from a video with ffprobe def probe_video( video: Path, *, @@ -111,6 +122,7 @@ def probe_video( timeout_s=timeout_s, purpose="ffprobe inspection", ) + # pull the first video stream and reject incomplete or malformed metadata try: payload = json.loads(result.stdout) stream = next( @@ -132,6 +144,7 @@ def probe_video( } +# extract a single frame at a timestamp with ffmpeg and verify it was written def _extract_frame( video: Path, output: Path, @@ -165,6 +178,7 @@ def _extract_frame( raise PreviewError(f"expected frame artifact was not produced: {output}") +# extract the initial and final frames and a five frame contact sheet def build_review_frames( video: Path, destination: Path, @@ -175,6 +189,7 @@ def build_review_frames( timeout_s: float, ) -> dict[str, str]: duration = float(metadata["duration_s"]) + # the final frame sits one frame before the end so ffmpeg does not run past the stream final_time = max(0.0, duration - 1 / float(metadata["frame_rate"])) initial = destination / "initial.png" final = destination / "final.png" @@ -195,6 +210,7 @@ def build_review_frames( timeout_s=timeout_s, ) + # sample five evenly spaced frames and paste their thumbnails side by side into one sheet with tempfile.TemporaryDirectory(prefix="contact-", dir=destination) as temp_name: temp_dir = Path(temp_name) frame_paths: list[Path] = [] @@ -230,6 +246,7 @@ def build_review_frames( } +# locate the newest rendered mp4 for a scene ignoring partial and section clips def _find_scene_video(media_dir: Path, scene_name: str) -> Path: matches = [ path @@ -241,6 +258,7 @@ def _find_scene_video(media_dir: Path, scene_name: str) -> Path: return max(matches, key=lambda path: path.stat().st_mtime_ns) +# load the section json manim wrote for a scene or return an empty list def _section_metadata(media_dir: Path, scene_name: str) -> list[dict[str, Any]]: candidates = [ path @@ -258,6 +276,7 @@ def _section_metadata(media_dir: Path, scene_name: str) -> list[dict[str, Any]]: return [dict(section) for section in sections if isinstance(section, dict)] +# render one scene and build its review artifacts and report under edit verify def render_scene( script: Path, scene_name: str, @@ -278,6 +297,7 @@ def render_scene( verify_root = (resolved_edit / "verify").resolve() destination = verify_root / "manim_previews" / script.stem / scene_name destination.mkdir(parents=True, exist_ok=True) + # refuse to write anywhere outside the verify folder if not destination.resolve().is_relative_to(verify_root): raise PreviewError("refusing to write preview artifacts outside edit/verify") @@ -339,6 +359,7 @@ def render_scene( return report +# render each requested scene and gather the reports def preview_scenes( script: Path, scene_names: Sequence[str], @@ -352,6 +373,7 @@ def preview_scenes( return {"status": "ok", "script": str(script.resolve()), "scenes": reports} +# define and parse the command line arguments def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace: parser = argparse.ArgumentParser( description="Render selected Manim scene classes into edit/verify review artifacts." @@ -364,6 +386,7 @@ def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace: return parser.parse_args(argv) +# run the preview and print a json report or a clear failure def main(argv: Sequence[str] | None = None) -> int: args = parse_args(argv) try: diff --git a/skills/manim-video/scripts/setup.sh b/skills/manim-video/scripts/setup.sh index 0e4676f2..b1ef1363 100755 --- a/skills/manim-video/scripts/setup.sh +++ b/skills/manim-video/scripts/setup.sh @@ -1,14 +1,22 @@ #!/usr/bin/env bash +# quick environment check for the manim video skill +# it looks for python manim latex and ffmpeg and prints a pass or fail line for each + set -euo pipefail +# terminal color codes for the pass and fail markers G="\033[0;32m"; R="\033[0;31m"; N="\033[0m" +# print a green plus line for a satisfied prerequisite ok() { echo -e " ${G}+${N} $1"; } +# print a red x line for a missing prerequisite fail() { echo -e " ${R}x${N} $1"; } echo ""; echo "Manim Video Skill — Setup Check"; echo "" errors=0 +# probe each prerequisite and bump the error count when one is missing command -v python3 &>/dev/null && ok "Python $(python3 --version 2>&1 | awk '{print $2}')" || { fail "Python 3 not found"; errors=$((errors+1)); } python3 -c "import manim" 2>/dev/null && ok "Manim $(manim --version 2>&1 | head -1)" || { fail "Manim not installed: pip install manim"; errors=$((errors+1)); } command -v pdflatex &>/dev/null && ok "LaTeX (pdflatex)" || { fail "LaTeX not found (macOS: brew install --cask mactex-no-gui)"; errors=$((errors+1)); } command -v ffmpeg &>/dev/null && ok "ffmpeg" || { fail "ffmpeg not found"; errors=$((errors+1)); } echo "" +# print the overall verdict based on the error count [ $errors -eq 0 ] && echo -e "${G}All prerequisites satisfied.${N}" || echo -e "${R}$errors prerequisite(s) missing.${N}" echo "" diff --git a/tests/fixtures/manim_domain_gallery.py b/tests/fixtures/manim_domain_gallery.py index 84724ca7..16eccead 100644 --- a/tests/fixtures/manim_domain_gallery.py +++ b/tests/fixtures/manim_domain_gallery.py @@ -1,3 +1,6 @@ +# gallery of tiny manim scenes used by the rendering test +# each scene shows one domain component or exercises the teaching scene api + from __future__ import annotations import sys @@ -28,49 +31,63 @@ THEME = VisualTheme() +# render a number line component class MathGallery(Scene): + # add the component and hold briefly def construct(self): self.camera.background_color = THEME.background self.add(NumberLineModel(THEME, label="math")) self.wait(0.1) +# render an array component class ComputingGallery(Scene): + # add the component and hold briefly def construct(self): self.camera.background_color = THEME.background self.add(ArrayModel(THEME, label="algorithms and AI")) self.wait(0.1) +# render a request flow component class SystemsGallery(Scene): + # add the component and hold briefly def construct(self): self.camera.background_color = THEME.background self.add(RequestFlow(THEME, label="software systems")) self.wait(0.1) +# render a body system component class PhysicsGallery(Scene): + # add the component and hold briefly def construct(self): self.camera.background_color = THEME.background self.add(BodySystem(THEME, label="physics")) self.wait(0.1) +# render a cell process component class BiologyGallery(Scene): + # add the component and hold briefly def construct(self): self.camera.background_color = THEME.background self.add(CellProcess(THEME, label="biology")) self.wait(0.1) +# render a cash flow component class FinanceGallery(Scene): + # add the component and hold briefly def construct(self): self.camera.background_color = THEME.background self.add(CashFlow(THEME, label="finance and business")) self.wait(0.1) +# exercise the teaching scene focus highlight transform and hold helpers class TeachingAPIGallery(TeachingScene): + # walk through each teaching helper with short run times def construct(self): self.theme = THEME self.camera.background_color = THEME.background @@ -90,7 +107,9 @@ def construct(self): self.hold(0.1, purpose="inspect transformed semantic object") +# exercise focus and restore on the three d teaching scene class TeachingThreeDGallery(TeachingThreeDScene): + # focus on a sphere and restore the context with short run times def construct(self): self.camera.background_color = THEME.background focus = self.remember("focus", Sphere(radius=0.25).shift([-0.8, 0, 0])) diff --git a/tests/test_manim_domains.py b/tests/test_manim_domains.py index 6121600f..6b2b6a88 100644 --- a/tests/test_manim_domains.py +++ b/tests/test_manim_domains.py @@ -1,3 +1,6 @@ +# tests that every domain component is semantic frame safe and theme aware +# it also checks a handful of component actions update their state + from __future__ import annotations import sys @@ -19,6 +22,7 @@ LABELS = ["", "short", "A label long enough to exercise component width fitting and frame safety"] +# build every component with each label length and check it stays inside the frame @pytest.mark.parametrize("component", COMPONENTS, ids=lambda component: component.__name__) @pytest.mark.parametrize("label", LABELS, ids=["empty", "short", "long"]) def test_domain_components_are_semantic_and_frame_safe(component: type, label: str) -> None: @@ -30,12 +34,14 @@ def test_domain_components_are_semantic_and_frame_safe(component: type, label: s assert model.height <= float(manim.config.frame_height) - 0.9 + 1e-6 +# every component must reject a missing theme def test_domain_components_require_a_shared_theme() -> None: for component in COMPONENTS: with pytest.raises(TypeError, match="VisualTheme"): component(None) +# call one action on several components and check the tracked state changed def test_domain_actions_update_semantic_state() -> None: theme = VisualTheme() diff --git a/tests/test_manim_gallery.py b/tests/test_manim_gallery.py index 01d90a48..6cce7080 100644 --- a/tests/test_manim_gallery.py +++ b/tests/test_manim_gallery.py @@ -1,3 +1,6 @@ +# renders the gallery fixture scenes at low quality through the manim cli +# it skips when manim is missing and checks each scene produced a video + from __future__ import annotations import shutil @@ -10,6 +13,7 @@ pytest.importorskip("manim") +# render every gallery scene in one manim call and check each mp4 exists def test_each_domain_family_renders_a_low_quality_gallery(tmp_path: Path) -> None: manim = shutil.which("manim") if manim is None: diff --git a/tests/test_manim_teaching.py b/tests/test_manim_teaching.py index 81e5acb8..612b2189 100644 --- a/tests/test_manim_teaching.py +++ b/tests/test_manim_teaching.py @@ -1,3 +1,6 @@ +# tests for the semantic teaching primitives in teaching py +# it covers part and anchor registration focus and restore transforms linked values and the three d scene + from __future__ import annotations import sys @@ -21,11 +24,13 @@ ) +# a default theme for each test @pytest.fixture def theme() -> VisualTheme: return VisualTheme() +# parts and anchors register and resolve under their names def test_semantic_part_and_anchor_registration(theme: VisualTheme) -> None: model = SemanticMobject(theme) dot = manim.Dot([1, 2, 0]) @@ -39,6 +44,7 @@ def test_semantic_part_and_anchor_registration(theme: VisualTheme) -> None: assert model.anchor_names == ("value_center",) +# duplicate names and unknown lookups raise def test_semantic_names_reject_duplicates_and_unknowns(theme: VisualTheme) -> None: model = SemanticMobject(theme) model.register_part("value", manim.Dot()) @@ -54,6 +60,7 @@ def test_semantic_names_reject_duplicates_and_unknowns(theme: VisualTheme) -> No model.anchor("missing") +# focus dims unrelated objects and restore brings back the exact opacities def test_focus_dims_unrelated_objects_and_restores_exact_opacity(theme: VisualTheme) -> None: scene = TeachingScene() scene.theme = theme @@ -73,6 +80,7 @@ def test_focus_dims_unrelated_objects_and_restores_exact_opacity(theme: VisualTh np.testing.assert_allclose(context.get_stroke_opacity(), before_stroke) +# transform keeps the same python object and adopts the target parts and anchors def test_transform_preserves_registry_identity_and_adopts_semantics(theme: VisualTheme) -> None: scene = TeachingScene() source = SemanticMobject(theme) @@ -92,6 +100,7 @@ def test_transform_preserves_registry_identity_and_adopts_semantics(theme: Visua np.testing.assert_allclose(transformed.anchor("center"), [0, 0, 0]) +# every dependent sees each value in order def test_linked_value_updates_all_representations_at_each_state(theme: VisualTheme) -> None: linked = LinkedValue(0) dot = manim.Dot() @@ -121,6 +130,7 @@ def test_linked_value_updates_all_representations_at_each_state(theme: VisualThe assert {name for name, recorded in seen[-2:] if recorded == value} == {"dot", "bar"} +# suspend blocks updates and clear detaches the updater def test_linked_value_suspend_resume_and_clear() -> None: linked = LinkedValue(0) dot = manim.Dot() @@ -134,6 +144,7 @@ def test_linked_value_suspend_resume_and_clear() -> None: assert dot.get_center()[0] == pytest.approx(2) +# the three d scene supports the registry and restores the camera after focus def test_three_d_scene_supports_registry_and_immediate_focus(theme: VisualTheme) -> None: scene = TeachingThreeDScene() focus = manim.Sphere(radius=0.25) diff --git a/tests/test_preview_scene.py b/tests/test_preview_scene.py index 82221535..3ef28da2 100644 --- a/tests/test_preview_scene.py +++ b/tests/test_preview_scene.py @@ -1,3 +1,6 @@ +# tests for the preview_scene script +# it loads the script from its path and checks error handling and one real low quality render + from __future__ import annotations import importlib.util @@ -15,12 +18,14 @@ / "scripts" / "preview_scene.py" ) +# load the script as a module directly from its file path SPEC = importlib.util.spec_from_file_location("preview_scene", SCRIPT_PATH) assert SPEC is not None and SPEC.loader is not None preview_scene = importlib.util.module_from_spec(SPEC) SPEC.loader.exec_module(preview_scene) +# write a tiny manim script with one scene class def _scene_script(path: Path, name: str = "Demo") -> Path: path.parent.mkdir(parents=True, exist_ok=True) path.write_text( @@ -35,6 +40,7 @@ def _scene_script(path: Path, name: str = "Demo") -> Path: return path +# an unknown scene class fails before any render def test_preview_rejects_missing_scene_before_render(tmp_path: Path) -> None: script = _scene_script(tmp_path / "edit" / "scene.py") @@ -42,6 +48,7 @@ def test_preview_rejects_missing_scene_before_render(tmp_path: Path) -> None: preview_scene.render_scene(script, "Missing") +# a missing manim executable fails with a clear message def test_preview_fails_clearly_when_manim_is_absent(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: script = _scene_script(tmp_path / "edit" / "scene.py") monkeypatch.setattr(preview_scene.shutil, "which", lambda _name: None) @@ -50,9 +57,11 @@ def test_preview_fails_clearly_when_manim_is_absent(tmp_path: Path, monkeypatch: preview_scene.render_scene(script, "Demo") +# a non zero render exit code surfaces the stderr text def test_preview_reports_render_failure(tmp_path: Path) -> None: script = _scene_script(tmp_path / "edit" / "scene.py") + # stand in for the manim subprocess that fails with an error message def failed_runner(*_args, **_kwargs): return subprocess.CompletedProcess([], 2, stdout="", stderr="render exploded") @@ -67,9 +76,11 @@ def failed_runner(*_args, **_kwargs): ) +# a successful run with no output video is rejected def test_preview_rejects_success_without_expected_artifacts(tmp_path: Path) -> None: script = _scene_script(tmp_path / "edit" / "scene.py") + # stand in for the manim subprocess that succeeds without writing anything def successful_runner(*_args, **_kwargs): return subprocess.CompletedProcess([], 0, stdout="", stderr="") @@ -84,6 +95,7 @@ def successful_runner(*_args, **_kwargs): ) +# a real render lands under edit verify with the expected metadata and artifacts def test_preview_real_manim_render_stays_under_edit_verify(tmp_path: Path) -> None: pytest.importorskip("manim") manim = shutil.which("manim") diff --git a/uv.lock b/uv.lock new file mode 100644 index 00000000..94fb01d6 --- /dev/null +++ b/uv.lock @@ -0,0 +1,2751 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" +resolution-markers = [ + "python_full_version >= '3.13'", + "python_full_version == '3.12.*'", + "python_full_version == '3.11.*'", + "python_full_version < '3.11'", +] + +[[package]] +name = "audioop-lts" +version = "0.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/38/53/946db57842a50b2da2e0c1e34bd37f36f5aadba1a929a3971c5d7841dbca/audioop_lts-0.2.2.tar.gz", hash = "sha256:64d0c62d88e67b98a1a5e71987b7aa7b5bcffc7dcee65b635823dbdd0a8dbbd0", size = 30686, upload-time = "2025-08-05T16:43:17.409Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/d4/94d277ca941de5a507b07f0b592f199c22454eeaec8f008a286b3fbbacd6/audioop_lts-0.2.2-cp313-abi3-macosx_10_13_universal2.whl", hash = "sha256:fd3d4602dc64914d462924a08c1a9816435a2155d74f325853c1f1ac3b2d9800", size = 46523, upload-time = "2025-08-05T16:42:20.836Z" }, + { url = "https://files.pythonhosted.org/packages/f8/5a/656d1c2da4b555920ce4177167bfeb8623d98765594af59702c8873f60ec/audioop_lts-0.2.2-cp313-abi3-macosx_10_13_x86_64.whl", hash = "sha256:550c114a8df0aafe9a05442a1162dfc8fec37e9af1d625ae6060fed6e756f303", size = 27455, upload-time = "2025-08-05T16:42:22.283Z" }, + { url = "https://files.pythonhosted.org/packages/1b/83/ea581e364ce7b0d41456fb79d6ee0ad482beda61faf0cab20cbd4c63a541/audioop_lts-0.2.2-cp313-abi3-macosx_11_0_arm64.whl", hash = "sha256:9a13dc409f2564de15dd68be65b462ba0dde01b19663720c68c1140c782d1d75", size = 26997, upload-time = "2025-08-05T16:42:23.849Z" }, + { url = "https://files.pythonhosted.org/packages/b8/3b/e8964210b5e216e5041593b7d33e97ee65967f17c282e8510d19c666dab4/audioop_lts-0.2.2-cp313-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:51c916108c56aa6e426ce611946f901badac950ee2ddaf302b7ed35d9958970d", size = 85844, upload-time = "2025-08-05T16:42:25.208Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2e/0a1c52faf10d51def20531a59ce4c706cb7952323b11709e10de324d6493/audioop_lts-0.2.2-cp313-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:47eba38322370347b1c47024defbd36374a211e8dd5b0dcbce7b34fdb6f8847b", size = 85056, upload-time = "2025-08-05T16:42:26.559Z" }, + { url = "https://files.pythonhosted.org/packages/75/e8/cd95eef479656cb75ab05dfece8c1f8c395d17a7c651d88f8e6e291a63ab/audioop_lts-0.2.2-cp313-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba7c3a7e5f23e215cb271516197030c32aef2e754252c4c70a50aaff7031a2c8", size = 93892, upload-time = "2025-08-05T16:42:27.902Z" }, + { url = "https://files.pythonhosted.org/packages/5c/1e/a0c42570b74f83efa5cca34905b3eef03f7ab09fe5637015df538a7f3345/audioop_lts-0.2.2-cp313-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:def246fe9e180626731b26e89816e79aae2276f825420a07b4a647abaa84becc", size = 96660, upload-time = "2025-08-05T16:42:28.9Z" }, + { url = "https://files.pythonhosted.org/packages/50/d5/8a0ae607ca07dbb34027bac8db805498ee7bfecc05fd2c148cc1ed7646e7/audioop_lts-0.2.2-cp313-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e160bf9df356d841bb6c180eeeea1834085464626dc1b68fa4e1d59070affdc3", size = 79143, upload-time = "2025-08-05T16:42:29.929Z" }, + { url = "https://files.pythonhosted.org/packages/12/17/0d28c46179e7910bfb0bb62760ccb33edb5de973052cb2230b662c14ca2e/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4b4cd51a57b698b2d06cb9993b7ac8dfe89a3b2878e96bc7948e9f19ff51dba6", size = 84313, upload-time = "2025-08-05T16:42:30.949Z" }, + { url = "https://files.pythonhosted.org/packages/84/ba/bd5d3806641564f2024e97ca98ea8f8811d4e01d9b9f9831474bc9e14f9e/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4a53aa7c16a60a6857e6b0b165261436396ef7293f8b5c9c828a3a203147ed4a", size = 93044, upload-time = "2025-08-05T16:42:31.959Z" }, + { url = "https://files.pythonhosted.org/packages/f9/5e/435ce8d5642f1f7679540d1e73c1c42d933331c0976eb397d1717d7f01a3/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:3fc38008969796f0f689f1453722a0f463da1b8a6fbee11987830bfbb664f623", size = 78766, upload-time = "2025-08-05T16:42:33.302Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3b/b909e76b606cbfd53875693ec8c156e93e15a1366a012f0b7e4fb52d3c34/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_s390x.whl", hash = "sha256:15ab25dd3e620790f40e9ead897f91e79c0d3ce65fe193c8ed6c26cffdd24be7", size = 87640, upload-time = "2025-08-05T16:42:34.854Z" }, + { url = "https://files.pythonhosted.org/packages/30/e7/8f1603b4572d79b775f2140d7952f200f5e6c62904585d08a01f0a70393a/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:03f061a1915538fd96272bac9551841859dbb2e3bf73ebe4a23ef043766f5449", size = 86052, upload-time = "2025-08-05T16:42:35.839Z" }, + { url = "https://files.pythonhosted.org/packages/b5/96/c37846df657ccdda62ba1ae2b6534fa90e2e1b1742ca8dcf8ebd38c53801/audioop_lts-0.2.2-cp313-abi3-win32.whl", hash = "sha256:3bcddaaf6cc5935a300a8387c99f7a7fbbe212a11568ec6cf6e4bc458c048636", size = 26185, upload-time = "2025-08-05T16:42:37.04Z" }, + { url = "https://files.pythonhosted.org/packages/34/a5/9d78fdb5b844a83da8a71226c7bdae7cc638861085fff7a1d707cb4823fa/audioop_lts-0.2.2-cp313-abi3-win_amd64.whl", hash = "sha256:a2c2a947fae7d1062ef08c4e369e0ba2086049a5e598fda41122535557012e9e", size = 30503, upload-time = "2025-08-05T16:42:38.427Z" }, + { url = "https://files.pythonhosted.org/packages/34/25/20d8fde083123e90c61b51afb547bb0ea7e77bab50d98c0ab243d02a0e43/audioop_lts-0.2.2-cp313-abi3-win_arm64.whl", hash = "sha256:5f93a5db13927a37d2d09637ccca4b2b6b48c19cd9eda7b17a2e9f77edee6a6f", size = 24173, upload-time = "2025-08-05T16:42:39.704Z" }, + { url = "https://files.pythonhosted.org/packages/58/a7/0a764f77b5c4ac58dc13c01a580f5d32ae8c74c92020b961556a43e26d02/audioop_lts-0.2.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:73f80bf4cd5d2ca7814da30a120de1f9408ee0619cc75da87d0641273d202a09", size = 47096, upload-time = "2025-08-05T16:42:40.684Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ed/ebebedde1a18848b085ad0fa54b66ceb95f1f94a3fc04f1cd1b5ccb0ed42/audioop_lts-0.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:106753a83a25ee4d6f473f2be6b0966fc1c9af7e0017192f5531a3e7463dce58", size = 27748, upload-time = "2025-08-05T16:42:41.992Z" }, + { url = "https://files.pythonhosted.org/packages/cb/6e/11ca8c21af79f15dbb1c7f8017952ee8c810c438ce4e2b25638dfef2b02c/audioop_lts-0.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fbdd522624141e40948ab3e8cdae6e04c748d78710e9f0f8d4dae2750831de19", size = 27329, upload-time = "2025-08-05T16:42:42.987Z" }, + { url = "https://files.pythonhosted.org/packages/84/52/0022f93d56d85eec5da6b9da6a958a1ef09e80c39f2cc0a590c6af81dcbb/audioop_lts-0.2.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:143fad0311e8209ece30a8dbddab3b65ab419cbe8c0dde6e8828da25999be911", size = 92407, upload-time = "2025-08-05T16:42:44.336Z" }, + { url = "https://files.pythonhosted.org/packages/87/1d/48a889855e67be8718adbc7a01f3c01d5743c325453a5e81cf3717664aad/audioop_lts-0.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dfbbc74ec68a0fd08cfec1f4b5e8cca3d3cd7de5501b01c4b5d209995033cde9", size = 91811, upload-time = "2025-08-05T16:42:45.325Z" }, + { url = "https://files.pythonhosted.org/packages/98/a6/94b7213190e8077547ffae75e13ed05edc488653c85aa5c41472c297d295/audioop_lts-0.2.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cfcac6aa6f42397471e4943e0feb2244549db5c5d01efcd02725b96af417f3fe", size = 100470, upload-time = "2025-08-05T16:42:46.468Z" }, + { url = "https://files.pythonhosted.org/packages/e9/e9/78450d7cb921ede0cfc33426d3a8023a3bda755883c95c868ee36db8d48d/audioop_lts-0.2.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:752d76472d9804ac60f0078c79cdae8b956f293177acd2316cd1e15149aee132", size = 103878, upload-time = "2025-08-05T16:42:47.576Z" }, + { url = "https://files.pythonhosted.org/packages/4f/e2/cd5439aad4f3e34ae1ee852025dc6aa8f67a82b97641e390bf7bd9891d3e/audioop_lts-0.2.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:83c381767e2cc10e93e40281a04852facc4cd9334550e0f392f72d1c0a9c5753", size = 84867, upload-time = "2025-08-05T16:42:49.003Z" }, + { url = "https://files.pythonhosted.org/packages/68/4b/9d853e9076c43ebba0d411e8d2aa19061083349ac695a7d082540bad64d0/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c0022283e9556e0f3643b7c3c03f05063ca72b3063291834cca43234f20c60bb", size = 90001, upload-time = "2025-08-05T16:42:50.038Z" }, + { url = "https://files.pythonhosted.org/packages/58/26/4bae7f9d2f116ed5593989d0e521d679b0d583973d203384679323d8fa85/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a2d4f1513d63c795e82948e1305f31a6d530626e5f9f2605408b300ae6095093", size = 99046, upload-time = "2025-08-05T16:42:51.111Z" }, + { url = "https://files.pythonhosted.org/packages/b2/67/a9f4fb3e250dda9e9046f8866e9fa7d52664f8985e445c6b4ad6dfb55641/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:c9c8e68d8b4a56fda8c025e538e639f8c5953f5073886b596c93ec9b620055e7", size = 84788, upload-time = "2025-08-05T16:42:52.198Z" }, + { url = "https://files.pythonhosted.org/packages/70/f7/3de86562db0121956148bcb0fe5b506615e3bcf6e63c4357a612b910765a/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:96f19de485a2925314f5020e85911fb447ff5fbef56e8c7c6927851b95533a1c", size = 94472, upload-time = "2025-08-05T16:42:53.59Z" }, + { url = "https://files.pythonhosted.org/packages/f1/32/fd772bf9078ae1001207d2df1eef3da05bea611a87dd0e8217989b2848fa/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e541c3ef484852ef36545f66209444c48b28661e864ccadb29daddb6a4b8e5f5", size = 92279, upload-time = "2025-08-05T16:42:54.632Z" }, + { url = "https://files.pythonhosted.org/packages/4f/41/affea7181592ab0ab560044632571a38edaf9130b84928177823fbf3176a/audioop_lts-0.2.2-cp313-cp313t-win32.whl", hash = "sha256:d5e73fa573e273e4f2e5ff96f9043858a5e9311e94ffefd88a3186a910c70917", size = 26568, upload-time = "2025-08-05T16:42:55.627Z" }, + { url = "https://files.pythonhosted.org/packages/28/2b/0372842877016641db8fc54d5c88596b542eec2f8f6c20a36fb6612bf9ee/audioop_lts-0.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9191d68659eda01e448188f60364c7763a7ca6653ed3f87ebb165822153a8547", size = 30942, upload-time = "2025-08-05T16:42:56.674Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ca/baf2b9cc7e96c179bb4a54f30fcd83e6ecb340031bde68f486403f943768/audioop_lts-0.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:c174e322bb5783c099aaf87faeb240c8d210686b04bd61dfd05a8e5a83d88969", size = 24603, upload-time = "2025-08-05T16:42:57.571Z" }, + { url = "https://files.pythonhosted.org/packages/5c/73/413b5a2804091e2c7d5def1d618e4837f1cb82464e230f827226278556b7/audioop_lts-0.2.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:f9ee9b52f5f857fbaf9d605a360884f034c92c1c23021fb90b2e39b8e64bede6", size = 47104, upload-time = "2025-08-05T16:42:58.518Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8c/daa3308dc6593944410c2c68306a5e217f5c05b70a12e70228e7dd42dc5c/audioop_lts-0.2.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:49ee1a41738a23e98d98b937a0638357a2477bc99e61b0f768a8f654f45d9b7a", size = 27754, upload-time = "2025-08-05T16:43:00.132Z" }, + { url = "https://files.pythonhosted.org/packages/4e/86/c2e0f627168fcf61781a8f72cab06b228fe1da4b9fa4ab39cfb791b5836b/audioop_lts-0.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5b00be98ccd0fc123dcfad31d50030d25fcf31488cde9e61692029cd7394733b", size = 27332, upload-time = "2025-08-05T16:43:01.666Z" }, + { url = "https://files.pythonhosted.org/packages/c7/bd/35dce665255434f54e5307de39e31912a6f902d4572da7c37582809de14f/audioop_lts-0.2.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6d2e0f9f7a69403e388894d4ca5ada5c47230716a03f2847cfc7bd1ecb589d6", size = 92396, upload-time = "2025-08-05T16:43:02.991Z" }, + { url = "https://files.pythonhosted.org/packages/2d/d2/deeb9f51def1437b3afa35aeb729d577c04bcd89394cb56f9239a9f50b6f/audioop_lts-0.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9b0b8a03ef474f56d1a842af1a2e01398b8f7654009823c6d9e0ecff4d5cfbf", size = 91811, upload-time = "2025-08-05T16:43:04.096Z" }, + { url = "https://files.pythonhosted.org/packages/76/3b/09f8b35b227cee28cc8231e296a82759ed80c1a08e349811d69773c48426/audioop_lts-0.2.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2b267b70747d82125f1a021506565bdc5609a2b24bcb4773c16d79d2bb260bbd", size = 100483, upload-time = "2025-08-05T16:43:05.085Z" }, + { url = "https://files.pythonhosted.org/packages/0b/15/05b48a935cf3b130c248bfdbdea71ce6437f5394ee8533e0edd7cfd93d5e/audioop_lts-0.2.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0337d658f9b81f4cd0fdb1f47635070cc084871a3d4646d9de74fdf4e7c3d24a", size = 103885, upload-time = "2025-08-05T16:43:06.197Z" }, + { url = "https://files.pythonhosted.org/packages/83/80/186b7fce6d35b68d3d739f228dc31d60b3412105854edb975aa155a58339/audioop_lts-0.2.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:167d3b62586faef8b6b2275c3218796b12621a60e43f7e9d5845d627b9c9b80e", size = 84899, upload-time = "2025-08-05T16:43:07.291Z" }, + { url = "https://files.pythonhosted.org/packages/49/89/c78cc5ac6cb5828f17514fb12966e299c850bc885e80f8ad94e38d450886/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0d9385e96f9f6da847f4d571ce3cb15b5091140edf3db97276872647ce37efd7", size = 89998, upload-time = "2025-08-05T16:43:08.335Z" }, + { url = "https://files.pythonhosted.org/packages/4c/4b/6401888d0c010e586c2ca50fce4c903d70a6bb55928b16cfbdfd957a13da/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:48159d96962674eccdca9a3df280e864e8ac75e40a577cc97c5c42667ffabfc5", size = 99046, upload-time = "2025-08-05T16:43:09.367Z" }, + { url = "https://files.pythonhosted.org/packages/de/f8/c874ca9bb447dae0e2ef2e231f6c4c2b0c39e31ae684d2420b0f9e97ee68/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8fefe5868cd082db1186f2837d64cfbfa78b548ea0d0543e9b28935ccce81ce9", size = 84843, upload-time = "2025-08-05T16:43:10.749Z" }, + { url = "https://files.pythonhosted.org/packages/3e/c0/0323e66f3daebc13fd46b36b30c3be47e3fc4257eae44f1e77eb828c703f/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:58cf54380c3884fb49fdd37dfb7a772632b6701d28edd3e2904743c5e1773602", size = 94490, upload-time = "2025-08-05T16:43:12.131Z" }, + { url = "https://files.pythonhosted.org/packages/98/6b/acc7734ac02d95ab791c10c3f17ffa3584ccb9ac5c18fd771c638ed6d1f5/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:088327f00488cdeed296edd9215ca159f3a5a5034741465789cad403fcf4bec0", size = 92297, upload-time = "2025-08-05T16:43:13.139Z" }, + { url = "https://files.pythonhosted.org/packages/13/c3/c3dc3f564ce6877ecd2a05f8d751b9b27a8c320c2533a98b0c86349778d0/audioop_lts-0.2.2-cp314-cp314t-win32.whl", hash = "sha256:068aa17a38b4e0e7de771c62c60bbca2455924b67a8814f3b0dee92b5820c0b3", size = 27331, upload-time = "2025-08-05T16:43:14.19Z" }, + { url = "https://files.pythonhosted.org/packages/72/bb/b4608537e9ffcb86449091939d52d24a055216a36a8bf66b936af8c3e7ac/audioop_lts-0.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:a5bf613e96f49712073de86f20dbdd4014ca18efd4d34ed18c75bd808337851b", size = 31697, upload-time = "2025-08-05T16:43:15.193Z" }, + { url = "https://files.pythonhosted.org/packages/f6/22/91616fe707a5c5510de2cac9b046a30defe7007ba8a0c04f9c08f27df312/audioop_lts-0.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:b492c3b040153e68b9fdaff5913305aaaba5bb433d8a7f73d5cf6a64ed3cc1dd", size = 25206, upload-time = "2025-08-05T16:43:16.444Z" }, +] + +[[package]] +name = "audioread" +version = "3.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/4a/874ecf9b472f998130c2b5e145dcdb9f6131e84786111489103b66772143/audioread-3.1.0.tar.gz", hash = "sha256:1c4ab2f2972764c896a8ac61ac53e261c8d29f0c6ccd652f84e18f08a4cab190", size = 20082, upload-time = "2025-10-26T19:44:13.484Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/16/fbe8e1e185a45042f7cd3a282def5bb8d95bb69ab9e9ef6a5368aa17e426/audioread-3.1.0-py3-none-any.whl", hash = "sha256:b30d1df6c5d3de5dcef0fb0e256f6ea17bdcf5f979408df0297d8a408e2971b4", size = 23143, upload-time = "2025-10-26T19:44:12.016Z" }, +] + +[[package]] +name = "av" +version = "13.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/0c/9d/486d31e76784cc0ad943f420c5e05867263b32b37e2f4b0f7f22fdc1ca3a/av-13.1.0.tar.gz", hash = "sha256:d3da736c55847d8596eb8c26c60e036f193001db3bc5c10da8665622d906c17e", size = 3957908, upload-time = "2024-10-06T04:54:57.507Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/d6/1c4a8056a88e006681ac6a3d5ac6082f0a48e52bd565bfd350bfc7c6a37d/av-13.1.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:a2af44fae6d16c3a40dd1c85bda41b449be08a2c172d8f44fb63395ccf6e6fb4", size = 24260057, upload-time = "2024-10-06T04:52:26.417Z" }, + { url = "https://files.pythonhosted.org/packages/23/be/cf89545117172d75a0c48066e6f368403237df623b2e3e93590fdeaef8bf/av-13.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0fea71fe06fd0dfe90a089200eb6468034797f860a321fa2d62e07d619c74749", size = 19475039, upload-time = "2024-10-06T04:52:29.615Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d0/8e261547f7763f320a4f5f68e139fea5f31814fddfe5503c8372123ebb8b/av-13.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:756997810dcca11811b598d209561cabd2071e5b472b867c295bb3e7022eecde", size = 31289005, upload-time = "2024-10-06T04:52:33.042Z" }, + { url = "https://files.pythonhosted.org/packages/82/a3/00cacfe80ebbe0664876dd26558fb23b65d034ffd2ce0ddb12f1c746e7cb/av-13.1.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f432102edaec4ee90087a675acf486bff0c81b47d98b85eb3218afe84575b60", size = 30705668, upload-time = "2024-10-06T04:52:36.63Z" }, + { url = "https://files.pythonhosted.org/packages/d7/37/faa98dca1a8f6c2e3f4ad3a935037872aff49a679b76918c5258cf5a1c70/av-13.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d568c4d7a36df52c0774d52e6d730148775ead16daed81c10dafc2569b5a38d", size = 33122108, upload-time = "2024-10-06T04:52:40.296Z" }, + { url = "https://files.pythonhosted.org/packages/25/81/c3a842477b558e23c7249f81cf723764c193636b6523267c2c02321da6b0/av-13.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:aa6f76e7c5e77bc5f99a27ada29f78c64fd4e0d42da2c4d203badc650bc0a686", size = 25775920, upload-time = "2024-10-06T04:52:44.067Z" }, + { url = "https://files.pythonhosted.org/packages/39/54/c4227080c9700384db90072ace70d89b6a288b3748bd2ec0e32580a49e7f/av-13.1.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:867385e6701464a5c95903e24d2e0df1c7e0dbf211ed91d0ce639cd687373e10", size = 24255112, upload-time = "2024-10-06T04:52:48.49Z" }, + { url = "https://files.pythonhosted.org/packages/32/4a/eb9348231655ca99b200b380f4edbceff7358c927a285badcc84b18fb1c9/av-13.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cb7a3f319401a46b0017771268ff4928501e77cf00b1a2aa0721e20b2fd1146e", size = 19467930, upload-time = "2024-10-06T04:52:52.118Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/48c80252bdbc3a75a54dd205a7fab8f613914009b9e5416202757208e040/av-13.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad904f860147bceaca65b0d3174a8153f35c570d465161d210f1879970b15559", size = 32207671, upload-time = "2024-10-06T04:52:55.82Z" }, + { url = "https://files.pythonhosted.org/packages/f9/66/3332c7fa8c43b65680a94f279ea3e832b5500de3a1392bac6112881e984b/av-13.1.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a906e017b29d0eb80d9ccf7a98d19268122da792dbb68eb741cfebba156e6aed", size = 31520911, upload-time = "2024-10-06T04:52:59.231Z" }, + { url = "https://files.pythonhosted.org/packages/e5/bb/2e03acb9b27591d97f700a3a6c27cfd1bc53fa148177747eda8a70cca1e9/av-13.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ce894d7847897da7be63277a0875bd93c51327134ac226c67978de014c7979f", size = 34048399, upload-time = "2024-10-06T04:53:03.934Z" }, + { url = "https://files.pythonhosted.org/packages/85/44/527aa3b65947d42cfe829326026edf0cd1a8c459390076034be275616c36/av-13.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:384bcdb5fc3238a263a5a25cc9efc690859fa4148cc4b07e00fae927178db22a", size = 25779569, upload-time = "2024-10-06T04:53:07.582Z" }, + { url = "https://files.pythonhosted.org/packages/9b/aa/4bdd8ce59173574fc6e0c282c71ee6f96fca82643d97bf172bc4cb5a5674/av-13.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:261dbc3f4b55f4f8f3375b10b2258fca7f2ab7a6365c01bc65e77a0d5327a195", size = 24268674, upload-time = "2024-10-06T04:53:11.251Z" }, + { url = "https://files.pythonhosted.org/packages/17/b4/b267dd5bad99eed49ec6731827c6bcb5ab03864bf732a7ebb81e3df79911/av-13.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83d259ef86b9054eb914bc7c6a7f6092a6d75cb939295e70ee979cfd92a67b99", size = 19475617, upload-time = "2024-10-06T04:53:13.832Z" }, + { url = "https://files.pythonhosted.org/packages/68/32/4209e51f54d7b54a1feb576d309c671ed1ff437b54fcc4ec68c239199e0a/av-13.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3b4d3ca159eceab97e3c0fb08fe756520fb95508417f76e48198fda2a5b0806", size = 32468873, upload-time = "2024-10-06T04:53:17.639Z" }, + { url = "https://files.pythonhosted.org/packages/b6/d8/c174da5f06b24f3c9e36f91fd02a7411c39da9ce792c17964260d4be675e/av-13.1.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40e8f757e373b73a2dc4640852a00cce4a4a92ef19b2e642a96d6994cd1fffbf", size = 31818484, upload-time = "2024-10-06T04:53:21.509Z" }, + { url = "https://files.pythonhosted.org/packages/7f/22/0dd8d1d5cad415772bb707d16aea8b81cf75d340d11d3668eea43468c730/av-13.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8aaec2c0bfd024359db3821d679009d4e637e1bee0321d20f61c54ed6b20f41", size = 34398652, upload-time = "2024-10-06T04:53:25.798Z" }, + { url = "https://files.pythonhosted.org/packages/7b/ff/48fa68888b8d5bae36d915556ff18f9e5fdc6b5ff5ae23dc4904c9713168/av-13.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:5ea0deab0e6a739cb742fba2a3983d8102f7516a3cdf3c46669f3cac0ed1f351", size = 25781343, upload-time = "2024-10-06T04:53:29.577Z" }, + { url = "https://files.pythonhosted.org/packages/82/6e/cdce12e534570df37d3fdcb3a74851d39e9ab79d388f3174dea9785a011a/av-13.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:47642ebaebfe20519b2391bd5b7c38b596efcd052bfd09c8d33058f94ddd0fd6", size = 24229340, upload-time = "2024-10-06T04:53:33.25Z" }, + { url = "https://files.pythonhosted.org/packages/7c/88/5359aeada9ea509426f2db63b6531833824a1b02470667b103479ddea7ae/av-13.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2f079c2daa3ae06557b3f6e9bed4fb9c876e8012175bec645ccd007199a302db", size = 19436445, upload-time = "2024-10-06T04:53:36.573Z" }, + { url = "https://files.pythonhosted.org/packages/b4/d4/64995e5b800476c86dae4ea1444a0eac44e2c4985fac6401b08401e2df11/av-13.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f0de8252deeeb1887637e88d4d9d18514e5cfe276bdb9e6ca8e9eef89d1667a", size = 32120549, upload-time = "2024-10-06T04:53:39.752Z" }, + { url = "https://files.pythonhosted.org/packages/68/76/9910694cf87d2d308d851f5b2b5c5b20f7f55411f596e2c158fb13bf84a3/av-13.1.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9ad0024f4def11b0cedfeee478fa6c6fd7ed3955e13387e0f27261fdda6121b4", size = 31495305, upload-time = "2024-10-06T04:53:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/6a/a8/cd92de947b9595a0eb2c64e6f7ba295aac2687972050ae092173c2f6ea0c/av-13.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb88e2590eaed45233eb117f1dfab1a43ed9a997b2c46da9f08468dd00f14895", size = 34065325, upload-time = "2024-10-06T04:53:47.25Z" }, + { url = "https://files.pythonhosted.org/packages/9d/d0/9869fcbd66422df2033d4b78a663e3c64aa6fe7eb9189c811d60f69d9871/av-13.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:c927e4fa4f6aeed4340b3e3b16b237d7cb743e5c1a55b92307407590ca4112aa", size = 25754728, upload-time = "2024-10-06T04:53:50.603Z" }, + { url = "https://files.pythonhosted.org/packages/63/62/09859d91bc2309918d548ac4585973c53e7db27010c432d050f02206f9bd/av-13.1.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fc5118f78ee712b2c396f345e4c51e60e61e28f1f606adbd4060c4dc44b0b652", size = 23861117, upload-time = "2024-10-06T04:54:17.122Z" }, + { url = "https://files.pythonhosted.org/packages/c7/43/f186435a0acad3a2bdf271ce51d3af97ac3153a410e54a623529d39a1818/av-13.1.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:81bcbd3545e523e7a350613be1866b515a5ee3fafa1d9d257d7ed02531fc2636", size = 19115008, upload-time = "2024-10-06T04:54:20.581Z" }, + { url = "https://files.pythonhosted.org/packages/31/eb/a1b4af95a615ba73dfc3cfcb9387e40826c92d7d6d383a1b68685a7ef920/av-13.1.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83b2bc641e8e16bbf058de35f1ba79ebed358ac6fe3cb5a665366294774fdb18", size = 22852637, upload-time = "2024-10-06T04:54:23.404Z" }, + { url = "https://files.pythonhosted.org/packages/0b/a6/94a34aa672af7fef2939e4a5d6c4c6c28e33da0c623aaa9485d977eeaa95/av-13.1.0-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d956ae3e68fabdc45eb2b986c2e842a31df084d8cfc90336509f07a727a9df62", size = 22703888, upload-time = "2024-10-06T04:54:26.921Z" }, + { url = "https://files.pythonhosted.org/packages/b9/69/08a72ceed2c8a6e689dea2ef8e941df9469cbe144a600b83d45f821477fc/av-13.1.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ef076fcdf172aafcb21ea3ef7bd68cc9151b050016a8ace13b3dae3d08a4427", size = 24657784, upload-time = "2024-10-06T04:54:30.244Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8c/c20894580a4341a76c7c74b59c43e26e6652b0fc60f7248f2c1bc5fdbb5e/av-13.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:bbf90397b7a466ff2879bd0944d55f796ad76c073fce50304315b83ad00113bd", size = 25562492, upload-time = "2024-10-06T04:54:33.711Z" }, +] + +[[package]] +name = "av" +version = "18.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13'", + "python_full_version == '3.12.*'", + "python_full_version == '3.11.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/8d/f4/f22114d30d3435e38c6af2b4870f37b864403dca6ae7af747a289ce0a18e/av-18.1.0.tar.gz", hash = "sha256:47bfc286e1bc9de7ab4681fc2b575cd2460a66919d31ffe1bd5aa54fae531a28", size = 4451061, upload-time = "2026-08-12T22:28:18.761Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/d4/d7cdc8bff143c17a6d35924375ae28dd692cacde38700a7d419fde54f44a/av-18.1.0-cp311-abi3-macosx_11_0_x86_64.whl", hash = "sha256:ae75d8bb6467895ed1f8572ededf7ffa49eac07f6e483222f5d7d62a41d12f04", size = 22546147, upload-time = "2026-08-12T22:27:11.851Z" }, + { url = "https://files.pythonhosted.org/packages/3f/c9/37a619297492256b77d5ed906e7d8166c10a26ed251dccf1ae03ab19bff6/av-18.1.0-cp311-abi3-macosx_14_0_arm64.whl", hash = "sha256:b30a4e8d934558e19602b68998a4d9ac9f250fa0dacef216f7e8e40153b13316", size = 18217603, upload-time = "2026-08-12T22:27:14.713Z" }, + { url = "https://files.pythonhosted.org/packages/d9/84/2464ffb64c08c5ce8b522c8e74594714414e3b0575267652c5c51c0574b9/av-18.1.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6fc837cc51adf80331ac850779cd53b5d4c4460b0ebe9057a02a921c6736f19d", size = 33640142, upload-time = "2026-08-12T22:27:17.835Z" }, + { url = "https://files.pythonhosted.org/packages/27/3a/204dbfc3e08eb4cdc6e6ff57be02150bc44523ebdb50182d10025792ebd9/av-18.1.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:8a032e8d8ebc73dec079364b9b4a6837638a2d106e8472314e685ffbf163e700", size = 35786210, upload-time = "2026-08-12T22:27:20.984Z" }, + { url = "https://files.pythonhosted.org/packages/e1/99/b0d04ec553ff9a7e00455458dfa3a39c8a8f627b273056b4e5fe57d590de/av-18.1.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:3c8b1f8b46f99d52e2d8b0ed5d0cdadf172d24794d46e2077b16e44ed08e26ff", size = 39379798, upload-time = "2026-08-12T22:27:24.432Z" }, + { url = "https://files.pythonhosted.org/packages/56/b1/e00d4feae59160149df6126585e726fdc6300798fd40c5dd324879e81f68/av-18.1.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ab5ac081bc9eaf54109120d4e56284674fecfbe520d9aa1707c7fa911ec5f4d2", size = 34690321, upload-time = "2026-08-12T22:27:27.769Z" }, + { url = "https://files.pythonhosted.org/packages/dc/94/836fa987e3084d11a21489f11357fb24843ef3aa8faf74ddddfc603d5062/av-18.1.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:191224788d87af06c31784a395bb73f14b72f33d7f4871ace0157de2abdc6276", size = 36859932, upload-time = "2026-08-12T22:27:31.403Z" }, + { url = "https://files.pythonhosted.org/packages/33/b4/76ba21e46704f632004276b85289a1582e95f5eff760436d6149875a1881/av-18.1.0-cp311-abi3-win_amd64.whl", hash = "sha256:ea1480b7a8d5405cb5f382b344731bf125fd2c1c6fae3964f6c48595628387ff", size = 27595679, upload-time = "2026-08-12T22:27:35.177Z" }, + { url = "https://files.pythonhosted.org/packages/4f/ad/a3135884c5753b09773176b97201ae602f67ad14206c395ff838d66bf9b0/av-18.1.0-cp311-abi3-win_arm64.whl", hash = "sha256:5509ec12aaa19fd6601de13cfa6f4cdad450da07982118510592875d970454d6", size = 20257584, upload-time = "2026-08-12T22:27:38.472Z" }, + { url = "https://files.pythonhosted.org/packages/4f/5b/4a756265d7fb164336c8d377bca21c39cfa2c178be23cedee840a69b59c5/av-18.1.0-cp314-cp314t-macosx_11_0_x86_64.whl", hash = "sha256:b36b0bae9e4c62f9487c99481ec15e4e3870fcc868522cd6d18fc2d6bfa04f01", size = 22795654, upload-time = "2026-08-12T22:27:42.016Z" }, + { url = "https://files.pythonhosted.org/packages/d5/cc/1bc841462114a1adf4f7d87456ab78a6972e23271e71865fcd2bbd0e7360/av-18.1.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:025f84494cb23278498f03b0d8117d3e47a1cbc9c44b97eb31875cf02251e46b", size = 18435735, upload-time = "2026-08-12T22:27:45.787Z" }, + { url = "https://files.pythonhosted.org/packages/b8/20/005500ed17a2e62a5e4bb94aa3786942560ec2f55ec1895ebf174c87abef/av-18.1.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:08a9ae288299cfcbf739dba4ad0c53b9b71f45184303dd45947920d022fed695", size = 37090807, upload-time = "2026-08-12T22:27:50.14Z" }, + { url = "https://files.pythonhosted.org/packages/5c/f7/11e7f6d848d3690c31ca4f8578167393e619177f1493ccc93b9400852d4e/av-18.1.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:cf8a17466bef07765dbdecc9e66ed9b25d20b4e14f654fbf35345a58ac45fa0c", size = 38976836, upload-time = "2026-08-12T22:27:54.565Z" }, + { url = "https://files.pythonhosted.org/packages/c3/63/b271473b24e806062d31191e40c6d65545e9cf59f80f044eba56dcbba0f4/av-18.1.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d49a5c542dfdc00f43c6cdb6cc41dac1781ee206fe180b56aa7433dfa816dfae", size = 40896630, upload-time = "2026-08-12T22:27:59.118Z" }, + { url = "https://files.pythonhosted.org/packages/6b/9f/2ab7fa292a947ad3466ed8e655eefa3b82f535d7ea598c297b4471a937c4/av-18.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5548b79e2bf1f59b3e9aedc918a72d9dc45b9adaac10ff9470d5dbdda0002e47", size = 37895673, upload-time = "2026-08-12T22:28:03.98Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d8/04507c57249b399c3e4f23f01d221532f357338b5316fd2858fbd343127d/av-18.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7ea063f6690193ea335a1d592d6e0274350d45e2ed6af83ee107cb90cbfd84f", size = 39992431, upload-time = "2026-08-12T22:28:08.736Z" }, + { url = "https://files.pythonhosted.org/packages/d6/d6/bc4b95bea9c2353a7e4d62a3fcfad9adcf0f881741c6ce01ee179d539ce3/av-18.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e4d48b9f12cad009cc72fe4f4099107de5e819c95f82767f4fd01a01481c0661", size = 28497798, upload-time = "2026-08-12T22:28:13.003Z" }, + { url = "https://files.pythonhosted.org/packages/c1/d2/0c277a46f12647c1833f40496e132fb6001e0d19e6144b5ea30896461feb/av-18.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:5cd9085028902c9880622bd37a12fd4b33060f06a52311f6f4867ca9f29a2c3b", size = 21421979, upload-time = "2026-08-12T22:28:16.48Z" }, +] + +[[package]] +name = "beautifulsoup4" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/65/318323f98dbee45d42dff61d8f047181bc6f2268a9068cfad035a46be5af/beautifulsoup4-4.15.0.tar.gz", hash = "sha256:288e3ca7d54b06f2ac191970bc275c1939cb46d450b255bf6718b04aa37ab4f7", size = 632571, upload-time = "2026-06-07T16:44:20.453Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/c6/92fcd42f1ba33e1184263f25bfabf3d27c383410470f169e4b8163bf9c17/beautifulsoup4-4.15.0-py3-none-any.whl", hash = "sha256:d6f88de62e1d4e38ecb1077eb9724cd0eff29d2a08ca16a401e9b9e93f117cf9", size = 109924, upload-time = "2026-06-07T16:44:21.566Z" }, +] + +[[package]] +name = "certifi" +version = "2026.7.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" }, +] + +[[package]] +name = "cffi" +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/ef/008a1939e372c06329a3fce4279c02f328488f3526744906eeec3da7ad5f/cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be", size = 530807, upload-time = "2026-08-03T21:21:18.939Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/d2/2cde336b375f55c76ca670f0be3978cc048e31e24f3b4d7ce8473150a388/cffi-2.1.1-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:baed1e86cc735622097354b9d1281406caf42ff42a886d29faa8e8d1630333be", size = 183779, upload-time = "2026-08-03T21:19:15.602Z" }, + { url = "https://files.pythonhosted.org/packages/94/1a/4b2f7c92293ba05cbd4a9a1b28faaf0326272d9488e6354657571c48a7aa/cffi-2.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ca82be1a1d406ecfe1d25dc16cb33488e5a16bf4438c9fb590484ea29d92478b", size = 184178, upload-time = "2026-08-03T21:19:16.67Z" }, + { url = "https://files.pythonhosted.org/packages/17/0b/ba385d8ccedf926c3cd06e8e2f327027da5afe5f0eb30f1f7bc43ac55125/cffi-2.1.1-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:42e2f76b9455f5a9a844f770bf3e200ed3da0e15f5df3db9c31fe80b04b3d004", size = 211037, upload-time = "2026-08-03T21:19:17.705Z" }, + { url = "https://files.pythonhosted.org/packages/a3/b9/0f2e58b2cefa33255bff36935d42b13180fe559bba82596540eb404bde7d/cffi-2.1.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5a59cc1c4442bc3d5c703bf720b51138d0bfc173618807c9ee2490a7541dd3d9", size = 218652, upload-time = "2026-08-03T21:19:18.735Z" }, + { url = "https://files.pythonhosted.org/packages/37/15/180e0dab27b9312c7479003d14c9e547634b7dcb934e2cc4650e1b131a7a/cffi-2.1.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:9f8d177621de5cb38ee3e731eda45d421db093ec0739f46a5594babda7987a98", size = 205422, upload-time = "2026-08-03T21:19:19.96Z" }, + { url = "https://files.pythonhosted.org/packages/18/d4/03026f0c850cbbaa9030750490225b4a7f4d524ea4df72c3cc740a90f4ef/cffi-2.1.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:75f80557d1389eddbd0de2681f6a390a0c5338c31ddaa821381c203fc3fd50d9", size = 205444, upload-time = "2026-08-03T21:19:21.246Z" }, + { url = "https://files.pythonhosted.org/packages/75/77/60bebf6f818bec84210ac5b6979ce4eeadce6fbbaabc9c7ab23e506d1ce5/cffi-2.1.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:194cffa889098ced9976c3fc6340305e43f6303657d298da55366907c05c22d6", size = 218742, upload-time = "2026-08-03T21:19:22.523Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ae/679bf47e73fd77b352171727f07de559a003f14de5d02b904a6ec1fa73ca/cffi-2.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5bb4e7ea95dcd6a014a6fef62e62467d67d8e582326443f3d68e71d6320a9fcf", size = 221054, upload-time = "2026-08-03T21:19:23.694Z" }, + { url = "https://files.pythonhosted.org/packages/09/b8/eefc0e06913b70aa153bf74c946094a18f58fd4aff11b7f372bfdfdca050/cffi-2.1.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3d22a20b1fb1632cc72c22f95f7b0d2961c3e1c235f245ba4c606c4771035659", size = 213489, upload-time = "2026-08-03T21:19:24.922Z" }, + { url = "https://files.pythonhosted.org/packages/6f/13/4e56852824a03cdf68523a35686f1c28eacd4bd30a7b0a78e682e6e6e1d3/cffi-2.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1dea0e4d7d4f11f619fe8c1d76caf49e24405b4b5743c0e3be16a500ecd930c9", size = 220241, upload-time = "2026-08-03T21:19:26.214Z" }, + { url = "https://files.pythonhosted.org/packages/99/7f/040f9e163e4acac3ee3d85b02d00b2576e7ca980d8785f0a3a5f1a9bf7f5/cffi-2.1.1-cp310-cp310-win32.whl", hash = "sha256:7ce713ace7c0e4520535b42b77eaa742c16dab813978064913e5a3cf82973b41", size = 174578, upload-time = "2026-08-03T21:19:27.338Z" }, + { url = "https://files.pythonhosted.org/packages/ba/0b/644a2ec1a4eaba49c2939410bb1eb1d25b09d6d0582f5d2f95c537043725/cffi-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:a48d62ab9d6f4f98c983223a547af44be6ca3691074c31cecced6facd3ba2dc1", size = 185082, upload-time = "2026-08-03T21:19:28.409Z" }, + { url = "https://files.pythonhosted.org/packages/70/d2/16d99a0c4948febc0ebd133a13b2f688ff7f8cb04da971e1128872ce0c03/cffi-2.1.1-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:c8d2c9fd1f2d16f780d15127abb050d13d1a76c03a4bd87d7e4980e45e511e12", size = 183838, upload-time = "2026-08-03T21:19:29.637Z" }, + { url = "https://files.pythonhosted.org/packages/cd/95/31b535a9f0220ae9f357de4a08d57ce89cb417653c2fd9f075f50822a388/cffi-2.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:398aff33cee2767e3e781d2554c54bd0dff386bb437581e0d8011fde1a942ec1", size = 184168, upload-time = "2026-08-03T21:19:30.764Z" }, + { url = "https://files.pythonhosted.org/packages/ad/5a/4707a0dc1f203f5dde5a907b0d4e3c25d71120241048bd5bc6f1bb9d4e71/cffi-2.1.1-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:154852545011f779917b11c78db2358d095da62a9a172b78ad0a583ee5adc0d0", size = 211805, upload-time = "2026-08-03T21:19:31.867Z" }, + { url = "https://files.pythonhosted.org/packages/ad/66/c19feabb28485b6e0bbaaafa90837a1ef5d302e90f2178bd33f17a49879b/cffi-2.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3311ed60d36f83378794e1009ac6258bafbf81f7888b4caa7b35a521e3f95813", size = 218716, upload-time = "2026-08-03T21:19:32.896Z" }, + { url = "https://files.pythonhosted.org/packages/a7/92/500760486c8baab49a7a8a58ba7fc3355ec3974b454b8a09e528efde9e1d/cffi-2.1.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6e192623c49c94421616a5778fba35cf0d5a8d000650c1967ef4448ee5cdd990", size = 205569, upload-time = "2026-08-03T21:19:34.142Z" }, + { url = "https://files.pythonhosted.org/packages/a5/a7/a67c733254d6e7373f7822f8082d8d6beade791e0cf12a7611f376fa61c7/cffi-2.1.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a6e721d4b0e45d5b65e87534470e67b18dcd092c83f68fba09f152b9cbc061af", size = 204907, upload-time = "2026-08-03T21:19:35.174Z" }, + { url = "https://files.pythonhosted.org/packages/f7/a4/4399daaf8f7dfee9d7c3327fdb0426ee041cc63edc358b93911ceb2bfc7a/cffi-2.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:34e261f78cb6ceaaa36f42f2613f4380d94d9c759a9c73c769ee6e0247364632", size = 217807, upload-time = "2026-08-03T21:19:36.286Z" }, + { url = "https://files.pythonhosted.org/packages/28/f7/dabe6da2466ecbd82dc62e7342dc6b1065dad990c06f00f0ede9ebf2a0ed/cffi-2.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7225e4514edb64eb6740324353e0da0711954fd8d7da4576755b1c6e09b697cd", size = 221252, upload-time = "2026-08-03T21:19:37.416Z" }, + { url = "https://files.pythonhosted.org/packages/ce/87/616202d8e51342c07d2534c510111c4cc37201775ce8f60802c9335d1edd/cffi-2.1.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:df913725b79db7bcf03448f36b7bf8815363417d5b58deecf9305e3e30f0f21a", size = 214214, upload-time = "2026-08-03T21:19:38.507Z" }, + { url = "https://files.pythonhosted.org/packages/b4/c6/ab025d75d2c26c19b087c0124e75ee31cb65032f4fe345d356d8c507ab97/cffi-2.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f5cfbc5fe74540d335175b656c725d74d90e3730c626d92575eea35029d9afaa", size = 219408, upload-time = "2026-08-03T21:19:39.809Z" }, + { url = "https://files.pythonhosted.org/packages/db/e2/7e8109f65445bdc673a7b54f02c677de462db75674220fd1335efc8eb598/cffi-2.1.1-cp311-cp311-win32.whl", hash = "sha256:f8ec5e643a9a937f64e1999eb9f75d072263751912dc5cd06d3c85f8f44be7c3", size = 174470, upload-time = "2026-08-03T21:19:41.246Z" }, + { url = "https://files.pythonhosted.org/packages/73/c0/77ba02423c2f7d7091143c45cd49e0e6575c4c1967394bb542bd923a9b74/cffi-2.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:42f6930c31dc7f50732c9ae793c2786c7b6b044195967bbdde40bb9be81c4cc0", size = 185096, upload-time = "2026-08-03T21:19:42.615Z" }, + { url = "https://files.pythonhosted.org/packages/7c/47/9f1f85f9672ceda4984dc6c4f8824e8558992a2972c3d3c81fb8eb28d4ba/cffi-2.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:c7659f22557c5a0bc4855cd635f55edec690cc008a40768527762cb9fb263455", size = 179941, upload-time = "2026-08-03T21:19:43.747Z" }, + { url = "https://files.pythonhosted.org/packages/10/69/43965eccfdead3b9220015fd1320e117be8c6ed01a62ffab76eeb752f5d5/cffi-2.1.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:c8c69575568085ba0b1b10c0249d779a214aea6f6522e949a0fc9fb0fcb449d0", size = 184821, upload-time = "2026-08-03T21:19:44.887Z" }, + { url = "https://files.pythonhosted.org/packages/54/7d/16e5a096677b5e313ca80cd5e5170efa3ea44624a82bb111925522da64b1/cffi-2.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f81b3b8f3d4e343550fa4baa0e479bba9f2d29ce9c2e9b51d1ce1718d7442fcf", size = 184719, upload-time = "2026-08-03T21:19:46.129Z" }, + { url = "https://files.pythonhosted.org/packages/56/e6/8941622732edec876dd17d0453dce07317ae96db34f2ec1436c9d3785986/cffi-2.1.1-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:811bd1e21d32de12efca32393a0ab3f5133b54fce9bd44b8bd77ab07da14bf6a", size = 214799, upload-time = "2026-08-03T21:19:47.218Z" }, + { url = "https://files.pythonhosted.org/packages/44/de/f98430906df1545ffde0d543dd124a7a439bc2cd32b36b9c53f805df7333/cffi-2.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:68e62fe11f30d5ca8289242866f0a5291402d8529ca2178ab8afc5c9694ae890", size = 222389, upload-time = "2026-08-03T21:19:48.331Z" }, + { url = "https://files.pythonhosted.org/packages/6a/5b/717f1526b9957b34456313c31645c5b82b8fb5c3fe9e4752999be7128bfc/cffi-2.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4a7c934f7360e8cd64fe9efadcbd10c7c6364f531e432b9a4bf5ccbc9e0e8b50", size = 210249, upload-time = "2026-08-03T21:19:49.543Z" }, + { url = "https://files.pythonhosted.org/packages/64/b3/f8aa4f3e34986c7e4ec45072d1b1b9dd295b6b18007b45518d79726dd725/cffi-2.1.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:3143d81e29e1e20a9ce10901ec369012947876596f75a222235965f2b7ae832e", size = 208775, upload-time = "2026-08-03T21:19:50.918Z" }, + { url = "https://files.pythonhosted.org/packages/b1/db/dceb9dd5b231e1da801793f8acc9f3c52a7e1afe40bb1aae37e02b0faad5/cffi-2.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf", size = 221822, upload-time = "2026-08-03T21:19:52.054Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d2/6cd24ae3be000a634109c247d1475d62e5616d0dc78c82770942ec384248/cffi-2.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:208f941bb9d18e768138677f0a6d2ce01f590df56043dda1df1535ac57c88517", size = 225232, upload-time = "2026-08-03T21:19:53.109Z" }, + { url = "https://files.pythonhosted.org/packages/cb/52/3fa190537004dd7f0ab860a6dc7c0175b8667f68d1e618a46f5498d30250/cffi-2.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:210019b6c7cf07f081b4c54635c8cf744377001350e29cc0f81c4377b4797735", size = 223597, upload-time = "2026-08-03T21:19:54.515Z" }, + { url = "https://files.pythonhosted.org/packages/80/fb/0bb75b7039588c074b37ae99f40d9bfddf990ecb2fbc346ebccd2e56b9be/cffi-2.1.1-cp312-cp312-win32.whl", hash = "sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e", size = 175292, upload-time = "2026-08-03T21:19:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/d9/79/615cc094e2fb508cade7de88d3b4f6c4ec2bab695c97bce9153dc65aadf5/cffi-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:f53e442b08449d42821fa4a4fba000095af9f62742a500f978a9f557ec44339a", size = 185919, upload-time = "2026-08-03T21:19:56.89Z" }, + { url = "https://files.pythonhosted.org/packages/70/c6/d0ea84713fe46b243a436a18fcd47d639732747e21635c8a27191b06dc30/cffi-2.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:7bde5e4cc5c10140859842b9d383af292b22639a4dffb725314baf45968cef80", size = 180093, upload-time = "2026-08-03T21:19:58.155Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f4/035513d4117049066b4779dc3b7c0c0fdad175fa13731c9f4003f1cd1478/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e", size = 194248, upload-time = "2026-08-03T21:19:59.399Z" }, + { url = "https://files.pythonhosted.org/packages/76/af/2aeb4dbb5fc41a04161ae9ff1518de7cec08e164f44a8ce6a4cf7fd2cd1d/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c", size = 196908, upload-time = "2026-08-03T21:20:00.746Z" }, + { url = "https://files.pythonhosted.org/packages/a7/46/2e5fdde8555706dd98139a910ca11be02809f3f605ce956f655d0214e100/cffi-2.1.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6", size = 184805, upload-time = "2026-08-03T21:20:02.02Z" }, + { url = "https://files.pythonhosted.org/packages/55/41/4c7042f317b9217502988f0873af87e16ad606dc20f84e546e3e6ce9764c/cffi-2.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971", size = 184764, upload-time = "2026-08-03T21:20:03.141Z" }, + { url = "https://files.pythonhosted.org/packages/43/1f/1c3d90d91811c8f86ced9ed637956c54bfe5b79ca98fe976d7f8c8979f6b/cffi-2.1.1-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c", size = 214722, upload-time = "2026-08-03T21:20:04.377Z" }, + { url = "https://files.pythonhosted.org/packages/37/6f/3b5ce4c3b2192d250f04908f2bfd91ef34552ec8f7716a5d4abdb8d67bb2/cffi-2.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125", size = 222369, upload-time = "2026-08-03T21:20:05.544Z" }, + { url = "https://files.pythonhosted.org/packages/02/10/4b3c75dde3d9663c9e02ba05c2668b954f671d4bbe346413ca8c696b295a/cffi-2.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264", size = 210175, upload-time = "2026-08-03T21:20:06.75Z" }, + { url = "https://files.pythonhosted.org/packages/df/62/14f74b9543e605d17701dc797b815958b8bb70b7624ce1b832ddad48ed6c/cffi-2.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3", size = 208670, upload-time = "2026-08-03T21:20:08.04Z" }, + { url = "https://files.pythonhosted.org/packages/95/95/86342356ff5953b3fb06f7ef7c5bee212d45e770abc7218d451b9148313c/cffi-2.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2", size = 221824, upload-time = "2026-08-03T21:20:09.274Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ff/7b3429ff53aafe931ed8a5fc69f481bbef7ba6de87ddcbb63d08f483f613/cffi-2.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b", size = 225148, upload-time = "2026-08-03T21:20:10.7Z" }, + { url = "https://files.pythonhosted.org/packages/34/34/a95870b9221e09cf4f2ce3178b1a210abdfe63a1bd357da940418d7b8d15/cffi-2.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7", size = 223564, upload-time = "2026-08-03T21:20:12.165Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/839b50531021a647fb5e929f72cf97bc1ff702b5472166164b5b6e76b851/cffi-2.1.1-cp313-cp313-win32.whl", hash = "sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac", size = 175263, upload-time = "2026-08-03T21:20:13.559Z" }, + { url = "https://files.pythonhosted.org/packages/60/a6/8b149b2c3f2e11aaa1618ef64500b45f50f22c57a977a4dff1aff1f91042/cffi-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d", size = 185688, upload-time = "2026-08-03T21:20:14.69Z" }, + { url = "https://files.pythonhosted.org/packages/01/9a/11f687cb39d6a3504060d5242f04f48c735afb4d3d533958a20594890cb2/cffi-2.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973", size = 180078, upload-time = "2026-08-03T21:20:15.917Z" }, + { url = "https://files.pythonhosted.org/packages/d3/7b/d6bbf82b8b96e7391438898c42f5bd96dd02030fd5b64937d248220003e2/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c", size = 194064, upload-time = "2026-08-03T21:20:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/94/e6/bcc91b283be94735e268487a054004f0aa19947b6348fa367db53230abc8/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb", size = 196720, upload-time = "2026-08-03T21:20:18.268Z" }, + { url = "https://files.pythonhosted.org/packages/d9/99/c4b0c17cacdc9c3b8f280026286a9826d6a208c0f047591a3c3ce99b91fd/cffi-2.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d28630f5854ab07ab1fd4aba756de52326c82e6be15d414b12793f1975048b54", size = 184964, upload-time = "2026-08-03T21:20:19.708Z" }, + { url = "https://files.pythonhosted.org/packages/b3/a9/9db617d05d7367c1ad0ab00b3aa6e6f9281edd689b4ee9ea0e5a84e89c97/cffi-2.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:661c298b4821edebead0c91edd2b00374d67ad7c5a1f7a91d4442633b79d6a72", size = 184962, upload-time = "2026-08-03T21:20:20.833Z" }, + { url = "https://files.pythonhosted.org/packages/67/b8/b42132ca113dc567d37684437b46ca1dafc885902b02a110a02d5b511857/cffi-2.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1", size = 222328, upload-time = "2026-08-03T21:20:22.118Z" }, + { url = "https://files.pythonhosted.org/packages/80/10/c5c0cbf0a657aecf59ef511409734230bf556f05a0d6c9eed7aa5c0a0166/cffi-2.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:456a61fa52d579ebf9df2e9552ead5129855dbaff6c1e5a9b1bc408809bdc062", size = 209985, upload-time = "2026-08-03T21:20:23.401Z" }, + { url = "https://files.pythonhosted.org/packages/d5/6c/bfa0b87b03b9238148beca990292843c9396ba069b54496596594173de7b/cffi-2.1.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a4f00aa42f75d6e4595e8866e748cc1705adc0cddfeb2ca86d0d03993d63ba03", size = 208530, upload-time = "2026-08-03T21:20:24.628Z" }, + { url = "https://files.pythonhosted.org/packages/e9/02/4e7d553a7ac4b4238b38b3c1b80d486e9d4436f8d2acbf87a0997fe3f402/cffi-2.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96", size = 221525, upload-time = "2026-08-03T21:20:25.758Z" }, + { url = "https://files.pythonhosted.org/packages/82/1d/a4aaf9babd75acb4d5f223bff71533bee748dd770a382619a798960ee9ba/cffi-2.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527", size = 225053, upload-time = "2026-08-03T21:20:26.985Z" }, + { url = "https://files.pythonhosted.org/packages/81/10/5dc0e7bdd18e22107054288283380fc97a06ae3f1656a106908d666a3c88/cffi-2.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13", size = 223213, upload-time = "2026-08-03T21:20:28.277Z" }, + { url = "https://files.pythonhosted.org/packages/0b/e9/d0061c364cde06ee43168a0d076ac1da512cbc380d44767b844ba34fe2b6/cffi-2.1.1-cp314-cp314-win32.whl", hash = "sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c", size = 177682, upload-time = "2026-08-03T21:20:44.288Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1c3e01e3ba14c39f6d10bfbac52753b7e22259e38088e5cfe1d704918690/cffi-2.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48", size = 187949, upload-time = "2026-08-03T21:20:45.623Z" }, + { url = "https://files.pythonhosted.org/packages/87/5b/da4e39efe18eeb89cf580ea9cfc66b6a7c3eadb808fc0cc1d3a295cb5a5d/cffi-2.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836", size = 182947, upload-time = "2026-08-03T21:20:46.955Z" }, + { url = "https://files.pythonhosted.org/packages/23/59/40338bf421c5accea1d45158170c87006ef1cd371b05c077e76476949728/cffi-2.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7750c6449dff7864bb9bb27ddfb0267756189201a3afc911d82b3caacd70dfc3", size = 188504, upload-time = "2026-08-03T21:20:29.495Z" }, + { url = "https://files.pythonhosted.org/packages/7d/47/5ecf1023850036e674c77ec4de86182d309ae344e39e7cba984b7df5d647/cffi-2.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2", size = 188259, upload-time = "2026-08-03T21:20:31.291Z" }, + { url = "https://files.pythonhosted.org/packages/2a/9c/92934c3bea9f785b23eba304538c0b4d37a2a96d2431eb3a1bc87a11aa19/cffi-2.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94", size = 223864, upload-time = "2026-08-03T21:20:32.571Z" }, + { url = "https://files.pythonhosted.org/packages/4d/45/ba4c93527bc38616a8bd36488acb69a2212d60486794f0c1f318949bbb76/cffi-2.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e2d65b31f36619cda3999b78b2aa9632e76b78448e7a56fc4240824200e7c4fc", size = 211538, upload-time = "2026-08-03T21:20:33.808Z" }, + { url = "https://files.pythonhosted.org/packages/80/e9/b6ef565e452acb932fb0cb5443f44a78efbd1233e566f02b5a83855e9115/cffi-2.1.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:28907ab9bfb6aa13184cfc17c6b8e1023c5ab6fd7076d8c20a35e59fe04f8f29", size = 210688, upload-time = "2026-08-03T21:20:34.974Z" }, + { url = "https://files.pythonhosted.org/packages/9a/95/eff5f0cee78d2eabc7eebffec40d3fc1876b5f3c95582e018bb4b99601f2/cffi-2.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676", size = 223803, upload-time = "2026-08-03T21:20:36.564Z" }, + { url = "https://files.pythonhosted.org/packages/fa/01/579d39fb8bef00a335a23d83757b44feb24cd6345a2c451b64cb67b9c362/cffi-2.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e", size = 226763, upload-time = "2026-08-03T21:20:37.816Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b0/0b44f47c60b01b57b6e2bbd92343f13a85a1d93bc46ccf6e47e244acd99c/cffi-2.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f", size = 225688, upload-time = "2026-08-03T21:20:38.959Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d2/3b7176cb570a1d3e27faf67b72f591af508036e0d8b2be2ef9af9e8c84bb/cffi-2.1.1-cp314-cp314t-win32.whl", hash = "sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4", size = 182868, upload-time = "2026-08-03T21:20:40.388Z" }, + { url = "https://files.pythonhosted.org/packages/56/78/31f00c1bcd97c9bbf55f1bfdf5bc809a5de8887473e90bb9960dca825e80/cffi-2.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e", size = 194104, upload-time = "2026-08-03T21:20:41.725Z" }, + { url = "https://files.pythonhosted.org/packages/7b/1b/58496f2ed0a35de575250c02a43ab3cc2c04d494a88fed31c1cabc0fd176/cffi-2.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5", size = 186402, upload-time = "2026-08-03T21:20:43.042Z" }, + { url = "https://files.pythonhosted.org/packages/c1/8f/9ebe220eab48a093d1a5a5e339ab0dc7316eef3bb04d63c42f0251b61f50/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d", size = 194043, upload-time = "2026-08-03T21:20:48.179Z" }, + { url = "https://files.pythonhosted.org/packages/ff/69/844bad3ece306c4782c2ecb93597035b6690d48704b803914c199da1e8b3/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b", size = 196737, upload-time = "2026-08-03T21:20:49.457Z" }, + { url = "https://files.pythonhosted.org/packages/1b/8a/af668013284634733f02d683458a0728739c7d6ddb5e14cb0c20832266fe/cffi-2.1.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4", size = 184933, upload-time = "2026-08-03T21:20:50.639Z" }, + { url = "https://files.pythonhosted.org/packages/0c/75/2f5207ff6d1a613133b23a5203cc0c2a628313b5eb3974d7956ae3c57950/cffi-2.1.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2ae64be792b8966f2c69538199728b290e34726562896df1e5dc8ffd8d8188e8", size = 185002, upload-time = "2026-08-03T21:20:52.173Z" }, + { url = "https://files.pythonhosted.org/packages/e2/31/9e1313b0a6e30e91b3b3d3fff51ae99c857c07738e3afcce1f7334e1b7ab/cffi-2.1.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6", size = 222271, upload-time = "2026-08-03T21:20:53.462Z" }, + { url = "https://files.pythonhosted.org/packages/50/e3/f6234a833e6e08c7007003074723c406559eecf9b48dfc97471e5a8eb7a0/cffi-2.1.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:246fa40ce8645a614ff682e0b70f37134e460eaf93a775e0cbe3cca585a67a80", size = 209919, upload-time = "2026-08-03T21:20:54.783Z" }, + { url = "https://files.pythonhosted.org/packages/0d/fc/5f74e293fced6edb51af3a46c4ccf6c23c9943774ecb375ddbd522c76add/cffi-2.1.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:471cee653ae88de62096552e6d24ccb4a5adb8c8c9f10b5054d0122c15bf2779", size = 208529, upload-time = "2026-08-03T21:20:56.066Z" }, + { url = "https://files.pythonhosted.org/packages/44/16/29e6d01b388bef055ecd6ca8244b3f4d336bd09e92d5d892187b9601084e/cffi-2.1.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399", size = 221630, upload-time = "2026-08-03T21:20:57.336Z" }, + { url = "https://files.pythonhosted.org/packages/a4/18/fa7f1f6857d5eb88a4ca99ffcbfb7c387a287ccc154c64a73e86314745d7/cffi-2.1.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688", size = 225134, upload-time = "2026-08-03T21:20:58.675Z" }, + { url = "https://files.pythonhosted.org/packages/e0/9f/e8e3dfa04a1b4c241f8c91faacad872b4d4efd051d49764ad4e2fd4b9fea/cffi-2.1.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7", size = 223197, upload-time = "2026-08-03T21:20:59.968Z" }, + { url = "https://files.pythonhosted.org/packages/f8/7e/8debeb04f1ab9fe2a6963964cd6f1aaf7192627b83926586a6a4e089c9fa/cffi-2.1.1-cp315-cp315-win32.whl", hash = "sha256:4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac", size = 177683, upload-time = "2026-08-03T21:21:14.901Z" }, + { url = "https://files.pythonhosted.org/packages/e0/31/5158704cc474ab65c1647932e88be78dc0873f47130e253be38bcaf13d01/cffi-2.1.1-cp315-cp315-win_amd64.whl", hash = "sha256:e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960", size = 187897, upload-time = "2026-08-03T21:21:16.108Z" }, + { url = "https://files.pythonhosted.org/packages/cc/4b/b3a2da8570c704ffc0f9762cdc3ec0f02c8573798e0b5cf7f11c82bbb70f/cffi-2.1.1-cp315-cp315-win_arm64.whl", hash = "sha256:27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1", size = 182935, upload-time = "2026-08-03T21:21:17.271Z" }, + { url = "https://files.pythonhosted.org/packages/d0/ef/5443574510a1207e6f6bc38ba6e1f1de36cb48fef07b2728bb896a21f430/cffi-2.1.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c26608d2222fb1e94487e4a387d85f13eb55d5ed725cb25a0c589ac4ee60e7bc", size = 188464, upload-time = "2026-08-03T21:21:01.163Z" }, + { url = "https://files.pythonhosted.org/packages/7e/ae/a56fa8c4686ad50e148fcbc8d3ae0d03915ff5c30d795058988c24118cef/cffi-2.1.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:4be96343e422f2dfcd12ab5c9f5aebe03f82f737c6bffeca6830b3875cb44aab", size = 188262, upload-time = "2026-08-03T21:21:02.382Z" }, + { url = "https://files.pythonhosted.org/packages/53/b2/6187f46f2912276a3ae284076109cc5c8680482f11f766ccf26db4a86427/cffi-2.1.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e", size = 223779, upload-time = "2026-08-03T21:21:03.553Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f6/c3ad28bd19f77047a03084424fbd4cbe997303267c14423737324be0385d/cffi-2.1.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:df423d40ee8654634421812bc3b196da3f9bd7d32929da813f8394c4348a5358", size = 211520, upload-time = "2026-08-03T21:21:04.863Z" }, + { url = "https://files.pythonhosted.org/packages/a0/cd/ccac9013a5bd9fd764de118674ab9c805b5ca10c19270d90ee273f8b2240/cffi-2.1.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a730a083190634c65cca36ba5f489531576ebd79bcd5c8e172130f6453127231", size = 210673, upload-time = "2026-08-03T21:21:06.223Z" }, + { url = "https://files.pythonhosted.org/packages/52/86/2976131c639aead931c5bee5aba67e4b09fbeb8018b6f282f70803f923a7/cffi-2.1.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6", size = 223835, upload-time = "2026-08-03T21:21:07.539Z" }, + { url = "https://files.pythonhosted.org/packages/ac/0c/33a7aeab2f9c76918c52e084beb39c570db3588133412929e8ec06fab90b/cffi-2.1.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94", size = 226705, upload-time = "2026-08-03T21:21:08.774Z" }, + { url = "https://files.pythonhosted.org/packages/e3/26/2cde30fdde421130bfc18f70395731a6e6b2053c6a1978a5258ff04e72fa/cffi-2.1.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5", size = 225539, upload-time = "2026-08-03T21:21:09.911Z" }, + { url = "https://files.pythonhosted.org/packages/6d/cd/a361394c94b2129d604bb846f624a8e88255a3ee33129c434a00d715e64f/cffi-2.1.1-cp315-cp315t-win32.whl", hash = "sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66", size = 182707, upload-time = "2026-08-03T21:21:11.226Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b5/ba2b299993c26577d529b6ae29841f9e15b9fcf004d65f423f4fcf94ade9/cffi-2.1.1-cp315-cp315t-win_amd64.whl", hash = "sha256:d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3", size = 193772, upload-time = "2026-08-03T21:21:12.39Z" }, + { url = "https://files.pythonhosted.org/packages/aa/29/35e016098c814cd93de9cd320c66b5bfba14dc6ecedd3cb518fa7c408c69/cffi-2.1.1-cp315-cp315t-win_arm64.whl", hash = "sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692", size = 186360, upload-time = "2026-08-03T21:21:13.636Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/3f/143b048436775b0f76ac3eec145c019e8173ccc2885c8f20319b996d5e83/charset_normalizer-3.5.1.tar.gz", hash = "sha256:6117b84ea48435e5356dc737f5121485c30920ba43375fa7b434fd753df0eac3", size = 171764, upload-time = "2026-08-15T08:20:44.807Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/aa/554e2614f38fc34c58ff1d0911ae8535ad2516440d5482d76fe59f1088b0/charset_normalizer-3.5.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1ee1e296209fdce05b81b663250eefa02213a2da7b41bf26f7829b8ba3545aa", size = 369072, upload-time = "2026-08-15T08:16:22.964Z" }, + { url = "https://files.pythonhosted.org/packages/03/6d/439231dfc3ccfa6f8c06477b7da2219cbd41a2de3d49084df8ec7b5100f2/charset_normalizer-3.5.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9fbdce1e47394b09bc9f26ab117dfc8d6491977a11d86f592bb42c779db2fda", size = 251142, upload-time = "2026-08-15T08:16:24.81Z" }, + { url = "https://files.pythonhosted.org/packages/55/53/7d819bd23a00ef45039146fa2cce1daa2f0771e758c5653ee1f6edac91ed/charset_normalizer-3.5.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:00668ebb0609751758682eb0b5857e7c35b9f00e84dfdef062e103244ec94d45", size = 240714, upload-time = "2026-08-15T08:16:26.392Z" }, + { url = "https://files.pythonhosted.org/packages/b2/2c/45847198c16f4b38090cc7423b2b6a9008e438704d8ab413211832498d31/charset_normalizer-3.5.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba2f37ee79e6338845261a3c5b1784e5d1acdff2c0785b284f1b633033d136ab", size = 279637, upload-time = "2026-08-15T08:16:27.961Z" }, + { url = "https://files.pythonhosted.org/packages/69/2b/d8be3523ddf9f0b0f3e56d1359034aa10653a4d11564c697f802b4775766/charset_normalizer-3.5.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ce854f5f478050ade5a238731c4ca985a7d3b3cb53ff600a9b5c3b689b5f0a7a", size = 276543, upload-time = "2026-08-15T08:16:29.399Z" }, + { url = "https://files.pythonhosted.org/packages/32/cd/4f564b8f132de25db594efc706897069f016790cea63a5669c9df2675f64/charset_normalizer-3.5.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:96eefc178f8636b9c760c5829345307fd81cfae9ab1e80997dbddeb0f54ee9a3", size = 261644, upload-time = "2026-08-15T08:16:30.722Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e3/38b975422534a608f98c360e79c2f07c763d66dd4272300d45fb1fee54b0/charset_normalizer-3.5.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:366ec70f5547c640d3ce1985722490f23faf4eb5216a7eeba78277490e78dacb", size = 259609, upload-time = "2026-08-15T08:16:32.248Z" }, + { url = "https://files.pythonhosted.org/packages/87/bd/fbc24d825c66f1c74f6ccdea3742c3d8354a4888e86d1315a197fee69061/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:950f23cb393f85543777b0433f082cddd25b51ab398eac7971146495679efe5f", size = 252457, upload-time = "2026-08-15T08:16:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/b9/2d/918d0e98a0e679469ed05bb2d90c2088b4d315bb612969d8499f76fb5210/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c1dcc36dcb96abc02236e182d17e0f71430152a6c2c7447421da2d2dc144edea", size = 242240, upload-time = "2026-08-15T08:16:35.396Z" }, + { url = "https://files.pythonhosted.org/packages/20/c8/c36f6e0b2dfec351bd38cbc05362697e58bcd073d7dbd95154290c9714ce/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:07ffd07412fc5d5e84cd8952acf9ff7e4ed7a708e69d1bada19d8ba91711353f", size = 280308, upload-time = "2026-08-15T08:16:36.825Z" }, + { url = "https://files.pythonhosted.org/packages/ca/7b/311b3e02e8c4092400c449c850a760d8c45d900983c83a70cc07208c551d/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:f5542f9b941279d82d41eb0aa9f98eba36fe4df5c7086c651df7944935b37182", size = 258679, upload-time = "2026-08-15T08:16:38.22Z" }, + { url = "https://files.pythonhosted.org/packages/b9/90/082cc45599c392f28c036a497f49e0634041a785fc3849c80ccf396d096f/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a545775cfe815855ea32d7c27731d79da358ef2055b4a25830231b1622dd18aa", size = 277221, upload-time = "2026-08-15T08:16:39.62Z" }, + { url = "https://files.pythonhosted.org/packages/58/ad/b9aecf38d805cbcf84fa94f14c5d972a16561e20296a11dc799a5dcf3763/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:494b70049a4d69aec6e8137c13af4cf8db8c9f9820a1392ac293b0dd2987a818", size = 263799, upload-time = "2026-08-15T08:16:40.885Z" }, + { url = "https://files.pythonhosted.org/packages/b7/23/b38a20598d5a825f85d9d7636860e56ff0db1479f86497a6e485aa9326f7/charset_normalizer-3.5.1-cp310-cp310-win32.whl", hash = "sha256:94fbf1c0c6cc0d3d5e50f9a9313a8cdca90dd696d34b381cd1704f8c9e939f20", size = 182037, upload-time = "2026-08-15T08:16:42.198Z" }, + { url = "https://files.pythonhosted.org/packages/d2/21/83fffb77864408b8bf0fe1ca603926401d6f8775a8e150b39aacc9958f8a/charset_normalizer-3.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:be47f99644b208bff7766314013f9acf57b056b04191d570d68ad14022cf5b1d", size = 206030, upload-time = "2026-08-15T08:16:43.787Z" }, + { url = "https://files.pythonhosted.org/packages/86/2e/b93135b5034b1157fb29554b0d06d4844ce62282f0e0a14036f93d7ee2e7/charset_normalizer-3.5.1-cp310-cp310-win_arm64.whl", hash = "sha256:a6d095662e73e74f0a49988e0593373e243e3a52e27bfeea0a859e88acf4a0f5", size = 185092, upload-time = "2026-08-15T08:16:45.177Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b6/034f6802e9c3f6418966cfabb7db8c9252cc2429c5098f41cc43af804149/charset_normalizer-3.5.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eda059b6bc8bc0812d626fd91a7ce01bf583df0a61296eff390fd94141a34e30", size = 363585, upload-time = "2026-08-15T08:16:46.646Z" }, + { url = "https://files.pythonhosted.org/packages/d5/fa/6a7e2a7c4b5451912b8c417732df79574354443592a88d616de03da66ae5/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa2bb0b37202dca27175591f761108b5d34096ade1191ffe4808bdf6b1571488", size = 251189, upload-time = "2026-08-15T08:16:48.287Z" }, + { url = "https://files.pythonhosted.org/packages/a4/c8/ab42b07cfd82e919f427fcfaa7c41abae8242833ad1aad66d42bae40b669/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b2b1b3fa5670c127b246df1d0c059defd41f689a868a3b9d79df9b1cac42d22", size = 239724, upload-time = "2026-08-15T08:16:49.67Z" }, + { url = "https://files.pythonhosted.org/packages/e7/80/b9348b5d3041209f98b4cdad7655766369233f1d533f4f4f7558e9717bec/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6e5e4d73d588ca5ed09df1b7dcd1b203d1df3c542e3f50d126c947d432b10731", size = 280078, upload-time = "2026-08-15T08:16:51.228Z" }, + { url = "https://files.pythonhosted.org/packages/82/38/083a24028304bc85bb9e376fed801178423dcbb67495f73b6ea0624e1894/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b54e7e13267d49ffbfe68e25b3cbd774dab38fa37238f71265e91b36146eb21c", size = 276650, upload-time = "2026-08-15T08:16:52.625Z" }, + { url = "https://files.pythonhosted.org/packages/0d/35/731ac04aa0a097fc1c97f0994c375bdb230c6c96619db794208fe664e9ce/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7b742bf31c88566b4bb6335a7f393bb322e580b6bb98df7bd0c25e6e3519ce8", size = 262325, upload-time = "2026-08-15T08:16:54.085Z" }, + { url = "https://files.pythonhosted.org/packages/f5/28/c2028e7021fb89c6e56868ed0e387b8e9aa811abdd2ab3208d6578d2c930/charset_normalizer-3.5.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6ba32c4d2abf1d2fe7cf27d280f4cca5664233b0f885549c7761719eb977f486", size = 261140, upload-time = "2026-08-15T08:16:55.604Z" }, + { url = "https://files.pythonhosted.org/packages/28/f0/0c0ceec6d98b7daa62e361e418135d59685811d79ba11529aad5cdf15e84/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0722590aabf9dc6a6c0343d523c05458fa2b5047dbe6302fd526bb570600753f", size = 252791, upload-time = "2026-08-15T08:16:57.103Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3e/48f4cd187b1c33189d86039e9cbe4f92c05454175504b44ff81806d4d1bf/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:aa1099b956fb795e686d073568f6dc002a0bb89765ea6d5b055dd7d9bf1b116c", size = 240730, upload-time = "2026-08-15T08:16:58.418Z" }, + { url = "https://files.pythonhosted.org/packages/42/85/f9e22af69af67c54cce42be9455d9c81294f918b4ccc454db01f66efcac2/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:bd6c173f04743d483881bffa1478d5a4624475b8cd1d2194956a75548e191c18", size = 280791, upload-time = "2026-08-15T08:16:59.918Z" }, + { url = "https://files.pythonhosted.org/packages/fd/4c/9044135f42127630b6fa742feb51256353f6ab87a78f2fdd1de3de955a7f/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:f298e218441525d3794428b4c8b8fb8662c6d3ea79925d4807ee6b9a96a3bca5", size = 259598, upload-time = "2026-08-15T08:17:01.421Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ed/1dd7cfebb4e75812934c49ca3b79757d11948053f7937ab7070c151f3c55/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:6e2912d4babbc65196ac13c2f53468dc57fb8b9c25ef913e8c59ddf7c6dc0e1b", size = 278217, upload-time = "2026-08-15T08:17:02.782Z" }, + { url = "https://files.pythonhosted.org/packages/bf/eb/239c84503cc9e3ba6eb34686a24bc66e84f3924efdd7e38e751a19f6bc10/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3d27167433c0d5f18dc850f07d0b3816221984fecdc405d6c157a6f0b8f8e9e6", size = 263417, upload-time = "2026-08-15T08:17:04.216Z" }, + { url = "https://files.pythonhosted.org/packages/37/ab/4e4510e1e288478e2c8333131d1c1382382ba8cd2165053c79e39d1da961/charset_normalizer-3.5.1-cp311-cp311-win32.whl", hash = "sha256:ac00177c4831ffa650f8609e4bdddd5fe09c03b1c0c47acece7e6ea20421598b", size = 181774, upload-time = "2026-08-15T08:17:05.58Z" }, + { url = "https://files.pythonhosted.org/packages/e3/57/32f0ccea59e8612057c61d6fd22ef2cb63cca93c9fe594094919696ac170/charset_normalizer-3.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:f9b1e28d0e8dbfa858abdba91d6b547beaf2df1a59bec6da6faae7b96a4991a9", size = 206653, upload-time = "2026-08-15T08:17:07.075Z" }, + { url = "https://files.pythonhosted.org/packages/17/d4/b65c433fc521e58b5f54293982a5e51c05cb5f2dd3f1c7a6acb65b75324e/charset_normalizer-3.5.1-cp311-cp311-win_arm64.whl", hash = "sha256:ae31a1a1db2ee6cc2942fccaf695c934bc7f3db9f2133a3fef1f367cf1a4ab10", size = 185630, upload-time = "2026-08-15T08:17:08.502Z" }, + { url = "https://files.pythonhosted.org/packages/30/27/78873dc8b6a56357517b74b6bb9568b80450e7bb4f6ef7e3fa9d22aa0bd7/charset_normalizer-3.5.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:5b6d1386bf0096d26d3a863dc0a487a5b4eb9aa93cf5ba69683d29dde6b9d60f", size = 344456, upload-time = "2026-08-15T08:17:10.072Z" }, + { url = "https://files.pythonhosted.org/packages/9a/4c/be49ada26b1f0232d57aa89bbebf997a5cc2332a5616b6eca26ff680044d/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4582c27e8c889d64811987b5967fbd3ae0c823fe1fd933b543d55ac20bb475fa", size = 238530, upload-time = "2026-08-15T08:17:11.563Z" }, + { url = "https://files.pythonhosted.org/packages/76/84/6f1290fa07ae6978d3960caa3eb1b8019bf9284ab7c2297b00c099ef4250/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1d1c7a53a6c2103925cdd6d7229f8c567379f211c869793df679f2e9f738c369", size = 230200, upload-time = "2026-08-15T08:17:12.919Z" }, + { url = "https://files.pythonhosted.org/packages/e7/a0/47b18adeed31c8f16ba9700f32c1b18594cfa09f47eb672a488c273c22bf/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e6621fb2a4988d6e53eedc455e5903e2679f3967b8acb3d639f1b63c14a2e893", size = 262222, upload-time = "2026-08-15T08:17:14.571Z" }, + { url = "https://files.pythonhosted.org/packages/38/fe/341861ac118dae06f3ec0eb487488af52128f2ef2faf0b11003944d22259/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7c0c10730342b0c9b35dd1d619beb8214e520bd96a1f870f452680b238aab3e0", size = 258951, upload-time = "2026-08-15T08:17:16.158Z" }, + { url = "https://files.pythonhosted.org/packages/6f/89/bb5108dc6c3651dca963f2b0a3ba19bbcb370c94e1b6d3e0e844a58e6dca/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b9af956078716df40d985fb0dfeb2c2120c5ca92ba4ff4b388acfd01cdc14d08", size = 248801, upload-time = "2026-08-15T08:17:17.683Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ba/ef83ae3aca816393decfa3530976f38a79812d707b80b580ac33b83f9877/charset_normalizer-3.5.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9f8405c2c758532c74fed975dbee57be1f31a6e865c031870c79a6ed3212ada", size = 244070, upload-time = "2026-08-15T08:17:19.191Z" }, + { url = "https://files.pythonhosted.org/packages/f6/0b/c5292a2462d69b7378ea89793bbb5b2b6fcf6f7dd6d1667f9619094ad553/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:96fef3e886d6a9874b14f27fc193fbdc69d5d8035783d86aa4e1cea594e695f9", size = 240110, upload-time = "2026-08-15T08:17:20.547Z" }, + { url = "https://files.pythonhosted.org/packages/46/22/111e5be3b740d5c2a5bfcedb3d237b6591e5c2e82ae9d6ffcb121fe0909c/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5d8531a6569d025f68e2321e7638fb7978f23db58e5f69f56913837aae03816e", size = 232836, upload-time = "2026-08-15T08:17:21.895Z" }, + { url = "https://files.pythonhosted.org/packages/f9/d2/d2aad6fe0dbb44b194bf3becb60f5a0ac48446ade999a47fe7bb41eb09a7/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:aae2ee51122d3ae968a3837d97dc24a0aeebb0dea23694422cd172bd30017cd6", size = 262712, upload-time = "2026-08-15T08:17:23.727Z" }, + { url = "https://files.pythonhosted.org/packages/35/5a/337e4663a5eae6de99db940ee8066d4145caafb61327db62deda15313cce/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7235dc28fc6dd9d832ac7c7bce95367dedb85929f17368a0c2bee1e080b9acbf", size = 242977, upload-time = "2026-08-15T08:17:25.157Z" }, + { url = "https://files.pythonhosted.org/packages/ca/85/f82f8a92e31c7519410e2e1afdc630f28ec47490ce2c09a11c1a43cbb459/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4abdc5f9ad448c1ecbfae2974b820535d6bc6e7eef63babbab3d81cf46968c71", size = 260207, upload-time = "2026-08-15T08:17:26.602Z" }, + { url = "https://files.pythonhosted.org/packages/b7/52/643d11ffd60e9ac2fd1fb87e167a19285b9eefeff4a40e63c87cbfbeab36/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ba501e667c17d8411f98e67a022d9604ef179aff0e459b7e292c796837c13573", size = 250562, upload-time = "2026-08-15T08:17:27.971Z" }, + { url = "https://files.pythonhosted.org/packages/62/16/46556278c2168d12df9da7fede5dc6fc70e60301b26a82bbeec238c9cfe3/charset_normalizer-3.5.1-cp312-cp312-win32.whl", hash = "sha256:cfa1c0cc3a8f9f53f1243a5a99ac36fd003880199383b37672e86ddda9cb07e2", size = 178507, upload-time = "2026-08-15T08:17:29.277Z" }, + { url = "https://files.pythonhosted.org/packages/9d/7a/4c6c298171e6b3e745633180ff59350fc0ca0db1ffd28df1e369e0579f71/charset_normalizer-3.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:3617ac3cfd8b9888f145ad89dd6e692285834b0201c6074a5eeaad3fd4d668c2", size = 200551, upload-time = "2026-08-15T08:17:30.668Z" }, + { url = "https://files.pythonhosted.org/packages/cd/d7/eb95a042f0dd22e304b0b6472b154f3546a1a039a9ee89ccb2a7f61591fc/charset_normalizer-3.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:88e85ab89cb822c1e635f51d6d32e488f94e002e70e2f492bdb8b945543f345a", size = 180700, upload-time = "2026-08-15T08:17:32.028Z" }, + { url = "https://files.pythonhosted.org/packages/bc/61/2cb6ad133dbbb449fa2d37ccae973232f4827e799af258d15e589a3d1e9e/charset_normalizer-3.5.1-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:4f298bdadb8f0b9e5672877f647d1be9373ef5320c9e2f049795e26cad28b6a9", size = 211584, upload-time = "2026-08-15T08:17:33.597Z" }, + { url = "https://files.pythonhosted.org/packages/18/57/a305c968be1ca13f3dd1b32f445877e97addf55d80b65c7cb35fac82b777/charset_normalizer-3.5.1-cp313-cp313-android_24_x86_64.whl", hash = "sha256:88ca277405c2d3b71c4e1c2ee0e7966e807bcba86a69d11e19ba199d18ae4491", size = 223359, upload-time = "2026-08-15T08:17:35.022Z" }, + { url = "https://files.pythonhosted.org/packages/09/0a/d3646670292ce8d8f8cc11ac067d44885e697a5591f57a9221128da5e7b3/charset_normalizer-3.5.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9362dd90aa7dab48c0054a21187791ccf05473f7dba5d92b8033ae62164675e7", size = 194464, upload-time = "2026-08-15T08:17:36.452Z" }, + { url = "https://files.pythonhosted.org/packages/de/93/d51ec556e01042fed6f993ea859311bc7917b466684182fbbceb6ca24762/charset_normalizer-3.5.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:977cdbd483a9cff38179bea4fd754289a6f2195c7abd414aba85410b3e66cc5e", size = 197676, upload-time = "2026-08-15T08:17:37.819Z" }, + { url = "https://files.pythonhosted.org/packages/a4/a0/562247944386f7d4ef94467e84876600cc1e0f1b93239aaa9213d2bc3cbd/charset_normalizer-3.5.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e90251c0c7bdd54a100a0dce3c07b7e637278c93af29dbf78ebb89a58c4bac7d", size = 340473, upload-time = "2026-08-15T08:17:39.303Z" }, + { url = "https://files.pythonhosted.org/packages/31/e7/1d994be1b93d41e9502b8b0460eaa88a1dd8df335df415db87d6c3e91ab2/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94d78ecec2605a8d0398b0f365d5f12a63248438516f5dac536a5eff7337df4a", size = 240156, upload-time = "2026-08-15T08:17:40.66Z" }, + { url = "https://files.pythonhosted.org/packages/09/53/27923ce5cc6cbccb832037b27dca98882d9c53e9b69e866bbbef4aae7fc8/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d59b75732e9b6f27388e10c14b0259cc5f2e48c78627d185e6a177b58ad3cffe", size = 228246, upload-time = "2026-08-15T08:17:42.003Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/5a97e84d63af1d55c07439cb80e56d99a8efb4295700eb4e18c0d1615d2c/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0d929fc574b4d6fd9e7c0f5c2ede8716a41911923aa7fa5fce38e0818aa4a1ac", size = 263660, upload-time = "2026-08-15T08:17:43.627Z" }, + { url = "https://files.pythonhosted.org/packages/7a/c2/071575791dcc88316c0a9a65ce38897a82e4cfe4a325f0f7fe1b1ac47bcf/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:394fea06235c8543390050ed5f529187074b029fb027213f6c46ac11ab5d950e", size = 260354, upload-time = "2026-08-15T08:17:45.094Z" }, + { url = "https://files.pythonhosted.org/packages/fb/af/63240b0c0248c075c2535a1f1bd992821d8251b9f173abc13329661d09e4/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62b55f6722735a6c472f88361cde6640608773d9443cebdbb51abf436a1fcdd3", size = 250638, upload-time = "2026-08-15T08:17:46.496Z" }, + { url = "https://files.pythonhosted.org/packages/4d/66/70dfad64f15be09c15ccfee81330a7e515895dbe296dd23114e9a231268a/charset_normalizer-3.5.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa48b1b63d639f9483e0633e092f5851e2348c352f1f9bb6c8182f87884ef876", size = 244583, upload-time = "2026-08-15T08:17:47.963Z" }, + { url = "https://files.pythonhosted.org/packages/c0/24/ef36367d38b9ddd4bccbf72888c342e8de1f5ae506fa0b2dcf970e2732a1/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c71fb0d56c920c269cd3e2e3fe7c610e3f1fdb21a6ce60efa6430ff63676cea6", size = 242038, upload-time = "2026-08-15T08:17:49.481Z" }, + { url = "https://files.pythonhosted.org/packages/db/ab/55e683ba0fff2e43adafc10daa3001eac90fdaa419a97227d5a7067eedde/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:485a0d363cafefcd2538a73c7c838daa2035f09b2c9f9b5e3133f80c6aeb84c2", size = 233677, upload-time = "2026-08-15T08:17:50.845Z" }, + { url = "https://files.pythonhosted.org/packages/bd/67/0f40eaf8d1b6e7cf15e82382a2965efaca787fc1c2794b7021d37aaf5036/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c0ea61a470e070686aa30892fed79e297d2c8d0ab46b8bcdf027d38c51da591", size = 264491, upload-time = "2026-08-15T08:17:52.61Z" }, + { url = "https://files.pythonhosted.org/packages/5c/64/12b4c2a11ee8df4fcc518c78b0d93e3a92bd3d5253d1617ce74ff0e8c7ef/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:90b7481fb62fbe172c558bc6fd1c4c98d82004a54a7551f20e11ac9bf0b8708c", size = 245196, upload-time = "2026-08-15T08:17:54.023Z" }, + { url = "https://files.pythonhosted.org/packages/37/2e/651d910af6d0fba325eee1cda37ec5443462ed25360e666c144166eb6091/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:35fe081843b35aad20ffeccec3eeffbe637b15d14f3fb22cc1b59cd8ec17e93c", size = 261660, upload-time = "2026-08-15T08:17:55.491Z" }, + { url = "https://files.pythonhosted.org/packages/90/c6/b09e05e6db7f64338e0dc067c79577b1138da86c1e38369096851d96be88/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fd0350afdc3aabd5576f60ea109228bd5538139713c7b094c5cd27c73a98bc6f", size = 252618, upload-time = "2026-08-15T08:17:57.025Z" }, + { url = "https://files.pythonhosted.org/packages/76/4e/362d4f9fdcdf5556fb2aa3ce7d4a58ebce03ed1ff03aa1d9aca8d02f13f3/charset_normalizer-3.5.1-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:9d9a0dc7cbe9bec24c3f767c9122c41fe5a1bc43f47cd099d00d393e09769de4", size = 140362, upload-time = "2026-08-15T08:17:58.425Z" }, + { url = "https://files.pythonhosted.org/packages/b4/d4/703be739b26acce318bd29eb3b25b7209e1b1f527f9eae3d1f1f01fdde2b/charset_normalizer-3.5.1-cp313-cp313-win32.whl", hash = "sha256:d63600d620ad0064c3a748b950ac5ea38a80190e5498532efefa4b7b3f1da1f3", size = 177755, upload-time = "2026-08-15T08:18:00.037Z" }, + { url = "https://files.pythonhosted.org/packages/8a/33/56d97ade41c8db611e727168c52ae46c9224c362ec28d4b65d7e9869e8da/charset_normalizer-3.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:aea996a6aba25260827c9ea511d1addfde2da9eb686ac961838509086188b7e6", size = 199295, upload-time = "2026-08-15T08:18:01.506Z" }, + { url = "https://files.pythonhosted.org/packages/5b/75/5b20dd1e6573a01a08158fe104104fa2c8abf941745596954185726cd46c/charset_normalizer-3.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:fd0a274c0e5f9a21565cd9d3dd749b61f96b7aa1e20a93aa1ba4029518f2e5c0", size = 179856, upload-time = "2026-08-15T08:18:02.929Z" }, + { url = "https://files.pythonhosted.org/packages/29/cd/2b812ce5e888f1ce69a5350281e58aab07ae64a958ecae8912f30865718e/charset_normalizer-3.5.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:774d157f112367ff4abd29019f38f023c24e00e56edc7829c20e358a5a913ad8", size = 212318, upload-time = "2026-08-15T08:18:04.403Z" }, + { url = "https://files.pythonhosted.org/packages/9e/4a/a6ee107430768a5334e6d63f31f148a04a1a491ef161a1ac9415a73f2fa8/charset_normalizer-3.5.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:26422d45fd13551cf564c58932f7d72b4f58b93b0fcf18c35ba6be12b46bb102", size = 224897, upload-time = "2026-08-15T08:18:05.997Z" }, + { url = "https://files.pythonhosted.org/packages/c3/d9/35ae3f64f29d0179c35c3baefe575904df2913dde519129c7f75995a2b1d/charset_normalizer-3.5.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:09a7bba9f739468c8e78c36a75c33768e53cb1959fc638f510454c14683f00d5", size = 194848, upload-time = "2026-08-15T08:18:07.397Z" }, + { url = "https://files.pythonhosted.org/packages/74/76/f2fc7380f056cc273a53af37f50d08ad54b2c59f61078f31432edcf1c2bd/charset_normalizer-3.5.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4c9548dc78002099910abaebc0a72ac58b7d30931869e0351c09b507dff4ece3", size = 198163, upload-time = "2026-08-15T08:18:08.989Z" }, + { url = "https://files.pythonhosted.org/packages/e9/40/095ce62fa078483cccc1fa2b36e6bc9580b85422a20ee9f925341c50e44f/charset_normalizer-3.5.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c428c6c31eb5f4277d7f8eccaf767fbd548ddd5ce3c8b4f4cbbfab3d96b5904c", size = 341823, upload-time = "2026-08-15T08:18:10.458Z" }, + { url = "https://files.pythonhosted.org/packages/f1/5a/0e58b1c04a1596e0256f407274a92d5fb2ee21324409d1fab1da48a65b5b/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f06b7eae9dbe77fe1d644ca244dad508de8d302870a43f3c559b521270938a0", size = 242458, upload-time = "2026-08-15T08:18:11.989Z" }, + { url = "https://files.pythonhosted.org/packages/22/95/b4618ce912e6db0b1aae89ba788e38e8a7eba0f3025cc66e8c0699f977b2/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b7430cf5728e68f6c462254009a6ef4086e1bea43cf2f57aa9c55fb4f50ff96", size = 226717, upload-time = "2026-08-15T08:18:13.401Z" }, + { url = "https://files.pythonhosted.org/packages/8a/76/c681192bbda3d55356db5dadd64381d5202b37c6b598fcda5282e88b5d3d/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab743e9bc90c1f73552ec33e10e3331315acd2c397b36065b591b0181de533cc", size = 266111, upload-time = "2026-08-15T08:18:14.961Z" }, + { url = "https://files.pythonhosted.org/packages/88/be/55127bfca72c0cff6c022488d140d7c5b04c771e3b72e9bdb4836d54979d/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6f7deae3feb4edfa2efaf7c574fe88cbf055038a6abdb40188e4fff66d5699f", size = 263128, upload-time = "2026-08-15T08:18:16.515Z" }, + { url = "https://files.pythonhosted.org/packages/e0/91/39c3af510b0aa32bbda03374259200f28430febfd1bf5e511fe765282ce5/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15f024313246a4ed976c60f440bb8d257815513a681d212ff74fd46f7d715a90", size = 251240, upload-time = "2026-08-15T08:18:18.127Z" }, + { url = "https://files.pythonhosted.org/packages/1c/a5/cbe418bbc6ecdfc3e05a0116002897c4b403a5e838d697e64c78e9f0190d/charset_normalizer-3.5.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:823f82903d189af463d7df250ef1f7f696f3cee08cc8d91deb565e8d425f6506", size = 245282, upload-time = "2026-08-15T08:18:19.625Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a4/689bb42e8e7cd492f3cb64907c6bc00ad247ec9a3628cd3f8eed126e8ae1/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:01e93745f7f219b703b60ba7afead36cfc4242782be5af484673fc500df12da5", size = 244597, upload-time = "2026-08-15T08:18:21.121Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ce/9962938e179cf9f699d3f1e7b3114b5d7642dee6a893745229f9dd04f274/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:329fc3ccb63ad22d867d84c2adea759a64079a37ba4a343433b02c7a2816871e", size = 231376, upload-time = "2026-08-15T08:18:22.57Z" }, + { url = "https://files.pythonhosted.org/packages/85/54/46000450ada53bd9eac5429a2c8c54cd2d9b39c0c255f229aea9af0948a5/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:bb57753e36e4855b8ca375069482250a6246372331a3e4f3407eaebb007443f5", size = 266715, upload-time = "2026-08-15T08:18:24.235Z" }, + { url = "https://files.pythonhosted.org/packages/3d/bb/618749d70f792b44252a777bf89bfb86823b9bbc1ea13fe8ce759b07f38a/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fce8cbd4997efeb450bd298b54f755dcdff18d496f7a5ddbb4867c6d7c88fdc3", size = 245848, upload-time = "2026-08-15T08:18:25.726Z" }, + { url = "https://files.pythonhosted.org/packages/7e/3f/ffb64458527c7668031d5eb095d978de561958dc9f5b53f8e488a533e603/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6c9cdde8becb25a7fde49924511aa2644d6f8081cc8df8e9452724303348d8e3", size = 264521, upload-time = "2026-08-15T08:18:27.193Z" }, + { url = "https://files.pythonhosted.org/packages/4f/ab/74a55fd803916a35ac461daf002708191aac19b546b80dc8cabfedc63d98/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9ac4444d8d4fd4c4bd08bf451ed3167aa9e7ec6cdb41b648794f1d1103652e36", size = 253054, upload-time = "2026-08-15T08:18:28.568Z" }, + { url = "https://files.pythonhosted.org/packages/a0/2a/6a9034b7d3c60b17499afb482df5878bf9fa20b50cc3887d5ef017a833db/charset_normalizer-3.5.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:f03ac127268b43ef4fe9e6ab6794a6794b49485a0cc0c1db79876d2f33f75bc7", size = 140580, upload-time = "2026-08-15T08:18:30.214Z" }, + { url = "https://files.pythonhosted.org/packages/f3/46/1d362e1a00d035d66b9869e1281eee115907f7e390a16a07824ab5737360/charset_normalizer-3.5.1-cp314-cp314-win32.whl", hash = "sha256:1f5883d77fd409a261abb5dc8ccbe335720d798b1de4abb3b1d47ccbbc76b53b", size = 180325, upload-time = "2026-08-15T08:18:31.877Z" }, + { url = "https://files.pythonhosted.org/packages/7a/7c/4938c329b6a9d446f6a59aa2092ff7118f274209b5ed0e26893d1d30a63c/charset_normalizer-3.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:c658c50ac0c98cd755a2dd50b7977d3bca7df401dcc47fbdfa87db53ef7d4e8b", size = 204175, upload-time = "2026-08-15T08:18:33.466Z" }, + { url = "https://files.pythonhosted.org/packages/ac/33/eeb384dbd8dec570661354592f4f2e1b2fcc92585624d146a000caf53841/charset_normalizer-3.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:4bea7f8ebe90bbd7f0e4a2de42ca6924ba23e3e76418c408ff82f1d46fabd687", size = 184123, upload-time = "2026-08-15T08:18:34.913Z" }, + { url = "https://files.pythonhosted.org/packages/1c/6c/c73fa9d5a85f6ab05395de61c5f6984e0a9ff40bb5ff888d46dff02526c6/charset_normalizer-3.5.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:fbc597639158fd7c14d55e808718848319540f51b0e6746e3eefa59723a4a348", size = 381682, upload-time = "2026-08-15T08:18:36.349Z" }, + { url = "https://files.pythonhosted.org/packages/30/c7/63565f860921457feba93bae6c86fb7746deb4cffeed2f375cb845318146/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e71c909f353863b2b89c83de2ebed71ea6d0df8a6ef65a128193c5e650766bef", size = 240826, upload-time = "2026-08-15T08:18:37.887Z" }, + { url = "https://files.pythonhosted.org/packages/06/ae/7ae8807410dfa33f8e6f1715740adeaafa8a816cc4cb33508f54b1f7c896/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7ac76cf9afd34929d76eb7fcb63be476a4853d8a96f0dcf2d0db68a0cbdf9885", size = 227861, upload-time = "2026-08-15T08:18:39.315Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a3/887c1642f0da26000b0e0652d91071113c0e72cea33952e225cf589f49a9/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a3a370082ce34d0612f421e15fe011c53bb1feff21a26d06ad4fb244dab5a375", size = 260758, upload-time = "2026-08-15T08:18:40.88Z" }, + { url = "https://files.pythonhosted.org/packages/3e/11/e6f5b9a3d0e55b0ef7505cd3765cdd48f22db89994c947b316f52f801fd8/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:256dd4d85d9e4dc595e2bc983c980e73f62ddeb3165c58b4c3dfe78c5c8548c1", size = 259950, upload-time = "2026-08-15T08:18:42.351Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ee/e4e10a94d51cd1ee638aa7e00b65399e6b2a4e8376ab6d2eac9f95586671/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:58d4aa13a59c969dbfdf9e6a9560e242cbfd9e8a8f50c2747714df1a423adf65", size = 249329, upload-time = "2026-08-15T08:18:43.914Z" }, + { url = "https://files.pythonhosted.org/packages/c4/25/d5f4198819e6059735a84e8d0bfb72dc33976da67b97adcd3fb5a5e07ec6/charset_normalizer-3.5.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0c6dfb5ca6723eeed15aa8e564a014d69fcb8812f94eef11fe3631e0508199f5", size = 243137, upload-time = "2026-08-15T08:18:45.368Z" }, + { url = "https://files.pythonhosted.org/packages/a5/e9/e925ca7569cf9fb9701fd82503fee73eea5268fdb856bdd64947092d3daa/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c010f5581d9c612804cc59fcf7b524b707fbcb72828551237ab545bb5c7034af", size = 242820, upload-time = "2026-08-15T08:18:46.842Z" }, + { url = "https://files.pythonhosted.org/packages/34/17/672c251a888ed2aebcdd2fe830ad0104e25ff83c43f5c4f9c15e9fc6853c/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:52ec005752a56ae79547a05c0139ca2501a0c866390b6115008456b9f0e7cde1", size = 230504, upload-time = "2026-08-15T08:18:48.353Z" }, + { url = "https://files.pythonhosted.org/packages/3f/fc/f6a85abebd42ce4da2f1db0aa56cc6a0df1995e318b3875d14401b8381d1/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2bced4061f000f7187254a02ad3433ae17eaf991747ceea2f478422590a5bba9", size = 263087, upload-time = "2026-08-15T08:18:49.859Z" }, + { url = "https://files.pythonhosted.org/packages/98/66/7c42677e739ba66746b297e2046918d793078094dc239e1e72768cffccc6/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9eea3ab2597a5e65fe65296e2d6a84570845a6b55532d90333d740d48bbc850a", size = 243269, upload-time = "2026-08-15T08:18:51.601Z" }, + { url = "https://files.pythonhosted.org/packages/de/d8/a50b79237f417af10f8c2a501ce8d1ca87829a22e69117891ca4ba20a69e/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:496846868fea80e479324862fa877f02411f2fd0f83b79ccee2607aa68b2a032", size = 258766, upload-time = "2026-08-15T08:18:53.23Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1d/0fc91aeaeb3c83b748f532399ce67cf84604b48297405d740000f7a9e786/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:85d5855daafc240cc045c026d7a15fd198a09b0fc8ff6f5ecbb5297b509cb11e", size = 250814, upload-time = "2026-08-15T08:18:54.768Z" }, + { url = "https://files.pythonhosted.org/packages/ae/10/3d8c777cf9024615295aa1b808324ad5b4a77855869c00824bad74ffaf8a/charset_normalizer-3.5.1-cp314-cp314t-win32.whl", hash = "sha256:58d3e12c88e0950bca850ae1f7c256055c097639c2edb9eb123af9807d8b15e4", size = 191074, upload-time = "2026-08-15T08:18:56.305Z" }, + { url = "https://files.pythonhosted.org/packages/4d/81/ae557d3c44d1a1d688696d60563413a0866a91b7ebc50f20df838be3d8c8/charset_normalizer-3.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:acaf604462bf330b0d07e7a07c1d6e4adac79e5fb13e9c5140590542cafacc00", size = 216476, upload-time = "2026-08-15T08:18:57.889Z" }, + { url = "https://files.pythonhosted.org/packages/27/e9/61c01fb8b804692569c036b3fc50495814502dcf13a60649c6055390b02c/charset_normalizer-3.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:fdb8a068947befafba9952162645dc2fecaeb400e64584829ed5e9b2fbe21a7f", size = 194115, upload-time = "2026-08-15T08:18:59.418Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4e/8544831ef59d8f27ce92c80871380fdacc8076a8a56ed62f82e54f991333/charset_normalizer-3.5.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:9085f87b0e38a2b92b8923059b4e8789fe40d9279712d15dcc670048d77079af", size = 342048, upload-time = "2026-08-15T08:19:01.054Z" }, + { url = "https://files.pythonhosted.org/packages/7f/a6/e3b46852424246065355644f4fb6dbccc0239a42a2eee27ecfc8957f0bcd/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2679de311c7946dde5d3b6f44941844133ff5c7cb86099c0061ab1e8901c20a8", size = 242997, upload-time = "2026-08-15T08:19:02.492Z" }, + { url = "https://files.pythonhosted.org/packages/03/3b/0cc9a26777334ab2f2e3089b948bbf4e4fe72ea70b897715ef6415043ec8/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:baf3775a2635e5a11fbd5e4e64ee69c7e86875d224a5c72aca4c141064589a90", size = 237014, upload-time = "2026-08-15T08:19:03.943Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c2/027335f0aa337a2a2e121bac1ad88c4f02ba6053ea0926802784f3db11af/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ac8c94b6539074e0f40899301273ac8402b9b3e01c7b7ba269ff30340aaaf20", size = 266174, upload-time = "2026-08-15T08:19:05.598Z" }, + { url = "https://files.pythonhosted.org/packages/86/d3/e367787febe4e74769dec0f406f2c3c8d1b955fce5aee1fd0f94e8367a45/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fe532b3c966d1fb794e0698e4589d0444017ae77fc0b31edea13c0e35bcc449", size = 263361, upload-time = "2026-08-15T08:19:07.251Z" }, + { url = "https://files.pythonhosted.org/packages/af/3d/391b193eb9f3e84b02f9314088c386debdc0debee843535aaea2e2c6715d/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5c84bec0ab5ae0c64bfe73a7d2adcb5ce73b467523fc27fd6a28ab2aa6cbe35a", size = 252143, upload-time = "2026-08-15T08:19:08.816Z" }, + { url = "https://files.pythonhosted.org/packages/2e/57/de221f1745a90d418199761967e2776bfe2c275a1194220985e8c1d37833/charset_normalizer-3.5.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:854066be00447fa8de2ccbbe893e2ffc4b123ef16d897af794c1e18bd4a714b0", size = 252086, upload-time = "2026-08-15T08:19:10.255Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e3/d119f86a01f9331e8186175f24873b1d74a7ee9e2e4b4d68f9947dae5afd/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:21b82d8082f6f5e7f456ef0bd16323d08de1266efbfeb476e64b2a91d1471a4e", size = 245231, upload-time = "2026-08-15T08:19:11.807Z" }, + { url = "https://files.pythonhosted.org/packages/26/de/d8e48c135ae480879539cdb179c8d3b50c7879497d75dd899b5763b69cee/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:838648accb3a7fd9803fd45c87bce8509648eb0c11bc34e216141300977244f2", size = 241546, upload-time = "2026-08-15T08:19:13.416Z" }, + { url = "https://files.pythonhosted.org/packages/67/c4/217755fd1abc50d326c252922cd642002758095a81ff45010337b8b3ef65/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:195ce897c6153c0700078142cf8efe3e6454ca4cf4357499e4078dfd83396626", size = 267033, upload-time = "2026-08-15T08:19:14.981Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d7/34d8e404e358d2adcc5a228c2134643af00104c8fb0bf525f3688d756f05/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:978eab16f55b4ab2c2a745be9a0a840bf8f09a7f227d9c76eb30214d078865a5", size = 252045, upload-time = "2026-08-15T08:19:16.618Z" }, + { url = "https://files.pythonhosted.org/packages/5e/fa/40414471acf0aa0692ca77305aa00e434fcd8288f0941c93c30e9a5f8f2f/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:cc0329df4caaceb950d2f580b5ac716a377f7059624a0bafaeaf8a218c6ed774", size = 264866, upload-time = "2026-08-15T08:19:18.101Z" }, + { url = "https://files.pythonhosted.org/packages/32/90/fcc850bae791abd2e0c041847f13e270aa08692a79f3e00de6d2dce1cb50/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:687c9ca3035544b113bea2055e180af96fb63c0c476e22a9180f51925186e7b7", size = 253932, upload-time = "2026-08-15T08:19:19.734Z" }, + { url = "https://files.pythonhosted.org/packages/af/af/53afe99068b3c10b4cbae592a52ef72a7c92c0188440e83ee3a078fd8f75/charset_normalizer-3.5.1-cp315-cp315-win32.whl", hash = "sha256:706bfd38730a5ac7a365793269a00f4e988178cec121391f4248d84ad8c972e9", size = 180320, upload-time = "2026-08-15T08:19:21.37Z" }, + { url = "https://files.pythonhosted.org/packages/c9/bc/f46a132041b29e4a8779ed712d3df1bf112e94ca8de58b66d7ec2c0cf8b9/charset_normalizer-3.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:92caef967d287a407085d61176fce4012b1dd62daed4eb6d5ceb26d3d2538712", size = 204174, upload-time = "2026-08-15T08:19:23.088Z" }, + { url = "https://files.pythonhosted.org/packages/a1/5d/9ed554480eda8e447b673648628fdc29574d23dbad01fe11837adedd1cae/charset_normalizer-3.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:5fc45d653ea8c9a20479167e11d4a0f8cb2fa3470737ab6f9c827532313187b7", size = 184126, upload-time = "2026-08-15T08:19:24.471Z" }, + { url = "https://files.pythonhosted.org/packages/3b/32/9b8929bf384061ee1fe5d9c27c6f9776d3d824039ad4e14c88ec00c7808e/charset_normalizer-3.5.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:59171c6e45bf07d0d5cab3b0bf81d945035530f6873398b3b531c31184d46663", size = 381441, upload-time = "2026-08-15T08:19:26.038Z" }, + { url = "https://files.pythonhosted.org/packages/96/10/e9aa7923d3ddac652c99a1c5f7be494e737e151566a44abe018daf757f2c/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9dbdd9205662134957cf0c324f639bdc5031c0ca056e2369e238db75187c0f11", size = 241742, upload-time = "2026-08-15T08:19:27.532Z" }, + { url = "https://files.pythonhosted.org/packages/28/53/a2d249ebddf47b889a100c0bdcb61a2f9dbb8bc24ef325cc062e4f476877/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e4b018dc5a0eee4676e38fe84a47a427816c590b93b55d9025274ec4d6ffc2dc", size = 235298, upload-time = "2026-08-15T08:19:29.274Z" }, + { url = "https://files.pythonhosted.org/packages/7d/07/469f78af590f7d5cd48e20d8dbfa3d66deeff9ba37768c04d886b5afd45c/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ced3fdd71aaa83ce593746c2edb42b7a59cb4c19c8b5c407781c72e493aae55a", size = 262500, upload-time = "2026-08-15T08:19:30.955Z" }, + { url = "https://files.pythonhosted.org/packages/55/66/3bb56a47f7dcba014055b1a1d33c6f08bbe9c1e74dba154cfa25f90ae885/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:19a3dd5aa73cef1c99687c4fc57db016a9c17104ae1185da88ba566a5d3bebe4", size = 258888, upload-time = "2026-08-15T08:19:32.458Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c1/2adc2800903fb013210349313b710a5376856578d9e33e6b9a1d8b36714a/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cc5d36d96478aa9c60654bd932525bf32964c62a7281eafdf16d85003a8d6004", size = 250243, upload-time = "2026-08-15T08:19:33.94Z" }, + { url = "https://files.pythonhosted.org/packages/95/b5/a18d0dd1157ab655cc2cb14a545f4a4784bbad70ab3502412e36097502d9/charset_normalizer-3.5.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:04368edf83514385ffc3e1cfd4546e595f4f1272dd23ba437a93a9cc3741d47b", size = 249871, upload-time = "2026-08-15T08:19:35.413Z" }, + { url = "https://files.pythonhosted.org/packages/ad/c3/525f508cd1e58d0450ac55ed40ac75bc3a97482c59def5278456a5fbf03c/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:9b5db6052055d34d41230fb78d7c439c23dc536a9896f6cb039e8dd92cfc1263", size = 243580, upload-time = "2026-08-15T08:19:36.886Z" }, + { url = "https://files.pythonhosted.org/packages/7c/c1/49a91fe7e97c8140094ca5c64161ab623a70d9f636bf834eace14048acb5/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:252d099029bcbea642f2a06c4ed5046bdf8b5a8150b64afa5e027e88b106e5ee", size = 239807, upload-time = "2026-08-15T08:19:38.392Z" }, + { url = "https://files.pythonhosted.org/packages/d3/58/56a48c296601274c4689b864a8e2dfb209b81dfcb39472753ce95eea662b/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:6199d5606e2bbf2b096cf64d03f8b6790c91081d5ac866b8e7bb6422738cc60c", size = 264083, upload-time = "2026-08-15T08:19:39.856Z" }, + { url = "https://files.pythonhosted.org/packages/10/4c/dc48409274a1817ff349711d26c62aa0c597df865d4d69ef79160c859193/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:77efcff2b23071c349402ac1066667a3d011f62398d81408c9b88ad991747c9e", size = 250317, upload-time = "2026-08-15T08:19:41.53Z" }, + { url = "https://files.pythonhosted.org/packages/81/58/d325912115caec62d6bdd77bbab5e0b7da5d234a9f20affdffcbcb530d0b/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:a5cbd90ecf0fc62e64726917ad083b73001f0563657a87ec3c0b504e277dc90d", size = 258173, upload-time = "2026-08-15T08:19:43.07Z" }, + { url = "https://files.pythonhosted.org/packages/34/f7/b13b1ccae2c8ec63980d13be1890eb73f8aeabbfce02a24aabc0908788f5/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:4d26f14f041e83dd8edfd61f4cd4fa7285d31798b5bf1f28e70c367ba6c41d61", size = 251960, upload-time = "2026-08-15T08:19:44.587Z" }, + { url = "https://files.pythonhosted.org/packages/1e/25/ed3f9919c5aef8cc818be1f972f565f7610d7b2076b8ebb98839516ffc3c/charset_normalizer-3.5.1-cp315-cp315t-win32.whl", hash = "sha256:ac13b004224fb341e1e25a1ed5e19d32f57cdb2a403e01f003b46f051a550f6f", size = 191186, upload-time = "2026-08-15T08:19:46.293Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/43c2b3e9d8267092b913eb8b0603f0f71993c395632886bd37a7223f96cf/charset_normalizer-3.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:35aea775dc2bd5f54cd84a1cd2696cc3207c479cb9cf0bd346f0d343e4300ddb", size = 215947, upload-time = "2026-08-15T08:19:47.853Z" }, + { url = "https://files.pythonhosted.org/packages/a8/76/9aad3e9c8865e5e0efa9a7f6f81c37a67635a985145ecd44528a81e088ee/charset_normalizer-3.5.1-cp315-cp315t-win_arm64.whl", hash = "sha256:fb78f6e7fcd8ad785d28cd577168bc1aaee827b25bb8755638f694794ea98f0a", size = 193909, upload-time = "2026-08-15T08:19:49.383Z" }, + { url = "https://files.pythonhosted.org/packages/5b/97/fb4e82231aba271ffd775a1b4993b0defc4e3059f286ae41d9433409fe85/charset_normalizer-3.5.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:41876ee62a3dddf48ff1121ad8f0798032aa03f2fd35f21f34a4cab14f18d8d2", size = 331467, upload-time = "2026-08-15T08:19:50.959Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2f/fe3f187327aac18e2d54e9d2b08e15d27bf9b642d9e51c219f130fc34d1a/charset_normalizer-3.5.1-cp37-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6dac12ff6b846103483683f60c5f8fee205121adc58ffd87e90a90a3af69e99", size = 253057, upload-time = "2026-08-15T08:19:52.654Z" }, + { url = "https://files.pythonhosted.org/packages/d7/c7/9e48cee5c161fe24da823b61bf381921d77cb994a0a4de148e95018c1984/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cee5dd7c6fb5dd52a0fe2a740f9bc6e3593f5f8b1788bde49de02086f30182b2", size = 240930, upload-time = "2026-08-15T08:19:54.163Z" }, + { url = "https://files.pythonhosted.org/packages/49/e0/716601f3cc69be7b198951150c75ead1ece33c3c8036ff6ffa46029659a0/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:343fb4f2821043bd87095f7b08a1a181febc8e36ac64212143bbfd0a0e1bc235", size = 230822, upload-time = "2026-08-15T08:19:55.807Z" }, + { url = "https://files.pythonhosted.org/packages/d3/05/71bfc5caa0abcc45aea1f6a4d50ac68e59605ddc7666fe8494f4cd229665/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ae4a097991662cd4fff0ddc74e0fe7874f82e00042fa0ea00855645ed0c79598", size = 260037, upload-time = "2026-08-15T08:19:57.312Z" }, + { url = "https://files.pythonhosted.org/packages/c3/92/de7e32ed05341e7a9c4c877c318418197b7f2d66a3b68d561bf2ac57ca3e/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b599739b93b2cbeded49645ae3c8d1405c29ddfbceac1545c87a3f9580a9e96", size = 255097, upload-time = "2026-08-15T08:19:59.056Z" }, + { url = "https://files.pythonhosted.org/packages/f5/7b/ade0a122600319dfa0b1000ab0f9731c94a817904cf3c5de408c73a4ede7/charset_normalizer-3.5.1-cp37-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b39b69b347e5e47a3b5b8cfc005c68c1ba347474e3960236c4944a8ecd174962", size = 250166, upload-time = "2026-08-15T08:20:00.612Z" }, + { url = "https://files.pythonhosted.org/packages/75/9c/019fbb9f4834491a160951349b1a3714439376f66e5f7cf18b4f18f0c7aa/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a2028475ba855475b8b4d3cfeb4994269c967aea8b9892dfba907f4263a863a3", size = 241821, upload-time = "2026-08-15T08:20:02.321Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b8/11d4840bfc99330cc7fbcc2681ee5a044553a6e77655508d8f9b2bff7b34/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:36047af20e17097c3bb9476c2b7655f2f7aa51322c0ba58c07695bedf755a950", size = 232529, upload-time = "2026-08-15T08:20:04.008Z" }, + { url = "https://files.pythonhosted.org/packages/18/96/2b3a21492d9f65171ac75d872f5018260013d00bfa0ff70ec9f179148cbd/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4c4fb141a727957c93edfe5c32a26ceb6b5f6461d67146e2d39f51e16170bea8", size = 260348, upload-time = "2026-08-15T08:20:05.877Z" }, + { url = "https://files.pythonhosted.org/packages/d6/aa/a69a2028e8bd052476c245460ab19d7de595de084dd968f2d75cd50c3e25/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:2f293479cce755c75f1697e87c409b7ae4c555c7dfecb6e988ad13abba943031", size = 247234, upload-time = "2026-08-15T08:20:07.487Z" }, + { url = "https://files.pythonhosted.org/packages/35/8a/3d130aeabcaf3d2466af76b7b141c08d9e89c9016ab4b7cdd0f7dc2d1c62/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_s390x.whl", hash = "sha256:3588e376b3ea2eea84976f67273d679f229e24c66dce7b82ae45aef04ff6e072", size = 256917, upload-time = "2026-08-15T08:20:09.142Z" }, + { url = "https://files.pythonhosted.org/packages/80/c2/a7379b840292d0c1ab9fbd17d1f3967aa81794dc95bc74be8999d7fedcf7/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e199fb99720074809a7720f1c0b4d919eea8b87e88713e0f8f602f7bef543d9d", size = 254846, upload-time = "2026-08-15T08:20:10.727Z" }, + { url = "https://files.pythonhosted.org/packages/01/65/d43b714731bb2f40d4053dfa00ecfc1c5a301f8e3316c5db3a09af59fe94/charset_normalizer-3.5.1-cp37-abi3-win32.whl", hash = "sha256:dd732602a7009217f658d5863d12d79d373a4de0eebc111094bcdd3bb8e0a6cc", size = 174216, upload-time = "2026-08-15T08:20:12.334Z" }, + { url = "https://files.pythonhosted.org/packages/35/4f/b911ed898b26a09789eba9c9200c999aff6c61b4bafaf4838e56d1a1e1a3/charset_normalizer-3.5.1-cp37-abi3-win_amd64.whl", hash = "sha256:70055ff39b97c99e7ae40ea3e393fb62aa2e44dbd9b29f8d14f42fb0025c3959", size = 199764, upload-time = "2026-08-15T08:20:13.908Z" }, + { url = "https://files.pythonhosted.org/packages/f0/a7/920baf467bfd9bf689f3b318340f37aee4572a71f162bd8db51da55ba4fa/charset_normalizer-3.5.1-cp37-abi3-win_arm64.whl", hash = "sha256:87e4f41d375c0b9be2fb5251aee4b8a689169e134535aed81bf085c3b647451e", size = 287318, upload-time = "2026-08-15T08:20:15.551Z" }, + { url = "https://files.pythonhosted.org/packages/cc/61/d01fc49b8dea277640b55a9e15960dbca9fdc8c9fde18e572d39c59f4019/charset_normalizer-3.5.1-py3-none-any.whl", hash = "sha256:6df0ec430f9a831772c23ca5a224cba36517a58a84bb32c32bb59a9fa67c47f6", size = 68658, upload-time = "2026-08-15T08:20:43.306Z" }, +] + +[[package]] +name = "click" +version = "8.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/0e/7fa0ef50764b67090eca4114772a2abf8b6148198475e54c660b97caeee6/click-8.5.0.tar.gz", hash = "sha256:ba0d2089de75ea0310e2dde03160e6ca10009947fb95a182f9b54021bb272e34", size = 382235, upload-time = "2026-08-26T13:33:14.56Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/50/6c0d534c5f134586a8e1ba4e330569e32f057e33372ae556463212fb4cd3/click-8.5.0-py3-none-any.whl", hash = "sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360", size = 125251, upload-time = "2026-08-26T13:33:12.928Z" }, +] + +[[package]] +name = "cloudpickle" +version = "3.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/27/fb/576f067976d320f5f0114a8d9fa1215425441bb35627b1993e5afd8111e5/cloudpickle-3.1.2.tar.gz", hash = "sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414", size = 22330, upload-time = "2025-11-03T09:25:26.604Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl", hash = "sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a", size = 22228, upload-time = "2025-11-03T09:25:25.534Z" }, +] + +[[package]] +name = "cloup" +version = "3.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/ca/cf02e965cfeb70d65c61fd3abb8022aaf5111a0de71b3c73a6ec2113aa25/cloup-3.1.0.tar.gz", hash = "sha256:637c1e628fe98f3f20a5e44da591a72b42bf54d7d4527190bf39ed5f64af7585", size = 230167, upload-time = "2026-05-26T02:48:18.11Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/21/07/644976263e2d346935b35305908bf89eb660ced7fbc7292b096f38c0a80d/cloup-3.1.0-py2.py3-none-any.whl", hash = "sha256:f4cfbdcdc96d30bcbd8c75eaffb0651e9c57180f2aee4b300cf0dd168b8415ed", size = 55028, upload-time = "2026-05-26T02:48:16.711Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "contourpy" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130, upload-time = "2025-04-15T17:47:53.79Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/a3/da4153ec8fe25d263aa48c1a4cbde7f49b59af86f0b6f7862788c60da737/contourpy-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ba38e3f9f330af820c4b27ceb4b9c7feee5fe0493ea53a8720f4792667465934", size = 268551, upload-time = "2025-04-15T17:34:46.581Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6c/330de89ae1087eb622bfca0177d32a7ece50c3ef07b28002de4757d9d875/contourpy-1.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc41ba0714aa2968d1f8674ec97504a8f7e334f48eeacebcaa6256213acb0989", size = 253399, upload-time = "2025-04-15T17:34:51.427Z" }, + { url = "https://files.pythonhosted.org/packages/c1/bd/20c6726b1b7f81a8bee5271bed5c165f0a8e1f572578a9d27e2ccb763cb2/contourpy-1.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9be002b31c558d1ddf1b9b415b162c603405414bacd6932d031c5b5a8b757f0d", size = 312061, upload-time = "2025-04-15T17:34:55.961Z" }, + { url = "https://files.pythonhosted.org/packages/22/fc/a9665c88f8a2473f823cf1ec601de9e5375050f1958cbb356cdf06ef1ab6/contourpy-1.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d2e74acbcba3bfdb6d9d8384cdc4f9260cae86ed9beee8bd5f54fee49a430b9", size = 351956, upload-time = "2025-04-15T17:35:00.992Z" }, + { url = "https://files.pythonhosted.org/packages/25/eb/9f0a0238f305ad8fb7ef42481020d6e20cf15e46be99a1fcf939546a177e/contourpy-1.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e259bced5549ac64410162adc973c5e2fb77f04df4a439d00b478e57a0e65512", size = 320872, upload-time = "2025-04-15T17:35:06.177Z" }, + { url = "https://files.pythonhosted.org/packages/32/5c/1ee32d1c7956923202f00cf8d2a14a62ed7517bdc0ee1e55301227fc273c/contourpy-1.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad687a04bc802cbe8b9c399c07162a3c35e227e2daccf1668eb1f278cb698631", size = 325027, upload-time = "2025-04-15T17:35:11.244Z" }, + { url = "https://files.pythonhosted.org/packages/83/bf/9baed89785ba743ef329c2b07fd0611d12bfecbedbdd3eeecf929d8d3b52/contourpy-1.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cdd22595308f53ef2f891040ab2b93d79192513ffccbd7fe19be7aa773a5e09f", size = 1306641, upload-time = "2025-04-15T17:35:26.701Z" }, + { url = "https://files.pythonhosted.org/packages/d4/cc/74e5e83d1e35de2d28bd97033426b450bc4fd96e092a1f7a63dc7369b55d/contourpy-1.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b4f54d6a2defe9f257327b0f243612dd051cc43825587520b1bf74a31e2f6ef2", size = 1374075, upload-time = "2025-04-15T17:35:43.204Z" }, + { url = "https://files.pythonhosted.org/packages/0c/42/17f3b798fd5e033b46a16f8d9fcb39f1aba051307f5ebf441bad1ecf78f8/contourpy-1.3.2-cp310-cp310-win32.whl", hash = "sha256:f939a054192ddc596e031e50bb13b657ce318cf13d264f095ce9db7dc6ae81c0", size = 177534, upload-time = "2025-04-15T17:35:46.554Z" }, + { url = "https://files.pythonhosted.org/packages/54/ec/5162b8582f2c994721018d0c9ece9dc6ff769d298a8ac6b6a652c307e7df/contourpy-1.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c440093bbc8fc21c637c03bafcbef95ccd963bc6e0514ad887932c18ca2a759a", size = 221188, upload-time = "2025-04-15T17:35:50.064Z" }, + { url = "https://files.pythonhosted.org/packages/b3/b9/ede788a0b56fc5b071639d06c33cb893f68b1178938f3425debebe2dab78/contourpy-1.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a37a2fb93d4df3fc4c0e363ea4d16f83195fc09c891bc8ce072b9d084853445", size = 269636, upload-time = "2025-04-15T17:35:54.473Z" }, + { url = "https://files.pythonhosted.org/packages/e6/75/3469f011d64b8bbfa04f709bfc23e1dd71be54d05b1b083be9f5b22750d1/contourpy-1.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b7cd50c38f500bbcc9b6a46643a40e0913673f869315d8e70de0438817cb7773", size = 254636, upload-time = "2025-04-15T17:35:58.283Z" }, + { url = "https://files.pythonhosted.org/packages/8d/2f/95adb8dae08ce0ebca4fd8e7ad653159565d9739128b2d5977806656fcd2/contourpy-1.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6658ccc7251a4433eebd89ed2672c2ed96fba367fd25ca9512aa92a4b46c4f1", size = 313053, upload-time = "2025-04-15T17:36:03.235Z" }, + { url = "https://files.pythonhosted.org/packages/c3/a6/8ccf97a50f31adfa36917707fe39c9a0cbc24b3bbb58185577f119736cc9/contourpy-1.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:70771a461aaeb335df14deb6c97439973d253ae70660ca085eec25241137ef43", size = 352985, upload-time = "2025-04-15T17:36:08.275Z" }, + { url = "https://files.pythonhosted.org/packages/1d/b6/7925ab9b77386143f39d9c3243fdd101621b4532eb126743201160ffa7e6/contourpy-1.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65a887a6e8c4cd0897507d814b14c54a8c2e2aa4ac9f7686292f9769fcf9a6ab", size = 323750, upload-time = "2025-04-15T17:36:13.29Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f3/20c5d1ef4f4748e52d60771b8560cf00b69d5c6368b5c2e9311bcfa2a08b/contourpy-1.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3859783aefa2b8355697f16642695a5b9792e7a46ab86da1118a4a23a51a33d7", size = 326246, upload-time = "2025-04-15T17:36:18.329Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e5/9dae809e7e0b2d9d70c52b3d24cba134dd3dad979eb3e5e71f5df22ed1f5/contourpy-1.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eab0f6db315fa4d70f1d8ab514e527f0366ec021ff853d7ed6a2d33605cf4b83", size = 1308728, upload-time = "2025-04-15T17:36:33.878Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/0058ba34aeea35c0b442ae61a4f4d4ca84d6df8f91309bc2d43bb8dd248f/contourpy-1.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d91a3ccc7fea94ca0acab82ceb77f396d50a1f67412efe4c526f5d20264e6ecd", size = 1375762, upload-time = "2025-04-15T17:36:51.295Z" }, + { url = "https://files.pythonhosted.org/packages/09/33/7174bdfc8b7767ef2c08ed81244762d93d5c579336fc0b51ca57b33d1b80/contourpy-1.3.2-cp311-cp311-win32.whl", hash = "sha256:1c48188778d4d2f3d48e4643fb15d8608b1d01e4b4d6b0548d9b336c28fc9b6f", size = 178196, upload-time = "2025-04-15T17:36:55.002Z" }, + { url = "https://files.pythonhosted.org/packages/5e/fe/4029038b4e1c4485cef18e480b0e2cd2d755448bb071eb9977caac80b77b/contourpy-1.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:5ebac872ba09cb8f2131c46b8739a7ff71de28a24c869bcad554477eb089a878", size = 222017, upload-time = "2025-04-15T17:36:58.576Z" }, + { url = "https://files.pythonhosted.org/packages/34/f7/44785876384eff370c251d58fd65f6ad7f39adce4a093c934d4a67a7c6b6/contourpy-1.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4caf2bcd2969402bf77edc4cb6034c7dd7c0803213b3523f111eb7460a51b8d2", size = 271580, upload-time = "2025-04-15T17:37:03.105Z" }, + { url = "https://files.pythonhosted.org/packages/93/3b/0004767622a9826ea3d95f0e9d98cd8729015768075d61f9fea8eeca42a8/contourpy-1.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:82199cb78276249796419fe36b7386bd8d2cc3f28b3bc19fe2454fe2e26c4c15", size = 255530, upload-time = "2025-04-15T17:37:07.026Z" }, + { url = "https://files.pythonhosted.org/packages/e7/bb/7bd49e1f4fa805772d9fd130e0d375554ebc771ed7172f48dfcd4ca61549/contourpy-1.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:106fab697af11456fcba3e352ad50effe493a90f893fca6c2ca5c033820cea92", size = 307688, upload-time = "2025-04-15T17:37:11.481Z" }, + { url = "https://files.pythonhosted.org/packages/fc/97/e1d5dbbfa170725ef78357a9a0edc996b09ae4af170927ba8ce977e60a5f/contourpy-1.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d14f12932a8d620e307f715857107b1d1845cc44fdb5da2bc8e850f5ceba9f87", size = 347331, upload-time = "2025-04-15T17:37:18.212Z" }, + { url = "https://files.pythonhosted.org/packages/6f/66/e69e6e904f5ecf6901be3dd16e7e54d41b6ec6ae3405a535286d4418ffb4/contourpy-1.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:532fd26e715560721bb0d5fc7610fce279b3699b018600ab999d1be895b09415", size = 318963, upload-time = "2025-04-15T17:37:22.76Z" }, + { url = "https://files.pythonhosted.org/packages/a8/32/b8a1c8965e4f72482ff2d1ac2cd670ce0b542f203c8e1d34e7c3e6925da7/contourpy-1.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b383144cf2d2c29f01a1e8170f50dacf0eac02d64139dcd709a8ac4eb3cfe", size = 323681, upload-time = "2025-04-15T17:37:33.001Z" }, + { url = "https://files.pythonhosted.org/packages/30/c6/12a7e6811d08757c7162a541ca4c5c6a34c0f4e98ef2b338791093518e40/contourpy-1.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c49f73e61f1f774650a55d221803b101d966ca0c5a2d6d5e4320ec3997489441", size = 1308674, upload-time = "2025-04-15T17:37:48.64Z" }, + { url = "https://files.pythonhosted.org/packages/2a/8a/bebe5a3f68b484d3a2b8ffaf84704b3e343ef1addea528132ef148e22b3b/contourpy-1.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3d80b2c0300583228ac98d0a927a1ba6a2ba6b8a742463c564f1d419ee5b211e", size = 1380480, upload-time = "2025-04-15T17:38:06.7Z" }, + { url = "https://files.pythonhosted.org/packages/34/db/fcd325f19b5978fb509a7d55e06d99f5f856294c1991097534360b307cf1/contourpy-1.3.2-cp312-cp312-win32.whl", hash = "sha256:90df94c89a91b7362e1142cbee7568f86514412ab8a2c0d0fca72d7e91b62912", size = 178489, upload-time = "2025-04-15T17:38:10.338Z" }, + { url = "https://files.pythonhosted.org/packages/01/c8/fadd0b92ffa7b5eb5949bf340a63a4a496a6930a6c37a7ba0f12acb076d6/contourpy-1.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:8c942a01d9163e2e5cfb05cb66110121b8d07ad438a17f9e766317bcb62abf73", size = 223042, upload-time = "2025-04-15T17:38:14.239Z" }, + { url = "https://files.pythonhosted.org/packages/2e/61/5673f7e364b31e4e7ef6f61a4b5121c5f170f941895912f773d95270f3a2/contourpy-1.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:de39db2604ae755316cb5967728f4bea92685884b1e767b7c24e983ef5f771cb", size = 271630, upload-time = "2025-04-15T17:38:19.142Z" }, + { url = "https://files.pythonhosted.org/packages/ff/66/a40badddd1223822c95798c55292844b7e871e50f6bfd9f158cb25e0bd39/contourpy-1.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3f9e896f447c5c8618f1edb2bafa9a4030f22a575ec418ad70611450720b5b08", size = 255670, upload-time = "2025-04-15T17:38:23.688Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c7/cf9fdee8200805c9bc3b148f49cb9482a4e3ea2719e772602a425c9b09f8/contourpy-1.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71e2bd4a1c4188f5c2b8d274da78faab884b59df20df63c34f74aa1813c4427c", size = 306694, upload-time = "2025-04-15T17:38:28.238Z" }, + { url = "https://files.pythonhosted.org/packages/dd/e7/ccb9bec80e1ba121efbffad7f38021021cda5be87532ec16fd96533bb2e0/contourpy-1.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de425af81b6cea33101ae95ece1f696af39446db9682a0b56daaa48cfc29f38f", size = 345986, upload-time = "2025-04-15T17:38:33.502Z" }, + { url = "https://files.pythonhosted.org/packages/dc/49/ca13bb2da90391fa4219fdb23b078d6065ada886658ac7818e5441448b78/contourpy-1.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:977e98a0e0480d3fe292246417239d2d45435904afd6d7332d8455981c408b85", size = 318060, upload-time = "2025-04-15T17:38:38.672Z" }, + { url = "https://files.pythonhosted.org/packages/c8/65/5245ce8c548a8422236c13ffcdcdada6a2a812c361e9e0c70548bb40b661/contourpy-1.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:434f0adf84911c924519d2b08fc10491dd282b20bdd3fa8f60fd816ea0b48841", size = 322747, upload-time = "2025-04-15T17:38:43.712Z" }, + { url = "https://files.pythonhosted.org/packages/72/30/669b8eb48e0a01c660ead3752a25b44fdb2e5ebc13a55782f639170772f9/contourpy-1.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c66c4906cdbc50e9cba65978823e6e00b45682eb09adbb78c9775b74eb222422", size = 1308895, upload-time = "2025-04-15T17:39:00.224Z" }, + { url = "https://files.pythonhosted.org/packages/05/5a/b569f4250decee6e8d54498be7bdf29021a4c256e77fe8138c8319ef8eb3/contourpy-1.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8b7fc0cd78ba2f4695fd0a6ad81a19e7e3ab825c31b577f384aa9d7817dc3bef", size = 1379098, upload-time = "2025-04-15T17:43:29.649Z" }, + { url = "https://files.pythonhosted.org/packages/19/ba/b227c3886d120e60e41b28740ac3617b2f2b971b9f601c835661194579f1/contourpy-1.3.2-cp313-cp313-win32.whl", hash = "sha256:15ce6ab60957ca74cff444fe66d9045c1fd3e92c8936894ebd1f3eef2fff075f", size = 178535, upload-time = "2025-04-15T17:44:44.532Z" }, + { url = "https://files.pythonhosted.org/packages/12/6e/2fed56cd47ca739b43e892707ae9a13790a486a3173be063681ca67d2262/contourpy-1.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e1578f7eafce927b168752ed7e22646dad6cd9bca673c60bff55889fa236ebf9", size = 223096, upload-time = "2025-04-15T17:44:48.194Z" }, + { url = "https://files.pythonhosted.org/packages/54/4c/e76fe2a03014a7c767d79ea35c86a747e9325537a8b7627e0e5b3ba266b4/contourpy-1.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0475b1f6604896bc7c53bb070e355e9321e1bc0d381735421a2d2068ec56531f", size = 285090, upload-time = "2025-04-15T17:43:34.084Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e2/5aba47debd55d668e00baf9651b721e7733975dc9fc27264a62b0dd26eb8/contourpy-1.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c85bb486e9be652314bb5b9e2e3b0d1b2e643d5eec4992c0fbe8ac71775da739", size = 268643, upload-time = "2025-04-15T17:43:38.626Z" }, + { url = "https://files.pythonhosted.org/packages/a1/37/cd45f1f051fe6230f751cc5cdd2728bb3a203f5619510ef11e732109593c/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:745b57db7758f3ffc05a10254edd3182a2a83402a89c00957a8e8a22f5582823", size = 310443, upload-time = "2025-04-15T17:43:44.522Z" }, + { url = "https://files.pythonhosted.org/packages/8b/a2/36ea6140c306c9ff6dd38e3bcec80b3b018474ef4d17eb68ceecd26675f4/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:970e9173dbd7eba9b4e01aab19215a48ee5dd3f43cef736eebde064a171f89a5", size = 349865, upload-time = "2025-04-15T17:43:49.545Z" }, + { url = "https://files.pythonhosted.org/packages/95/b7/2fc76bc539693180488f7b6cc518da7acbbb9e3b931fd9280504128bf956/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6c4639a9c22230276b7bffb6a850dfc8258a2521305e1faefe804d006b2e532", size = 321162, upload-time = "2025-04-15T17:43:54.203Z" }, + { url = "https://files.pythonhosted.org/packages/f4/10/76d4f778458b0aa83f96e59d65ece72a060bacb20cfbee46cf6cd5ceba41/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc829960f34ba36aad4302e78eabf3ef16a3a100863f0d4eeddf30e8a485a03b", size = 327355, upload-time = "2025-04-15T17:44:01.025Z" }, + { url = "https://files.pythonhosted.org/packages/43/a3/10cf483ea683f9f8ab096c24bad3cce20e0d1dd9a4baa0e2093c1c962d9d/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d32530b534e986374fc19eaa77fcb87e8a99e5431499949b828312bdcd20ac52", size = 1307935, upload-time = "2025-04-15T17:44:17.322Z" }, + { url = "https://files.pythonhosted.org/packages/78/73/69dd9a024444489e22d86108e7b913f3528f56cfc312b5c5727a44188471/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e298e7e70cf4eb179cc1077be1c725b5fd131ebc81181bf0c03525c8abc297fd", size = 1372168, upload-time = "2025-04-15T17:44:33.43Z" }, + { url = "https://files.pythonhosted.org/packages/0f/1b/96d586ccf1b1a9d2004dd519b25fbf104a11589abfd05484ff12199cca21/contourpy-1.3.2-cp313-cp313t-win32.whl", hash = "sha256:d0e589ae0d55204991450bb5c23f571c64fe43adaa53f93fc902a84c96f52fe1", size = 189550, upload-time = "2025-04-15T17:44:37.092Z" }, + { url = "https://files.pythonhosted.org/packages/b0/e6/6000d0094e8a5e32ad62591c8609e269febb6e4db83a1c75ff8868b42731/contourpy-1.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:78e9253c3de756b3f6a5174d024c4835acd59eb3f8e2ca13e775dbffe1558f69", size = 238214, upload-time = "2025-04-15T17:44:40.827Z" }, + { url = "https://files.pythonhosted.org/packages/33/05/b26e3c6ecc05f349ee0013f0bb850a761016d89cec528a98193a48c34033/contourpy-1.3.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fd93cc7f3139b6dd7aab2f26a90dde0aa9fc264dbf70f6740d498a70b860b82c", size = 265681, upload-time = "2025-04-15T17:44:59.314Z" }, + { url = "https://files.pythonhosted.org/packages/2b/25/ac07d6ad12affa7d1ffed11b77417d0a6308170f44ff20fa1d5aa6333f03/contourpy-1.3.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:107ba8a6a7eec58bb475329e6d3b95deba9440667c4d62b9b6063942b61d7f16", size = 315101, upload-time = "2025-04-15T17:45:04.165Z" }, + { url = "https://files.pythonhosted.org/packages/8f/4d/5bb3192bbe9d3f27e3061a6a8e7733c9120e203cb8515767d30973f71030/contourpy-1.3.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ded1706ed0c1049224531b81128efbd5084598f18d8a2d9efae833edbd2b40ad", size = 220599, upload-time = "2025-04-15T17:45:08.456Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c0/91f1215d0d9f9f343e4773ba6c9b89e8c0cc7a64a6263f21139da639d848/contourpy-1.3.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5f5964cdad279256c084b69c3f412b7801e15356b16efa9d78aa974041903da0", size = 266807, upload-time = "2025-04-15T17:45:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/d4/79/6be7e90c955c0487e7712660d6cead01fa17bff98e0ea275737cc2bc8e71/contourpy-1.3.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49b65a95d642d4efa8f64ba12558fcb83407e58a2dfba9d796d77b63ccfcaff5", size = 318729, upload-time = "2025-04-15T17:45:20.166Z" }, + { url = "https://files.pythonhosted.org/packages/87/68/7f46fb537958e87427d98a4074bcde4b67a70b04900cfc5ce29bc2f556c1/contourpy-1.3.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8c5acb8dddb0752bf252e01a3035b21443158910ac16a3b0d20e7fed7d534ce5", size = 221791, upload-time = "2025-04-15T17:45:24.794Z" }, +] + +[[package]] +name = "contourpy" +version = "1.3.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13'", + "python_full_version == '3.12.*'", + "python_full_version == '3.11.*'", +] +dependencies = [ + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "2.5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/2e/c4390a31919d8a78b90e8ecf87cd4b4c4f05a5b48d05ec17db8e5404c6f4/contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1", size = 288773, upload-time = "2025-07-26T12:01:02.277Z" }, + { url = "https://files.pythonhosted.org/packages/0d/44/c4b0b6095fef4dc9c420e041799591e3b63e9619e3044f7f4f6c21c0ab24/contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381", size = 270149, upload-time = "2025-07-26T12:01:04.072Z" }, + { url = "https://files.pythonhosted.org/packages/30/2e/dd4ced42fefac8470661d7cb7e264808425e6c5d56d175291e93890cce09/contourpy-1.3.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7", size = 329222, upload-time = "2025-07-26T12:01:05.688Z" }, + { url = "https://files.pythonhosted.org/packages/f2/74/cc6ec2548e3d276c71389ea4802a774b7aa3558223b7bade3f25787fafc2/contourpy-1.3.3-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1", size = 377234, upload-time = "2025-07-26T12:01:07.054Z" }, + { url = "https://files.pythonhosted.org/packages/03/b3/64ef723029f917410f75c09da54254c5f9ea90ef89b143ccadb09df14c15/contourpy-1.3.3-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a", size = 380555, upload-time = "2025-07-26T12:01:08.801Z" }, + { url = "https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db", size = 355238, upload-time = "2025-07-26T12:01:10.319Z" }, + { url = "https://files.pythonhosted.org/packages/98/56/f914f0dd678480708a04cfd2206e7c382533249bc5001eb9f58aa693e200/contourpy-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620", size = 1326218, upload-time = "2025-07-26T12:01:12.659Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d7/4a972334a0c971acd5172389671113ae82aa7527073980c38d5868ff1161/contourpy-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f", size = 1392867, upload-time = "2025-07-26T12:01:15.533Z" }, + { url = "https://files.pythonhosted.org/packages/75/3e/f2cc6cd56dc8cff46b1a56232eabc6feea52720083ea71ab15523daab796/contourpy-1.3.3-cp311-cp311-win32.whl", hash = "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff", size = 183677, upload-time = "2025-07-26T12:01:17.088Z" }, + { url = "https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42", size = 225234, upload-time = "2025-07-26T12:01:18.256Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b6/71771e02c2e004450c12b1120a5f488cad2e4d5b590b1af8bad060360fe4/contourpy-1.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470", size = 193123, upload-time = "2025-07-26T12:01:19.848Z" }, + { url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419, upload-time = "2025-07-26T12:01:21.16Z" }, + { url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979, upload-time = "2025-07-26T12:01:22.448Z" }, + { url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653, upload-time = "2025-07-26T12:01:24.155Z" }, + { url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536, upload-time = "2025-07-26T12:01:25.91Z" }, + { url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397, upload-time = "2025-07-26T12:01:27.152Z" }, + { url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601, upload-time = "2025-07-26T12:01:28.808Z" }, + { url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288, upload-time = "2025-07-26T12:01:31.198Z" }, + { url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386, upload-time = "2025-07-26T12:01:33.947Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018, upload-time = "2025-07-26T12:01:35.64Z" }, + { url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567, upload-time = "2025-07-26T12:01:36.804Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655, upload-time = "2025-07-26T12:01:37.999Z" }, + { url = "https://files.pythonhosted.org/packages/68/35/0167aad910bbdb9599272bd96d01a9ec6852f36b9455cf2ca67bd4cc2d23/contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5", size = 293257, upload-time = "2025-07-26T12:01:39.367Z" }, + { url = "https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1", size = 274034, upload-time = "2025-07-26T12:01:40.645Z" }, + { url = "https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286", size = 334672, upload-time = "2025-07-26T12:01:41.942Z" }, + { url = "https://files.pythonhosted.org/packages/ed/93/b43d8acbe67392e659e1d984700e79eb67e2acb2bd7f62012b583a7f1b55/contourpy-1.3.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5", size = 381234, upload-time = "2025-07-26T12:01:43.499Z" }, + { url = "https://files.pythonhosted.org/packages/46/3b/bec82a3ea06f66711520f75a40c8fc0b113b2a75edb36aa633eb11c4f50f/contourpy-1.3.3-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67", size = 385169, upload-time = "2025-07-26T12:01:45.219Z" }, + { url = "https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9", size = 362859, upload-time = "2025-07-26T12:01:46.519Z" }, + { url = "https://files.pythonhosted.org/packages/33/71/e2a7945b7de4e58af42d708a219f3b2f4cff7386e6b6ab0a0fa0033c49a9/contourpy-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659", size = 1332062, upload-time = "2025-07-26T12:01:48.964Z" }, + { url = "https://files.pythonhosted.org/packages/12/fc/4e87ac754220ccc0e807284f88e943d6d43b43843614f0a8afa469801db0/contourpy-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7", size = 1403932, upload-time = "2025-07-26T12:01:51.979Z" }, + { url = "https://files.pythonhosted.org/packages/a6/2e/adc197a37443f934594112222ac1aa7dc9a98faf9c3842884df9a9d8751d/contourpy-1.3.3-cp313-cp313-win32.whl", hash = "sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d", size = 185024, upload-time = "2025-07-26T12:01:53.245Z" }, + { url = "https://files.pythonhosted.org/packages/18/0b/0098c214843213759692cc638fce7de5c289200a830e5035d1791d7a2338/contourpy-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263", size = 226578, upload-time = "2025-07-26T12:01:54.422Z" }, + { url = "https://files.pythonhosted.org/packages/8a/9a/2f6024a0c5995243cd63afdeb3651c984f0d2bc727fd98066d40e141ad73/contourpy-1.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9", size = 193524, upload-time = "2025-07-26T12:01:55.73Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b3/f8a1a86bd3298513f500e5b1f5fd92b69896449f6cab6a146a5d52715479/contourpy-1.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d", size = 306730, upload-time = "2025-07-26T12:01:57.051Z" }, + { url = "https://files.pythonhosted.org/packages/3f/11/4780db94ae62fc0c2053909b65dc3246bd7cecfc4f8a20d957ad43aa4ad8/contourpy-1.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216", size = 287897, upload-time = "2025-07-26T12:01:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/ae/15/e59f5f3ffdd6f3d4daa3e47114c53daabcb18574a26c21f03dc9e4e42ff0/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae", size = 326751, upload-time = "2025-07-26T12:02:00.343Z" }, + { url = "https://files.pythonhosted.org/packages/0f/81/03b45cfad088e4770b1dcf72ea78d3802d04200009fb364d18a493857210/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20", size = 375486, upload-time = "2025-07-26T12:02:02.128Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ba/49923366492ffbdd4486e970d421b289a670ae8cf539c1ea9a09822b371a/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99", size = 388106, upload-time = "2025-07-26T12:02:03.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/52/5b00ea89525f8f143651f9f03a0df371d3cbd2fccd21ca9b768c7a6500c2/contourpy-1.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b", size = 352548, upload-time = "2025-07-26T12:02:05.165Z" }, + { url = "https://files.pythonhosted.org/packages/32/1d/a209ec1a3a3452d490f6b14dd92e72280c99ae3d1e73da74f8277d4ee08f/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a", size = 1322297, upload-time = "2025-07-26T12:02:07.379Z" }, + { url = "https://files.pythonhosted.org/packages/bc/9e/46f0e8ebdd884ca0e8877e46a3f4e633f6c9c8c4f3f6e72be3fe075994aa/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e", size = 1391023, upload-time = "2025-07-26T12:02:10.171Z" }, + { url = "https://files.pythonhosted.org/packages/b9/70/f308384a3ae9cd2209e0849f33c913f658d3326900d0ff5d378d6a1422d2/contourpy-1.3.3-cp313-cp313t-win32.whl", hash = "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3", size = 196157, upload-time = "2025-07-26T12:02:11.488Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dd/880f890a6663b84d9e34a6f88cded89d78f0091e0045a284427cb6b18521/contourpy-1.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8", size = 240570, upload-time = "2025-07-26T12:02:12.754Z" }, + { url = "https://files.pythonhosted.org/packages/80/99/2adc7d8ffead633234817ef8e9a87115c8a11927a94478f6bb3d3f4d4f7d/contourpy-1.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301", size = 199713, upload-time = "2025-07-26T12:02:14.4Z" }, + { url = "https://files.pythonhosted.org/packages/72/8b/4546f3ab60f78c514ffb7d01a0bd743f90de36f0019d1be84d0a708a580a/contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a", size = 292189, upload-time = "2025-07-26T12:02:16.095Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77", size = 273251, upload-time = "2025-07-26T12:02:17.524Z" }, + { url = "https://files.pythonhosted.org/packages/b1/71/f93e1e9471d189f79d0ce2497007731c1e6bf9ef6d1d61b911430c3db4e5/contourpy-1.3.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5", size = 335810, upload-time = "2025-07-26T12:02:18.9Z" }, + { url = "https://files.pythonhosted.org/packages/91/f9/e35f4c1c93f9275d4e38681a80506b5510e9327350c51f8d4a5a724d178c/contourpy-1.3.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4", size = 382871, upload-time = "2025-07-26T12:02:20.418Z" }, + { url = "https://files.pythonhosted.org/packages/b5/71/47b512f936f66a0a900d81c396a7e60d73419868fba959c61efed7a8ab46/contourpy-1.3.3-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36", size = 386264, upload-time = "2025-07-26T12:02:21.916Z" }, + { url = "https://files.pythonhosted.org/packages/04/5f/9ff93450ba96b09c7c2b3f81c94de31c89f92292f1380261bd7195bea4ea/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3", size = 363819, upload-time = "2025-07-26T12:02:23.759Z" }, + { url = "https://files.pythonhosted.org/packages/3e/a6/0b185d4cc480ee494945cde102cb0149ae830b5fa17bf855b95f2e70ad13/contourpy-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b", size = 1333650, upload-time = "2025-07-26T12:02:26.181Z" }, + { url = "https://files.pythonhosted.org/packages/43/d7/afdc95580ca56f30fbcd3060250f66cedbde69b4547028863abd8aa3b47e/contourpy-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36", size = 1404833, upload-time = "2025-07-26T12:02:28.782Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e2/366af18a6d386f41132a48f033cbd2102e9b0cf6345d35ff0826cd984566/contourpy-1.3.3-cp314-cp314-win32.whl", hash = "sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d", size = 189692, upload-time = "2025-07-26T12:02:30.128Z" }, + { url = "https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd", size = 232424, upload-time = "2025-07-26T12:02:31.395Z" }, + { url = "https://files.pythonhosted.org/packages/18/79/a9416650df9b525737ab521aa181ccc42d56016d2123ddcb7b58e926a42c/contourpy-1.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339", size = 198300, upload-time = "2025-07-26T12:02:32.956Z" }, + { url = "https://files.pythonhosted.org/packages/1f/42/38c159a7d0f2b7b9c04c64ab317042bb6952b713ba875c1681529a2932fe/contourpy-1.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772", size = 306769, upload-time = "2025-07-26T12:02:34.2Z" }, + { url = "https://files.pythonhosted.org/packages/c3/6c/26a8205f24bca10974e77460de68d3d7c63e282e23782f1239f226fcae6f/contourpy-1.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77", size = 287892, upload-time = "2025-07-26T12:02:35.807Z" }, + { url = "https://files.pythonhosted.org/packages/66/06/8a475c8ab718ebfd7925661747dbb3c3ee9c82ac834ccb3570be49d129f4/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13", size = 326748, upload-time = "2025-07-26T12:02:37.193Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a3/c5ca9f010a44c223f098fccd8b158bb1cb287378a31ac141f04730dc49be/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe", size = 375554, upload-time = "2025-07-26T12:02:38.894Z" }, + { url = "https://files.pythonhosted.org/packages/80/5b/68bd33ae63fac658a4145088c1e894405e07584a316738710b636c6d0333/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f", size = 388118, upload-time = "2025-07-26T12:02:40.642Z" }, + { url = "https://files.pythonhosted.org/packages/40/52/4c285a6435940ae25d7410a6c36bda5145839bc3f0beb20c707cda18b9d2/contourpy-1.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0", size = 352555, upload-time = "2025-07-26T12:02:42.25Z" }, + { url = "https://files.pythonhosted.org/packages/24/ee/3e81e1dd174f5c7fefe50e85d0892de05ca4e26ef1c9a59c2a57e43b865a/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4", size = 1322295, upload-time = "2025-07-26T12:02:44.668Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b2/6d913d4d04e14379de429057cd169e5e00f6c2af3bb13e1710bcbdb5da12/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f", size = 1391027, upload-time = "2025-07-26T12:02:47.09Z" }, + { url = "https://files.pythonhosted.org/packages/93/8a/68a4ec5c55a2971213d29a9374913f7e9f18581945a7a31d1a39b5d2dfe5/contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae", size = 202428, upload-time = "2025-07-26T12:02:48.691Z" }, + { url = "https://files.pythonhosted.org/packages/fa/96/fd9f641ffedc4fa3ace923af73b9d07e869496c9cc7a459103e6e978992f/contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc", size = 250331, upload-time = "2025-07-26T12:02:50.137Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831, upload-time = "2025-07-26T12:02:51.449Z" }, + { url = "https://files.pythonhosted.org/packages/a5/29/8dcfe16f0107943fa92388c23f6e05cff0ba58058c4c95b00280d4c75a14/contourpy-1.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497", size = 278809, upload-time = "2025-07-26T12:02:52.74Z" }, + { url = "https://files.pythonhosted.org/packages/85/a9/8b37ef4f7dafeb335daee3c8254645ef5725be4d9c6aa70b50ec46ef2f7e/contourpy-1.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8", size = 261593, upload-time = "2025-07-26T12:02:54.037Z" }, + { url = "https://files.pythonhosted.org/packages/0a/59/ebfb8c677c75605cc27f7122c90313fd2f375ff3c8d19a1694bda74aaa63/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e", size = 302202, upload-time = "2025-07-26T12:02:55.947Z" }, + { url = "https://files.pythonhosted.org/packages/3c/37/21972a15834d90bfbfb009b9d004779bd5a07a0ec0234e5ba8f64d5736f4/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989", size = 329207, upload-time = "2025-07-26T12:02:57.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315, upload-time = "2025-07-26T12:02:58.801Z" }, +] + +[[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, 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, upload-time = "2023-10-07T05:32:16.783Z" }, +] + +[[package]] +name = "cython" +version = "3.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/d8/4981ef716ad0e3ff0d3ef383aefc6b03c4a88dee33b272bf8e0d833001ca/cython-3.3.0.tar.gz", hash = "sha256:eed0d93fbca7087f143b42c34b05a825849bdf17f101572c2105acfa49aa88b8", size = 3727515, upload-time = "2026-08-22T05:16:39.493Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/62/eae58684ab7a46e877f3193f393244a4de6fd5e700cca4265a7d33664f38/cython-3.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0507d9caf7dc35f1212627145d5d13dbc5dd7128529a6608ab72690472fa688e", size = 3142413, upload-time = "2026-08-22T05:16:50.378Z" }, + { url = "https://files.pythonhosted.org/packages/af/67/dffaf12b7203f7e936d98b967e065c50a7883f152c51ded44ed8762128f4/cython-3.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ec09dbf73ff4f7be2b339b995fadae9c4bb517bbbed7ec11d6fe99c2092b48fd", size = 3134644, upload-time = "2026-08-22T05:16:59.112Z" }, + { url = "https://files.pythonhosted.org/packages/da/0f/95bad838a80ac52c9e982dad00bd9a0b2bad57fb4c688e5f53ac3ef65ff0/cython-3.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03bc5333932f5dda3ba9315298ecdd21daa1b58410bb1f8ce04c78ec8337130a", size = 3143472, upload-time = "2026-08-22T05:17:07.956Z" }, + { url = "https://files.pythonhosted.org/packages/2f/cc/abc977cf683140e372714acea42164ecfc5cd3d3984ed025860e6d830ee4/cython-3.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:03056533fe4fdbc4f1d34a39178f9a4937ff35196f8bcdde2a67b5b5809c61fe", size = 3134892, upload-time = "2026-08-22T05:17:16.675Z" }, + { url = "https://files.pythonhosted.org/packages/8a/7f/e409f76bb955ecdcb746b80350b945fbb808846d797346d647a37e1790ca/cython-3.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0deedc2e9a5a664e1adfa4c2d310aa7b54903e1a647c274b6c9213f77a02d637", size = 3186700, upload-time = "2026-08-22T05:17:24.288Z" }, + { url = "https://files.pythonhosted.org/packages/ea/45/7f6988070013e16918e39b1b3dab9c5f2c8e404253a7fd10ee685bbd6902/cython-3.3.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:596e8df019372a2cd417805015022d42cb8ee4e1803ccdc11ed00e451625fb66", size = 3177681, upload-time = "2026-08-22T05:17:32.523Z" }, + { url = "https://files.pythonhosted.org/packages/14/59/bc1a84b434cb5bebb0cd6f50da8f239d35a5c141b20fdeafc2817fd87778/cython-3.3.0-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:e0d2713d2b292c826bc21dc8732bd9e47628103aa3764180c881e04b3fef95dc", size = 3063660, upload-time = "2026-08-22T05:17:40.923Z" }, + { url = "https://files.pythonhosted.org/packages/bf/77/67b0b24e45073a699610e50f00c18474ff9b09ea29ecc95083bdf5e60acd/cython-3.3.0-py3-none-any.whl", hash = "sha256:9b24b5c8cd536946b62086fcafee6d5509d3f549f72d553d2336af87ffbe0da1", size = 1349151, upload-time = "2026-08-22T05:16:36.741Z" }, +] + +[[package]] +name = "decorator" +version = "5.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/60/8b/32f9823da46cde7df2087faa08cd98d01b908f8dcab982cdba9c84e85355/decorator-5.3.1.tar.gz", hash = "sha256:4cbcdd55a6efadb9dbea26b858f4fb3264567b52d69ca0d25b721b553f60ea82", size = 58084, upload-time = "2026-05-18T06:03:28.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/7f/798705f5296a58ca505d600456748d1be48078eac8a7050d8a98bc9edb89/decorator-5.3.1-py3-none-any.whl", hash = "sha256:f47fe6fdbd2edd623ecfe36875d37aba411624e2670dd395dddae1358689bb3c", size = 10365, upload-time = "2026-05-18T06:03:26.517Z" }, +] + +[[package]] +name = "fonttools" +version = "4.64.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/41/0f072a712dc74496e03710e462a18a4cfd8a258ad055a4e22d28b43a7abd/fonttools-4.64.0.tar.gz", hash = "sha256:ecb2e59a7bc692fee64dda6010deb66222335693b30046f15cccf81233aa715f", size = 3664266, upload-time = "2026-08-31T15:44:33.685Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/d2/51a541f2f807845933223c3d86d1157ca0fac1351626757999f7b2908e64/fonttools-4.64.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:50e52b6f479ddb1fe32423c2ec860811f36584cf6eabf279fb9a4f98b859a8b4", size = 3093843, upload-time = "2026-08-31T15:42:09.581Z" }, + { url = "https://files.pythonhosted.org/packages/5a/25/71fd00d06bacbaf0df604bdea4d6574f0ce091692bde028847a445f9f726/fonttools-4.64.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:730eed859508cb7b0775ebe6bb39f18901f168eb989d8ee23a4fe082700e1e3f", size = 2586331, upload-time = "2026-08-31T15:42:11.79Z" }, + { url = "https://files.pythonhosted.org/packages/ab/64/9f2f0ee36669073ae21a28bb2e6cb91c5aa963fae39b3636afc5e29dc959/fonttools-4.64.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6946c033a144086d5b98c976b72f476b70c93fbbedf914eee0e886f073a4e9fa", size = 5309251, upload-time = "2026-08-31T15:42:13.773Z" }, + { url = "https://files.pythonhosted.org/packages/d6/93/515ea8d3f637450c4d354acbabea576ec977ab03684a775116a9e6e30fc3/fonttools-4.64.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d16102cbcd4615b09c64e6022733faccc93200785f1ab0d4493afb8b0261edde", size = 5251598, upload-time = "2026-08-31T15:42:15.948Z" }, + { url = "https://files.pythonhosted.org/packages/b6/82/801c3d780439d486cf6fc92b2a212d9f902cd7d5af7e8d8d67fa0a76243e/fonttools-4.64.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a3238a693e806a3158375c6403b8f6f71d86eb9c149b60c97f26dfd560c98ac8", size = 5273084, upload-time = "2026-08-31T15:42:18.176Z" }, + { url = "https://files.pythonhosted.org/packages/52/da/cba7640c3d064f8a72de17adc38c899ac35db2708e7bd9efb52993924f08/fonttools-4.64.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:70fd99e5a09fb77f14b29d70879a4fce9529b2d2948b14c96708e0a61e001b98", size = 5409243, upload-time = "2026-08-31T15:42:20.491Z" }, + { url = "https://files.pythonhosted.org/packages/19/95/0890e3438f94f861c4c37f89fe4a08cbe79bd2b256c78f4dce016c51d8cc/fonttools-4.64.0-cp310-cp310-win32.whl", hash = "sha256:507c553cdb5abe2e951b5368423849fe29911a828c2135319c3e500e3bf25b32", size = 1575835, upload-time = "2026-08-31T15:42:22.497Z" }, + { url = "https://files.pythonhosted.org/packages/35/29/bfa508a7211ce57d5303f2dd30f3ebb8601c144576a2cbfd9040b87019a8/fonttools-4.64.0-cp310-cp310-win_amd64.whl", hash = "sha256:e7b34209eef39462563c05ea9dcf51c272a2ded56f5753da925e66bca3baa484", size = 1632090, upload-time = "2026-08-31T15:42:24.658Z" }, + { url = "https://files.pythonhosted.org/packages/07/f7/a222d1e20d460a09d08fe0b612a6f373235168c6c3228ff6a913cc8ff9ea/fonttools-4.64.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:dac25768be4c03a990c359f408cb7e8958ed0e93061e495b3642ce7909761205", size = 3087538, upload-time = "2026-08-31T15:42:26.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/fd/571331ac2b9ea43403ba43f27f5b45427a13b5c559d4379c99f3f9437b59/fonttools-4.64.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d652592c71683941b768306fa1c7c6ce1bb9b072505043feafe86305d71030b7", size = 2582847, upload-time = "2026-08-31T15:42:28.664Z" }, + { url = "https://files.pythonhosted.org/packages/dd/bc/583ad5e4d6fbc600b1d9eb3c2e8b4eac5ca1c204fa29e4ef0ff344d8aa60/fonttools-4.64.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:801fd04899d72eab34f02ab78d0451525621b3bd589da9d2d480dfffe951b643", size = 5493826, upload-time = "2026-08-31T15:42:30.991Z" }, + { url = "https://files.pythonhosted.org/packages/f8/3d/4edf079bdb01791abe87753b7c8fdca4e8ec709276e7b030a1aa84a88a51/fonttools-4.64.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ff7aff4637fbf71394df139c63ccfe08a47aa4252d2f91224ddb3335c716c925", size = 5455609, upload-time = "2026-08-31T15:42:33.149Z" }, + { url = "https://files.pythonhosted.org/packages/7e/b6/e36ea109c8cfdb0f174c09d8464b4ddf6b446f97313118d69b6a28cd7189/fonttools-4.64.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f521d79d6acda4923b264805541696f452079db0952a5bb96f9ff742f50629ec", size = 5460840, upload-time = "2026-08-31T15:42:35.423Z" }, + { url = "https://files.pythonhosted.org/packages/6c/bf/0d8bb1fbb96c621c1da5f93e288bec8424802df10265e790dddc66626b1f/fonttools-4.64.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a0afa8bac675445dc0e2ba2891ecbedd9be89cb437afa94c823e0290cc2c4bc5", size = 5591341, upload-time = "2026-08-31T15:42:37.874Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e1/6f8a1a01e5ed4408fdffb934c77ce65d978a2df6fd6eaaec5e6b864b10f5/fonttools-4.64.0-cp311-cp311-win32.whl", hash = "sha256:c3c1fb656063a2f762db5378ea8d38ad5f7836b4f3fb8c4652270ded43df2935", size = 2439647, upload-time = "2026-08-31T15:42:40.196Z" }, + { url = "https://files.pythonhosted.org/packages/21/41/b575f14a653911f33f17ef62bbeaa818c94bc7e694579cc9ddec6935d5b2/fonttools-4.64.0-cp311-cp311-win_amd64.whl", hash = "sha256:e63b63b8b5fdb8e29318dff2b15c5f852be46e972775b466f75b848f6eed4502", size = 2497436, upload-time = "2026-08-31T15:42:42.36Z" }, + { url = "https://files.pythonhosted.org/packages/82/23/4ea251977fef70ed14193785e1b2949355f1f5927dc0ef1dada675c0bbb2/fonttools-4.64.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9ecb2b206b5b2386f6968721a0770226b66bdd54adc4279bfff3ddf62873eed8", size = 3095501, upload-time = "2026-08-31T15:42:44.299Z" }, + { url = "https://files.pythonhosted.org/packages/c4/32/943e9034f49797e1a25dbbd60c8047ce0c38c3585c5d1b2db38ce64059c5/fonttools-4.64.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8c631303bb1fd7be3067c47536a30ff1fcb4846d6008c112bc52a03f7cd6965", size = 2583091, upload-time = "2026-08-31T15:42:46.57Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4b/332cb3105d5d550cb5728e669151601c4f7698cbedafd134154eb806ef83/fonttools-4.64.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2763e452b025ee8e990f0462e76052de9bb094ebc21d296f62c6dfe958886b4", size = 5423449, upload-time = "2026-08-31T15:42:48.615Z" }, + { url = "https://files.pythonhosted.org/packages/dc/8f/e5f2906ea833d362916c64a2f6b1922a07b19442744fd3cd89563f429bff/fonttools-4.64.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:66a83f93579fb3493e458c4449d1d566a7b2a1c7b19915cd0fa3c9b8b5a8540b", size = 5400981, upload-time = "2026-08-31T15:42:50.862Z" }, + { url = "https://files.pythonhosted.org/packages/fe/46/1b325ebb20aef8bb05f803052ea65931d73638ae3e0d02a6658e3d14e0f8/fonttools-4.64.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cf67f96dc0bfe9607f5f2b734cedfbe2f6f995231adee4ccefa12872044d452d", size = 5359588, upload-time = "2026-08-31T15:42:53.341Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d3/ac56fa880e01339aa6ad0bcb457cc30bb4d6c5ca79922e1bd650e4a3a396/fonttools-4.64.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6786bed88581e19bc4f28ea7a64ad531e8f54acf50327fddca942688824a60bd", size = 5520698, upload-time = "2026-08-31T15:42:55.919Z" }, + { url = "https://files.pythonhosted.org/packages/d7/40/1251fef04c308836a3a7db523703a7ca8ab010dc1e3b9a9bf08e0861c7a8/fonttools-4.64.0-cp312-cp312-win32.whl", hash = "sha256:da4c9bdeaf6b06c12d13d0addfc8ef15aa9695d26574a6dc10751258bef72f30", size = 2430689, upload-time = "2026-08-31T15:42:58.48Z" }, + { url = "https://files.pythonhosted.org/packages/6f/1c/ce0e89183c0235ff6cfbf0603d593023a5afa5333efb35ff569cc0be9ce2/fonttools-4.64.0-cp312-cp312-win_amd64.whl", hash = "sha256:06b6409b868494556a831ae33b2d9a090476c37516b38d70f45a9720b460d423", size = 2482071, upload-time = "2026-08-31T15:43:00.598Z" }, + { url = "https://files.pythonhosted.org/packages/44/7b/49b0054a79b9ed918c018e70e09b56eb5678ee8b44e59e126c79de4d8d73/fonttools-4.64.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9443eefff58aad558608f352092e1be6d278980e8c3b4e8621fcbfda97818500", size = 3092750, upload-time = "2026-08-31T15:43:02.705Z" }, + { url = "https://files.pythonhosted.org/packages/83/e2/08e73bd2f6e6248f071d3e9debe4c2b4cecda3a7dc057c56b44e255421c3/fonttools-4.64.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:09657817b75575822bcd6098ef0ebf0386f34430839ee53109e70fd40a7f6539", size = 2583001, upload-time = "2026-08-31T15:43:04.923Z" }, + { url = "https://files.pythonhosted.org/packages/2f/58/6dc0ff0963fc1e12f53bac59175617ec5927342ee7a4ffb871b44d56d81b/fonttools-4.64.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d7995b906666037d7114c20a5566a372902747452af7d5bd4cd6bca8f1a2550", size = 5392765, upload-time = "2026-08-31T15:43:07.096Z" }, + { url = "https://files.pythonhosted.org/packages/7a/dd/4d3911049680da7b3aecf97cb5094e3a05554f5d94829358fc7664640456/fonttools-4.64.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2c42237b7e8c6813643e57d3efed3be094d4c06339dc2166b626e2cc5c12ee93", size = 5373969, upload-time = "2026-08-31T15:43:09.617Z" }, + { url = "https://files.pythonhosted.org/packages/48/0a/a2cf94121fd3ca9166bdb4863f71d6db32a5606b223c81e2ba99832a0612/fonttools-4.64.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:498f02ea92c9ca18c0f9c581ea93184a9d56c25b0af14189b0767adaf34235d8", size = 5333854, upload-time = "2026-08-31T15:43:11.985Z" }, + { url = "https://files.pythonhosted.org/packages/44/e2/632f4a8b94e6d7eea41e6d82f0ed337f64f5fb96f9910e2ac6b1689910e2/fonttools-4.64.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8252f20108e557532f91d7d6dd9af87c16ed6fa930f65516aa480fa2cfed3363", size = 5492828, upload-time = "2026-08-31T15:43:14.288Z" }, + { url = "https://files.pythonhosted.org/packages/ff/25/7fee1978fdedc1a2d978b76dddbea88785416f5774e7222327b867712d71/fonttools-4.64.0-cp313-cp313-win32.whl", hash = "sha256:45e3ecc3888f1637094fd75cd8fc727f3a4b06d1ddf89181126c071e244fd2a5", size = 2429011, upload-time = "2026-08-31T15:43:16.463Z" }, + { url = "https://files.pythonhosted.org/packages/dd/ed/bb1c217c9e1fbfa59f42b266af57df937a703882a73e59a309773752228f/fonttools-4.64.0-cp313-cp313-win_amd64.whl", hash = "sha256:e4812f71c39d77ec5041348dafa400532adf7bf8f1fffa9aa6495fce5876d7b8", size = 2480199, upload-time = "2026-08-31T15:43:18.774Z" }, + { url = "https://files.pythonhosted.org/packages/a0/0a/59b9074b8ab165cc28d3e08bcf7a8eff8e1dc5932a5a41c87b15952fa3af/fonttools-4.64.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:6f1ce9ef9a1b13098efdc2e43a2ed96d9851bbde7b31c652a87552c4efe9b422", size = 3096790, upload-time = "2026-08-31T15:43:21.125Z" }, + { url = "https://files.pythonhosted.org/packages/17/39/af1077a36feefe79f36d67d75aee637a96673c0a74eeb2c6f24266f5a20b/fonttools-4.64.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:83cc48d1411d2ff388dab99973dca81172cc9ceae9c9799da9548d494cfb38cb", size = 2584288, upload-time = "2026-08-31T15:43:23.148Z" }, + { url = "https://files.pythonhosted.org/packages/00/c4/1ea58af0eb78264d28e5871bba0810bf0943cd93d641fdeae92553ad3410/fonttools-4.64.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e412767d1c9765cf1b82f7b00f1686c6ca5809ebb77af363b3f9f2325a465c01", size = 5378061, upload-time = "2026-08-31T15:43:25.193Z" }, + { url = "https://files.pythonhosted.org/packages/93/99/5c7e36d770407b66f7fc8378d2bb47f1530d083d8b9247ff011ec9b4dd70/fonttools-4.64.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b4a7af455ffed980925bc0ebf5b8d6239e6c3e797d9d755b6db192fb3080d614", size = 5319458, upload-time = "2026-08-31T15:43:27.696Z" }, + { url = "https://files.pythonhosted.org/packages/a8/09/89e8d600e92723309d4ddd3944c4ca565197a5df0236ae4115ccbe797375/fonttools-4.64.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:398b14f89ca950b288bd290875f07e4e10685644fa4ac668546fb107b1ada4d4", size = 5316656, upload-time = "2026-08-31T15:43:29.842Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b3/56719ab37e1592ceac89724574146622bedec036a207be80f3c7b1c14cbe/fonttools-4.64.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:dc96150f99e05a317cb1f042b92c4cf8bc93cdb1f9f85717322e202ecdf2e505", size = 5448380, upload-time = "2026-08-31T15:43:32.311Z" }, + { url = "https://files.pythonhosted.org/packages/3b/b0/15951b7e3006073f3260da4f8983732d18903905cfe26daa5138a0bbf682/fonttools-4.64.0-cp314-cp314-win32.whl", hash = "sha256:1c3661324f3f0fa4539a32288a3e0711a5f3ccf020036e760bb558ae9811a16f", size = 2432778, upload-time = "2026-08-31T15:43:34.421Z" }, + { url = "https://files.pythonhosted.org/packages/8e/f3/0c97402b29411f4f6d31b32f2af5b4bcd5babe5a93c64558a8fd3654f140/fonttools-4.64.0-cp314-cp314-win_amd64.whl", hash = "sha256:043f6c572bf236f2a76e762c25f841daea11e8fc03e78088d7be66e0c5b4e4c0", size = 2485394, upload-time = "2026-08-31T15:43:36.506Z" }, + { url = "https://files.pythonhosted.org/packages/00/da/82192c7bee5314f04129a068dddae11082a63295d4e3b5ce08657de96608/fonttools-4.64.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4691a122b8c1d0d82d6e7510ce59d5c42146518240274b53e912e255573924f7", size = 3170118, upload-time = "2026-08-31T15:43:38.579Z" }, + { url = "https://files.pythonhosted.org/packages/42/2d/0b4f608d754f625fecc7d94b3b26af6f65f6ab4527b386bb0c6141cdd9ea/fonttools-4.64.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:3200180abc69639483cf54a17cca2e13c31ede5f665979ea0a9c829d093f372f", size = 2617333, upload-time = "2026-08-31T15:43:40.934Z" }, + { url = "https://files.pythonhosted.org/packages/5e/df/9f7448c38dea05458acfee81c443b14ef97d6d66c636af053d41cf8a32dd/fonttools-4.64.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53eee22af5b5a305c1ee2652955ed46b148e881456fcec1e7f0eb27f642f6bb4", size = 5542345, upload-time = "2026-08-31T15:43:43.114Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e8/cbf4a81e8be322bc4bee4ba31cd33a7b6336ef8030fa13b53b3e9c09fbf9/fonttools-4.64.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:08f172961e11f4eb4f80f2f20049e09b0ea8e044fa6d456fed8346eb8588f360", size = 5349981, upload-time = "2026-08-31T15:43:45.363Z" }, + { url = "https://files.pythonhosted.org/packages/d8/dc/106fd9e93e962dbb60482d30c0913af3d108f5762429dfe93265a850bdc5/fonttools-4.64.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6eae4376adb104c2acfa76fd9ea0cb12b572ca1d70eceac709871f638ff76e93", size = 5409249, upload-time = "2026-08-31T15:43:47.817Z" }, + { url = "https://files.pythonhosted.org/packages/b0/2d/c7be990abf74c9c5c5367ae3dc65953ffce49fe00f62abd129cf725a2397/fonttools-4.64.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2730946ca8f12c356bd98eb9b2b095c8e761ed05bed5afb0d5b380cebe4f6370", size = 5443781, upload-time = "2026-08-31T15:43:49.909Z" }, + { url = "https://files.pythonhosted.org/packages/9b/cc/bffec3dccb9e3f4f6097a2baedaef5a8dec2cb816522ee163e4bb7f54b44/fonttools-4.64.0-cp314-cp314t-win32.whl", hash = "sha256:d30c966bea2deffa19c738c81776f7182da5ccabd97e666bae4f3d6ba87341d9", size = 2466542, upload-time = "2026-08-31T15:43:52.088Z" }, + { url = "https://files.pythonhosted.org/packages/96/49/e31f97dfc94e0648999f04bfccb37a97062d978f87df60654f496942c89f/fonttools-4.64.0-cp314-cp314t-win_amd64.whl", hash = "sha256:917fd520bb60809d83c14d43cfe48d5ad2516abaf2c073d65a431800dade2d29", size = 2516703, upload-time = "2026-08-31T15:43:53.954Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ec/e39b9e56db7dcc859983caa24cf2a36209dd71f6cc8ddbbf00d7342d7988/fonttools-4.64.0-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:8dd18fdff0ac9759b8d67a714730abee07b2312e3656c20ba5affb0107094762", size = 3091150, upload-time = "2026-08-31T15:43:56.309Z" }, + { url = "https://files.pythonhosted.org/packages/a8/e1/3e6a409d6f99efb66c2f6d0a71986432ff9cc9aca0df9ff4a8bed1d850f6/fonttools-4.64.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:5af87d1a6d247d7467ee082ae977a5443b2c45f8cd4d59375b6daa38d523c2de", size = 2582833, upload-time = "2026-08-31T15:43:58.348Z" }, + { url = "https://files.pythonhosted.org/packages/91/7e/e8aec0e6eaf93267450535c1be9c9ae63e1d9c49659c2ffdb52c19982114/fonttools-4.64.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:769fb64412ca237547ca73f111a64252d9e32c9d938bed51ed537bc9146a8f54", size = 5375327, upload-time = "2026-08-31T15:44:00.417Z" }, + { url = "https://files.pythonhosted.org/packages/61/d1/fa9ce5c1c3c0ef858a2cbe5632e9b6b137deca010b28d5d9a677e4aa89a7/fonttools-4.64.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e662f874ab2c7da9861584db44a13573e0936df087215f63013138f6e5eba083", size = 5338647, upload-time = "2026-08-31T15:44:02.62Z" }, + { url = "https://files.pythonhosted.org/packages/0d/13/e4d4fa3c166f7f5a8f9b6403ad9dcb3887cd05f8b9d2638bd2b43d2889ef/fonttools-4.64.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:2524a26f8fdb9051b0d778d052f5d238285ca9f91a7dc004514c7d6cf38d35f4", size = 5313557, upload-time = "2026-08-31T15:44:05.037Z" }, + { url = "https://files.pythonhosted.org/packages/91/c8/5a352d69608ea7606f677080ede971090fc7340b9f143c0b4fa00cfbf63e/fonttools-4.64.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:1e4e84b47839d35be24dbf476845a34f2ccf99707b66df125c1c414d3e86d25d", size = 5462363, upload-time = "2026-08-31T15:44:07.13Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f1/b15b845f66b559a24a9c40859391bbd3048af3ac0fd04e45b57dca11c4c7/fonttools-4.64.0-cp315-cp315-win32.whl", hash = "sha256:be084d19a3ac0c8b2aba696680642d703118d3b1f18cf83f5b7dbaf0ffc62ab6", size = 2431682, upload-time = "2026-08-31T15:44:09.293Z" }, + { url = "https://files.pythonhosted.org/packages/c6/b2/909beff0d2e2e1adac896e36cff7f9d76fd154bcd57e303d8f09aee8f72b/fonttools-4.64.0-cp315-cp315-win_amd64.whl", hash = "sha256:de8acaa5f4160f537a3cf41b031171d51004b9f4aebfa6c194f18dffa9533d03", size = 2484393, upload-time = "2026-08-31T15:44:11.117Z" }, + { url = "https://files.pythonhosted.org/packages/40/47/06a51becf651cc071daa88e18ecb9f45ace9e3a570d8191ed8b3cae353fa/fonttools-4.64.0-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:5b90ad6637237b636d15c9ae8b7c4a7a1c194f33def378677e468c13fd4542f8", size = 3162235, upload-time = "2026-08-31T15:44:13.182Z" }, + { url = "https://files.pythonhosted.org/packages/dd/96/8b3faf58fd7ec3bb11943126658ad15b3f385068b23248deffccf2327ac1/fonttools-4.64.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:fa75c7970bc6bca340cc6e20f20f069201bfcb50094c31a536fd99724d1d01ca", size = 2613507, upload-time = "2026-08-31T15:44:15.123Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3a/223d1437e72d79f1549be7bafb3cc08f397e1d9c354a241a8ec8573b7d33/fonttools-4.64.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:236e59bc7e2a63557a4d7b013f9cb9e28d9aebc45bc09f85e545e6bf091db626", size = 5518005, upload-time = "2026-08-31T15:44:17.359Z" }, + { url = "https://files.pythonhosted.org/packages/9d/9f/6f1a4b40ae533b9e907b7e627e397be632d0d9fd6f6be7756cf458870279/fonttools-4.64.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a515f664cad988f2295056833a59f62220bc3e46afdaffe389a29060f6712355", size = 5341654, upload-time = "2026-08-31T15:44:19.869Z" }, + { url = "https://files.pythonhosted.org/packages/52/f6/9ca30ba98730a22b527bcc1e02034f0b13b3be2ede764bb0fe8b8e0408d6/fonttools-4.64.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:5bfdaada437e7730c17d366bd7bb8c4a16639963ddbfc1b2f302a68a17a290e7", size = 5385994, upload-time = "2026-08-31T15:44:21.979Z" }, + { url = "https://files.pythonhosted.org/packages/09/27/baf1b61bff983bfec72cbb7b32162c4b68d76dc824aaaa48a1c26fa10b6b/fonttools-4.64.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:c60be0aed97a32c6ba8cee21f0d0477136e495451bd97910f589ac892db120d4", size = 5432407, upload-time = "2026-08-31T15:44:24.469Z" }, + { url = "https://files.pythonhosted.org/packages/21/ff/2eab37d43f2e2ccc0993959d8f2605d9a68470082c592684ac98611ee70a/fonttools-4.64.0-cp315-cp315t-win32.whl", hash = "sha256:f8669ce37851b597d3435b91fefa51139e58d506ca449ca0e5bb68c63b8b6d2b", size = 2463614, upload-time = "2026-08-31T15:44:27.198Z" }, + { url = "https://files.pythonhosted.org/packages/dc/8d/8bdff3ea656592197c8ef5062774221885468ffa01d8b0dc782dae23a83a/fonttools-4.64.0-cp315-cp315t-win_amd64.whl", hash = "sha256:89356c0793b474af7e49ec90d39fb2363e2341516a90460e38231df5ebe8acd5", size = 2512422, upload-time = "2026-08-31T15:44:29.478Z" }, + { url = "https://files.pythonhosted.org/packages/82/f8/7188153c4b265c899cd035de6a062677d51f67118a4ba640902bd9683e90/fonttools-4.64.0-py3-none-any.whl", hash = "sha256:4a05783ff54ce4c7a28f18e5772efdf63c219374bd9ffc55452182e1cef8be60", size = 1195327, upload-time = "2026-08-31T15:44:31.741Z" }, +] + +[[package]] +name = "glcontext" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/80/8238a0e6e972292061176141c1028b5e670aa8c94cf4c2f819bd730d314e/glcontext-3.0.0.tar.gz", hash = "sha256:57168edcd38df2fc0d70c318edf6f7e59091fba1cd3dadb289d0aa50449211ef", size = 16422, upload-time = "2024-08-10T20:01:20.004Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/44/f605689047cca15d5e5d31c0a4b9ca87dd1d52bfabf3c3a05d70a4636c08/glcontext-3.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b154c25a57e16dbb073478d0cbe2c0090649d135c4c9f87e753b6181b97ec848", size = 9329, upload-time = "2024-08-10T19:59:49.485Z" }, + { url = "https://files.pythonhosted.org/packages/10/fd/461c9e37ad50514dac82fda04a7a0aabb1a5e6b0f76476538caa4e74a25a/glcontext-3.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fa5a14778d13ecf4a0dd60a7825427bf6ac0383eacb3b8b1a9c23e4976aa0c8c", size = 9735, upload-time = "2024-08-10T19:59:51.329Z" }, + { url = "https://files.pythonhosted.org/packages/db/86/c879bf61d4f52d8b0cd702db5f7180b5eae8804d29a798de8da0568a64b2/glcontext-3.0.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c229290a3a33004a59799b94a50bc5e6f8addd1b5bc5039ef9e78f86d888b31", size = 50476, upload-time = "2024-08-10T19:59:52.854Z" }, + { url = "https://files.pythonhosted.org/packages/d6/c4/8b45b1c910f4a9b4df1ee3449b4c1aaa798fc25d86390e8eb2a18bc59669/glcontext-3.0.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1445a03d8795113034e1f9ffa662f795df65ae69ee21b26ed3b1d66100ba3f8c", size = 51480, upload-time = "2024-08-10T19:59:54.149Z" }, + { url = "https://files.pythonhosted.org/packages/91/00/159f7df65554f30e0cc6f7fb78869e95717ed5319729f386dcc81ae9ee3d/glcontext-3.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:09247011c09c37b8d30eca9aa24659288de2febaeaa6a817b33b1498b5ef164c", size = 44902, upload-time = "2024-08-10T19:59:55.268Z" }, + { url = "https://files.pythonhosted.org/packages/91/85/21d2b619576690d48e4967dce37fabe616aeee06b7c6a6c6ceb48d99d3ef/glcontext-3.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c8c1223f1cbcfc0b88428e1717baca829ee863ed5d88e9b5574c7ed6598249cd", size = 47035, upload-time = "2024-08-10T20:00:03.972Z" }, + { url = "https://files.pythonhosted.org/packages/5e/99/6c200c6d16b55d19dfd13bfed1ed5794e17e49ea8ccf9e4fab52e2d0e2d0/glcontext-3.0.0-cp310-cp310-win32.whl", hash = "sha256:c13dedb3636328b133c4d53c047ce69040ae784095e8f239432ad74d6f921712", size = 12218, upload-time = "2024-08-10T20:00:06.285Z" }, + { url = "https://files.pythonhosted.org/packages/93/36/8a8f25366ab80dc4e6c9ded7b21f11152bba65fd6b8c0f28641ef3e133f5/glcontext-3.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:4817f4cd52c7fe5410c92ca12b6712435548918719373882ade76f8f75d80abd", size = 12973, upload-time = "2024-08-10T20:00:07.733Z" }, + { url = "https://files.pythonhosted.org/packages/01/36/3d0d09f7352b179a7ecc5fc3322beb85c8995c66db780acf791853e49043/glcontext-3.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3a9e56fa3597cc709cfd0fdf2ae682cda36510a13faac2b3142f401e823b64f4", size = 9328, upload-time = "2024-08-10T20:00:09.43Z" }, + { url = "https://files.pythonhosted.org/packages/44/9d/0c8fd9c660db000071ebace14a40bd381a41775c10faa262fedfae8227e3/glcontext-3.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a0484308af75e04b0e56066dc2324a8fb9f1443b76ddb98833439982322b2a39", size = 9738, upload-time = "2024-08-10T20:00:10.831Z" }, + { url = "https://files.pythonhosted.org/packages/78/cf/7bcadb995830cdd6a1a31f0527a52b2c441a499feabed9749106f7e41e67/glcontext-3.0.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:983231394396aa2a1e2b96df49404cc8f8aa729d462ed40e605a74b079c46342", size = 50663, upload-time = "2024-08-10T20:00:12.301Z" }, + { url = "https://files.pythonhosted.org/packages/43/fb/646c2773cb097b914afe1f06c95e65deb8a544d770389bf29c76a8f3a8fd/glcontext-3.0.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fa413f4420abff2bbb5aa5770a3e1deffcdc13e0ef2f459b145fa79c36909e7", size = 51680, upload-time = "2024-08-10T20:00:13.646Z" }, + { url = "https://files.pythonhosted.org/packages/89/b7/04aac6c50071b858cfd02a9bccafb42a21f567992fa448c8ad8aa62939b9/glcontext-3.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7d0ac35ac07fc91eccea093beb9d1c1a4eae250bc33836047deff01a3b5f4757", size = 45063, upload-time = "2024-08-10T20:00:15.137Z" }, + { url = "https://files.pythonhosted.org/packages/3c/6e/6e398492b55f3c453cc6a2ecc2886a01b2a465623aa0c476d3e115be85c4/glcontext-3.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7145d17a70adc5784ca59ebbe19a56435ba21816070b8b433f43aa2dfb8be71a", size = 47281, upload-time = "2024-08-10T20:00:16.679Z" }, + { url = "https://files.pythonhosted.org/packages/b5/fd/4f59118e5067a3c88217862d8672463aff29f29c1ea9f47971f8ca67e83c/glcontext-3.0.0-cp311-cp311-win32.whl", hash = "sha256:b31808ca2517fedcac8ca5b296ff46c8af012911eaa2080889a1f244d329ef9a", size = 12222, upload-time = "2024-08-10T20:00:18.108Z" }, + { url = "https://files.pythonhosted.org/packages/20/1b/f402574ae4644e7fb389b9534de9d1be575b8a4da901a9023d1cec72c4aa/glcontext-3.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:ef4b4ec35e2b720f4cd250bb92cf6417add445490bf780345596da5a796a0e6f", size = 12975, upload-time = "2024-08-10T20:00:19.236Z" }, + { url = "https://files.pythonhosted.org/packages/6e/61/d8f77d44bbf477b235ecbff8d421a5511d4b4f6dc676dacd84d012348516/glcontext-3.0.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:848f870a2bc72a29de7ab6756b9e8f2e6ce052e17873ebc6b3f25129b6e0d58a", size = 9360, upload-time = "2024-08-10T20:00:20.42Z" }, + { url = "https://files.pythonhosted.org/packages/de/46/680a97d974cfe7af798542918b65bd8e65a5f8f7647edfd9fdb91a95df6c/glcontext-3.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4b3b12a66f57379566dd4d36899ac265abdbe040f3fc3293f50cd6678a1dcc9b", size = 9733, upload-time = "2024-08-10T20:00:21.624Z" }, + { url = "https://files.pythonhosted.org/packages/74/2c/be188c4eb63b4d0cc74c644a1519b5e3a37488da3cbda724570cd5fed8d3/glcontext-3.0.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:449eaefd89c0519900715b8363ead59ac4aa32457722ca521ce01297441edb34", size = 50409, upload-time = "2024-08-10T20:00:22.827Z" }, + { url = "https://files.pythonhosted.org/packages/32/ba/9ccb80650e5bd61e739f16f33aec3bb290a80f314631be8f7dc0a2b22b5f/glcontext-3.0.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:04921720740438ceea8fb8a38b5665963520c7c8f27bef03df8aeb3ea3cfbfb6", size = 51440, upload-time = "2024-08-10T20:00:23.987Z" }, + { url = "https://files.pythonhosted.org/packages/c0/99/f6c9a0e614809ba5b83bd8403c475b788bd574d694bd5bc6b6ae2e2cefdf/glcontext-3.0.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:25538bdb106f673638d70e8a16a0c037a92a24c4cf40a05f0d3fa14b483d6194", size = 44820, upload-time = "2024-08-10T20:00:25.436Z" }, + { url = "https://files.pythonhosted.org/packages/a2/29/fdbb94e4c9374390639b741774384f7413efcd7634cc9c4baacc2cf00a1f/glcontext-3.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d11f7701b900a5a34c994e1d91c547be1cc469b73f881471460fd905f69f9e4c", size = 47055, upload-time = "2024-08-10T20:00:26.723Z" }, + { url = "https://files.pythonhosted.org/packages/5d/fe/25b5348fe5e856697dacad34fc07e80f48eecfb38bd09806679fe0e62769/glcontext-3.0.0-cp312-cp312-win32.whl", hash = "sha256:5d2b567eaf34adb016aadce81fd2f1d4c8e4a39e3d6f2a395ce528e2a350dd3f", size = 12219, upload-time = "2024-08-10T20:00:27.76Z" }, + { url = "https://files.pythonhosted.org/packages/17/d3/6619693ddad97011ca1c9aaeb82216ab2bfd54757be752b12f4e9a2fc489/glcontext-3.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:e80bb37ba727bd20c192f2754aea40c437a7665005c1001c10752f91913964e9", size = 12971, upload-time = "2024-08-10T20:00:29.575Z" }, + { url = "https://files.pythonhosted.org/packages/12/be/0ef6a6710164fde818040238b041b02a082a2b9d210f18632ab2354d863e/glcontext-3.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5bd37089570d3cdb01c6c0b315c49ce8a4dcdab2c431f5ba9f37a8b633cebfdf", size = 9366, upload-time = "2024-08-10T20:00:30.531Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d7/c3220898d72fbf938660ba5789c19cf245d21b45802a5d86cbcc67d66413/glcontext-3.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:857fd83e60f15580afd369dfb651a10d84a70ec35995622d253551bfb3ff9477", size = 9735, upload-time = "2024-08-10T20:00:31.708Z" }, + { url = "https://files.pythonhosted.org/packages/74/c1/2d57062d2f2f6e55c58b12bdeab2a39b209f959df32039d099ecdfe96bf7/glcontext-3.0.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:93fda9b378ce6d91f366e83e71ebdafdd167280a9834d1d6341ce6457c4e42ed", size = 50166, upload-time = "2024-08-10T20:00:32.627Z" }, + { url = "https://files.pythonhosted.org/packages/03/4d/7fee00c76d678b06529e939ab6f3e3190af30208fe2e984526899026a437/glcontext-3.0.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89ad50d34aa62f03f6aaf6ae39fc27afd1b0eaefb0281aac51f686dc5672d473", size = 51186, upload-time = "2024-08-10T20:00:33.718Z" }, + { url = "https://files.pythonhosted.org/packages/f0/58/dc9a56192b889587e51ea511804bad5dec816de81ce16831db9fe19d5c40/glcontext-3.0.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2634d5e9647a6d7b0c5a5c0c57e91ac98aa79759bffb42459af4374b049fab01", size = 44658, upload-time = "2024-08-10T20:00:35.232Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ed/acb12e67589deaa96ad29d6994c2b9383afd18700f4f2a42ff342628aac5/glcontext-3.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0140c5df37cb48271527355062d35589dc3e1e7e73b51adf9962ed5048115f69", size = 46882, upload-time = "2024-08-10T20:00:36.33Z" }, + { url = "https://files.pythonhosted.org/packages/e0/6d/033ba23466d596c945f5f31f5c4e50cba3bc6664fdee58f6ceab54b76f4b/glcontext-3.0.0-cp313-cp313-win32.whl", hash = "sha256:6678e0552b516fa8fe62f500ef2b953bec991e82a003be2a9840d16556d03d2e", size = 12220, upload-time = "2024-08-10T20:00:37.874Z" }, + { url = "https://files.pythonhosted.org/packages/53/b4/f0e0860526b8661ec6ae2b25a15b61100e551f57f488613c564752173a56/glcontext-3.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:18aa4b1df50e8c8ea39bd0f775f39bcc987521f92c4ed019ec7d70078471354d", size = 12971, upload-time = "2024-08-10T20:00:39.233Z" }, + { url = "https://files.pythonhosted.org/packages/ee/84/8f6ca5b8005ec58adbf51e2e6c3b5a20f234ea8f7a2e1df52835c9b23274/glcontext-3.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:f4e285e5d40e7a9bafeb0651c3d8f1a7c822525dec7091e97109ba87a70dbd1e", size = 9272, upload-time = "2024-08-10T20:01:00.133Z" }, + { url = "https://files.pythonhosted.org/packages/39/a2/a0d1e7492f14727682820120062717a118f0196cda8cf6c1a6e6c4ab6d12/glcontext-3.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:96d1bbe62c5bc5315ca2f84a2abae6fa7b7d645dd368415a0cd1ee5ba6f3f8f7", size = 9581, upload-time = "2024-08-10T20:01:01.098Z" }, + { url = "https://files.pythonhosted.org/packages/07/ba/2f093542218c9c38b293df0f501509fdfeb5dad975135a5467431aa9a774/glcontext-3.0.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3a190b1cdb39110c7b56c33857dbff493a364633bfd0ff402a1ce205956c94ca", size = 17842, upload-time = "2024-08-10T20:01:02.308Z" }, + { url = "https://files.pythonhosted.org/packages/01/e5/5209be64858f9d7f9861f03ed5662491642d8d31b51a6a8495f084869386/glcontext-3.0.0-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d467cce2dac8c3928847e90310eb6bdfdfa69f8df39b76a74734046faa15e688", size = 17226, upload-time = "2024-08-10T20:01:03.362Z" }, + { url = "https://files.pythonhosted.org/packages/06/cb/0995eef99e56394b93bea3aa20bc269ae09125b66c0b7c1e3338f44c93cb/glcontext-3.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2b0c5240125d75498a8f14596484233e4effe98a6a035f566872dd2fdf472ceb", size = 12988, upload-time = "2024-08-10T20:01:04.728Z" }, +] + +[[package]] +name = "idna" +version = "3.19" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/f7/abb373e5757eaec4b922b92f97ec8d6d7e057cf06778247604fbc4e7c3f3/idna-3.19.tar.gz", hash = "sha256:5e0811a4383b21dc5838069f801c4fb62113b7447663d2530d2bd6e77b49bf15", size = 215237, upload-time = "2026-08-18T05:14:24.27Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/b0/0e52c878c53f245edd3a11020f20979b3f490f245af532c7cae3027754b5/idna-3.19-py3-none-any.whl", hash = "sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4", size = 68550, upload-time = "2026-08-18T05:14:22.343Z" }, +] + +[[package]] +name = "isosurfaces" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "numpy", version = "2.5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/cf/bd7e70bb7b8dfd77afdc79aba8d83afd4a9263f045861cd4ddd34b7f6a12/isosurfaces-0.1.2.tar.gz", hash = "sha256:fa51ebe864ea9355b26830e27fdd6a41d5a58b419fa8d4b47e3b8b80718d6e21", size = 11348, upload-time = "2024-02-26T00:20:52.066Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/68/d5e9e6e0d6e43107d8393d2ee3d231dbb597bf93052c6f3117b313724980/isosurfaces-0.1.2-py3-none-any.whl", hash = "sha256:525a49ba93f4dbc35303cd2faf30976af0f99d9274cfa2787aec016b8ef96c64", size = 11649, upload-time = "2024-02-26T00:20:41.308Z" }, +] + +[[package]] +name = "joblib" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cloudpickle" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d5/1d/537ab090f302b838943a1b56497dd53059b9a9b46a074936470173a2e207/joblib-1.6.0.tar.gz", hash = "sha256:2ccc96785b12046c08fd6d55839c12857831b54a3c1673ffadd2f04bfc4eda03", size = 327903, upload-time = "2026-08-31T09:39:04.122Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/53/84099323c2ec4be98d935f63c033ac4151ee83836ca1050ede3b3aadf155/joblib-1.6.0-py3-none-any.whl", hash = "sha256:3dbbf9f6e4b592a2357b854608e980fe6390d131d7a82f011a377ef2ebef7aba", size = 306115, upload-time = "2026-08-31T09:39:02.298Z" }, +] + +[[package]] +name = "kiwisolver" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/07/bd78e6a8fae171ea041ef5bba3ed21a003522fa088834b069b1909981f30/kiwisolver-1.5.1.tar.gz", hash = "sha256:f1303ef2eec81262a4b708c3e858afe58d7c75ad91c1c05266eda7673369859a", size = 104395, upload-time = "2026-08-28T10:28:27.153Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/54/1b8d2bc580414cc75b3a5b3d195981a6facacd8a8d986e589d0a3b51a709/kiwisolver-1.5.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1acc7e5b7ef05e9da8bb70cd6c7c4513090213d2e1ad9720f599f0bf6c52aec5", size = 123362, upload-time = "2026-08-28T10:24:43.402Z" }, + { url = "https://files.pythonhosted.org/packages/57/19/92c30f540dcfff86ff625389427c39b53a9aaea16420ddcc09b2ab8d1073/kiwisolver-1.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bad20d4c69c851c982a1e3606f4c293edfd5a87885786c50082412240c4b1ffd", size = 66551, upload-time = "2026-08-28T10:24:44.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ae/30b94088722e7bce8b821cd5ee935a87f80023e7243125f1accf98e39bf7/kiwisolver-1.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0a4faea5c6db201c6a21391d2ac926ea97acf7dacdbc3c417189e1adb1a00837", size = 64076, upload-time = "2026-08-28T10:24:46.184Z" }, + { url = "https://files.pythonhosted.org/packages/f1/0e/9f394d693be549fa3fab62498c2778294595991047adc3dcf10aa99b91c0/kiwisolver-1.5.1-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e05c2f7925f1d88778e53cb44f14e0223204a3bdd09a41664750363acfb1f2ef", size = 1628782, upload-time = "2026-08-28T10:24:47.91Z" }, + { url = "https://files.pythonhosted.org/packages/64/96/26efc04348c0f332b6e6c471dff132bb8b18b2df494a1b65826601674e62/kiwisolver-1.5.1-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3a4e41e3096bf1f0f1b76e2ffd6d828d6547f574f702d59bdbef7acfa59db9c", size = 1228112, upload-time = "2026-08-28T10:24:49.503Z" }, + { url = "https://files.pythonhosted.org/packages/5a/ac/df9ddc19ec972cd97b60b262dd2c4be28c6192b5a779cad429c6657ee680/kiwisolver-1.5.1-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1d56ec54d257d05e0b50f5780d967540cd07beeaf9e5f645b26d50cce79f4d8", size = 1246756, upload-time = "2026-08-28T10:24:51.225Z" }, + { url = "https://files.pythonhosted.org/packages/cf/de/c38a246b8a4f4b293c7291aa650917b05c5549b0d532749bc523296e90c4/kiwisolver-1.5.1-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8de6f2a4ce7e7bd27d23dd94abf0ccafe0e0e5cc9c764b0577191f2c25f08f26", size = 1295555, upload-time = "2026-08-28T10:24:52.953Z" }, + { url = "https://files.pythonhosted.org/packages/b6/48/3c95a5232d8b5783050d8c8089b58bf1f57018f9135232d72c5aa6b0c01b/kiwisolver-1.5.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:16895f553ee6620a827d2da56b871f835fb70b9216cca5d188e885caf6e3bd23", size = 2179476, upload-time = "2026-08-28T10:24:54.568Z" }, + { url = "https://files.pythonhosted.org/packages/e6/e9/b9e999336afc561b154e93682f597832bdc7446f2ee18b467c5b0e924867/kiwisolver-1.5.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:6b92f60017dda7d877fdc546438b5e28f31c523264f49cf5a48c1d0ce1a0dfbc", size = 2275258, upload-time = "2026-08-28T10:24:56.186Z" }, + { url = "https://files.pythonhosted.org/packages/76/cd/c1c550796ada4a59644b8264b425fe332686d2a086c4729fcc640ececdfe/kiwisolver-1.5.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:7824b5e8bdbf0bccb4ccd37bbb115849a1dc45437fb4de8351385ed07c437ee0", size = 2443424, upload-time = "2026-08-28T10:24:58.093Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d4/30f641eb8e10f6ad5bb9af8a15ec8eea386162a6ef491e68995b8680ef4e/kiwisolver-1.5.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:aa7d00b1700966d2917e54d278aba86897890ca9276dd8b76cf6446b6c181b92", size = 2249457, upload-time = "2026-08-28T10:25:00.223Z" }, + { url = "https://files.pythonhosted.org/packages/1c/d9/211e016f8aa5b5bf496dd3bd7ebab4b73b22d91f025f25e20156a44a77fc/kiwisolver-1.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:96c30002424670b5e1e46495c2b8cbffef39cf77c1d79e76462029d50339785b", size = 70643, upload-time = "2026-08-28T10:25:01.888Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8e/ae7007266dea1fd93fcc48541042c1f6583958055de3b424a59ec16def25/kiwisolver-1.5.1-cp310-cp310-win_arm64.whl", hash = "sha256:f0f4a42db92d6ec7677ab9d12830a2a8ec145a9c6d15db2b593466bc875c78d7", size = 68215, upload-time = "2026-08-28T10:25:03.136Z" }, + { url = "https://files.pythonhosted.org/packages/94/7b/2de6908edc668427c149af5f93112e931f87e1fa4cab80bac32c5844dccc/kiwisolver-1.5.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b3d78f7bb2b9d9a30345be1474b9aaa8685430b54afb51ba3639b5c6c11e9ed6", size = 123364, upload-time = "2026-08-28T10:25:04.359Z" }, + { url = "https://files.pythonhosted.org/packages/8a/24/e70914415c77c97be7e22c80a0740869cb7428768cc380fdcdf6703e7084/kiwisolver-1.5.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5025e36fb4fb275cef0a4e30dbb11cb4ae61d1c83deb90189cb5d7e4cafd6b55", size = 66558, upload-time = "2026-08-28T10:25:05.506Z" }, + { url = "https://files.pythonhosted.org/packages/e8/2b/8b08b11833db4d475b8ef1f36174f8d8a7abd31bedd7e794be78e8814b48/kiwisolver-1.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc1a26b8e53395a01c2c611e58602fa47461f136fba7cd5542e6db6d64be1839", size = 64071, upload-time = "2026-08-28T10:25:06.7Z" }, + { url = "https://files.pythonhosted.org/packages/89/00/05c2d0369ac322d22d5c05f84b5c4a6856fa6207fbae42869108a28f0383/kiwisolver-1.5.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:95a02752aa032eef4aed01cda6d9b687c669bd0396bf4519eef8bba22a286720", size = 1438206, upload-time = "2026-08-28T10:25:08.254Z" }, + { url = "https://files.pythonhosted.org/packages/c0/05/c941a139f27438c1910d630fdc3ccfdab7c8407c72052299ead12ece086e/kiwisolver-1.5.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:719a35fa1156db3640555f95ebb94f60a444e64d1c69626b0edef5df78eba225", size = 1248975, upload-time = "2026-08-28T10:25:10.053Z" }, + { url = "https://files.pythonhosted.org/packages/58/a1/2669ee5512e39b9d4de25faacaedf788c957f93730c5f7c63993ec4f5933/kiwisolver-1.5.1-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:febcce10f2bcdbb80b4ea919238a6a4ac13dbc4c7cadbe8d5d75c3682f8b5404", size = 1266301, upload-time = "2026-08-28T10:25:11.754Z" }, + { url = "https://files.pythonhosted.org/packages/28/b8/353f52f2c7f861a9e90cd2e8f90f85b3ad03060835f823e08298d094c463/kiwisolver-1.5.1-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1d852545c4d0e35a72728d072cbaa59e2fa7dd84bdf01e068d670dd0ceb58eb6", size = 1319708, upload-time = "2026-08-28T10:25:13.559Z" }, + { url = "https://files.pythonhosted.org/packages/21/0e/14b83200eadc2c1d63b76bac01c1813bf072aecf567429f303e00b70258e/kiwisolver-1.5.1-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:2e10ae1bba1899188b33557c10d73affcc12033edd18adddb57d209039976a4c", size = 971720, upload-time = "2026-08-28T10:25:14.934Z" }, + { url = "https://files.pythonhosted.org/packages/86/91/9d43d84d23b1cbff72a142d387ead1ea03db0cba8ff86ed5335addad3cc9/kiwisolver-1.5.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b69602970994a2ed8bbfa78c2f0394a7435226c6040489702d9f0a0ad0c07052", size = 2200119, upload-time = "2026-08-28T10:25:16.636Z" }, + { url = "https://files.pythonhosted.org/packages/ff/7c/f2bd9616f27ffb5e17cecc0baa5d0bbcee7e55aeddc0ccc871d69e2fc3ee/kiwisolver-1.5.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d50de98e8d807dc31822fff96f50293163a62418eb65487a21b42713d72ed0b7", size = 2295005, upload-time = "2026-08-28T10:25:18.374Z" }, + { url = "https://files.pythonhosted.org/packages/ba/17/ee671b72bf8f46a08379d4392c65582541759a542428197562f2898294ad/kiwisolver-1.5.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3221f78211074f561c44ca42eac0619828171bec15a2c4cf6f7747d07df76e8e", size = 1960982, upload-time = "2026-08-28T10:25:19.893Z" }, + { url = "https://files.pythonhosted.org/packages/2e/f7/0e26b4c05bee3bdb0f048dfa305e4fe701999ea17b51e9c616ef91035bbe/kiwisolver-1.5.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0ba9527afc80ae3d7814ed98b6572d02bf85eaf48065678342c5f0c6dab7a8c7", size = 2464918, upload-time = "2026-08-28T10:25:21.65Z" }, + { url = "https://files.pythonhosted.org/packages/ae/62/6eb431133d30ce656ac1e5ff72fac70dd34d54c3984f4011b9ac8bf77d54/kiwisolver-1.5.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e12dfea7f5fc2a34a9080efbf79c4c44eb380ec5b9c6fea09407e08f0d1e941d", size = 2270967, upload-time = "2026-08-28T10:25:23.643Z" }, + { url = "https://files.pythonhosted.org/packages/bf/9f/6f9e489c188200e6fb3193935501894811e8c97577c8ffe9033589bf3521/kiwisolver-1.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:1a7587dc335f2c0f5bd577fd0540bd16c66006bdb60f759a1059f025e6c4f071", size = 70744, upload-time = "2026-08-28T10:25:25.061Z" }, + { url = "https://files.pythonhosted.org/packages/c6/6d/dfc430d1d43957061599adea3f08ea982bb6f4ab601a8c974bedcf2ba850/kiwisolver-1.5.1-cp311-cp311-win_arm64.whl", hash = "sha256:e4e4523d6f336708d732516e6cfca7796cf3d96c9474eb5aecf6165f2f1fefc3", size = 68404, upload-time = "2026-08-28T10:25:26.186Z" }, + { url = "https://files.pythonhosted.org/packages/6b/9b/65b302742389c6f96f2956bef5decf26011309feb2fc5d79613af18adea4/kiwisolver-1.5.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:63fb7294b768f444eb4b068965f2662f28c2fd4161e23bd60fcf3ff27b74c046", size = 123876, upload-time = "2026-08-28T10:25:27.44Z" }, + { url = "https://files.pythonhosted.org/packages/71/74/c21f339956f6f691b2ed7e31d5f3ae767304df6c460192739fc830853051/kiwisolver-1.5.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0ebdef3eae5336568147c39a55be6a2036ffde53faa9ca2d978989ae7c2da12c", size = 66487, upload-time = "2026-08-28T10:25:28.728Z" }, + { url = "https://files.pythonhosted.org/packages/84/e5/bdb34e21523e01dceda064d63713f3bdec91388af24fba1eca7ea5e85864/kiwisolver-1.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1798e83840c3f627246104c4d8a9639c60fa068adf9ce92b61791781fa8a68c1", size = 64660, upload-time = "2026-08-28T10:25:30.071Z" }, + { url = "https://files.pythonhosted.org/packages/fc/f4/dadfec469313c7f428efa7e84b4aba9732f813c13ea7131a24b7b008ef57/kiwisolver-1.5.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:34633ecf50d16187ab8e5528b7a2530f2feb4e23f300db4672538b51cfc5cd38", size = 1477929, upload-time = "2026-08-28T10:25:31.495Z" }, + { url = "https://files.pythonhosted.org/packages/6f/35/09c58daac34e6f6ea5c6dee0094b422118e5a7c265586008a95fd135ac5f/kiwisolver-1.5.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d27c2123977cb9269c30a49ba45f03a4323017ef693e19db4ec9dbe1299a3002", size = 1278499, upload-time = "2026-08-28T10:25:33.375Z" }, + { url = "https://files.pythonhosted.org/packages/19/32/739765e24fbad29d13f83e546ea4abc215a78cea9d677ca09025b027724d/kiwisolver-1.5.1-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6a797a1cefc8b9c93170db580337e1fe3d011ad18b1299943231279406342048", size = 1296677, upload-time = "2026-08-28T10:25:35.059Z" }, + { url = "https://files.pythonhosted.org/packages/df/32/03304d1010e2cc45e5b3b52cef7e43fed3a2a5cd6c87a89b4a88e1d85b5d/kiwisolver-1.5.1-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2551cf9917af48ee7c4b29cc82320489508cf96fd26a51f6fc124de661cd44c7", size = 1346037, upload-time = "2026-08-28T10:25:36.705Z" }, + { url = "https://files.pythonhosted.org/packages/3e/57/4c49377bfd274450dd72ecaa13eaac32ea804a03363e4d1db0c5aa999ceb/kiwisolver-1.5.1-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:38f6e0deb4d0a4615efe0c4efc5990b06ae450ab50a0b321c0b078b6d238c083", size = 988248, upload-time = "2026-08-28T10:25:38.299Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c3/38df144a08b6c5d75ca4504e5cc3141bb3bfef64c04f4ef48204f42711b6/kiwisolver-1.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bfd1de989b3330420e29de39352f5c049905c9e3ee67233a50d550e3d652c148", size = 2228722, upload-time = "2026-08-28T10:25:40.038Z" }, + { url = "https://files.pythonhosted.org/packages/e7/11/3221838a89cd64d9b386353e000cd8a296069a20fbe3584507fdfd5bebae/kiwisolver-1.5.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1209042a623ddfda5497e4066c7b77651dde8e1d3a9dd97599dc7e97f3b9b78c", size = 2325216, upload-time = "2026-08-28T10:25:41.699Z" }, + { url = "https://files.pythonhosted.org/packages/83/d4/075c219230697bb5db910d37262b9bacf880f92b4811a02ab81ed073a253/kiwisolver-1.5.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:26e8268480be5061d509e29669d59103c067a26377a56491630ece11762e3858", size = 1977689, upload-time = "2026-08-28T10:25:43.559Z" }, + { url = "https://files.pythonhosted.org/packages/bb/08/1d219c3c2dd960983d0d4da623d916e9de6385df2b0bab3d1af0e9b8fccc/kiwisolver-1.5.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:d79308fa689fac89cbcfbd4dbfc80b5f95c54c5a7fd4d194be221f9d33d026e6", size = 2491443, upload-time = "2026-08-28T10:25:45.242Z" }, + { url = "https://files.pythonhosted.org/packages/ba/d3/024208ec1079d273f1047468d1bdffbf38bb75b7b268090fd3a0301b9d9a/kiwisolver-1.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b03af77d77e50edba2030fd5f7c352ff209314b09030a3cba7c14edf9a09a444", size = 2295200, upload-time = "2026-08-28T10:25:46.984Z" }, + { url = "https://files.pythonhosted.org/packages/6e/7c/7b210498f9f92e1cd7855f260fa69ef056881087b199ee20c208f0e4189a/kiwisolver-1.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:06a6917674de9e0fe3f66f5430787f59a9f2ddb64af9b714eaec547e29ef5c19", size = 70748, upload-time = "2026-08-28T10:25:48.444Z" }, + { url = "https://files.pythonhosted.org/packages/94/61/ef0daa157c8bb23672f7423e0d14c39db1dc6ef8ed47e6bc54c9c1bef3bf/kiwisolver-1.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:ad8b9671348d7c8716715652ae11f85ed0eb99e265a2df2ca490577d69860b2c", size = 68324, upload-time = "2026-08-28T10:25:49.81Z" }, + { url = "https://files.pythonhosted.org/packages/08/c1/88018321d976f53c421e379c43bc6993e70ce0c8a3ec5edc4bfe102257f6/kiwisolver-1.5.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b6ae6a0328f0bc035741820fdeecdcd67bf4694eee03972e843663107122f450", size = 62272, upload-time = "2026-08-28T10:25:51.02Z" }, + { url = "https://files.pythonhosted.org/packages/85/d2/712bc17ea4f1d216034928069d612defbc6c95a471c55a7203a39faecb1a/kiwisolver-1.5.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:886fc26012f0e8b5f69d1cfe6d711f6b11f194621539bf8e6bb1c25c5dc82724", size = 64481, upload-time = "2026-08-28T10:25:52.22Z" }, + { url = "https://files.pythonhosted.org/packages/1b/e6/6c5380d676f43b6d918033962ea5e72360ca69e5a404154bc496b598ffdb/kiwisolver-1.5.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:aefe930d113798330e9462f7874542977869c0613cba3262e2de3a8d5dee8f3a", size = 66260, upload-time = "2026-08-28T10:25:53.387Z" }, + { url = "https://files.pythonhosted.org/packages/92/6a/7087f5822cc8bb272679641404b1966a42504dac9ee74e2b33840475a0aa/kiwisolver-1.5.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5ca5aebae78a0bc13c1943af4af615d4966c5b650b05d5aa83b50e427196fee", size = 123876, upload-time = "2026-08-28T10:25:54.644Z" }, + { url = "https://files.pythonhosted.org/packages/5c/4b/9f385087ca09ee5ab9c09c6832561a7d2f7c78d3e5661d511e669f70e439/kiwisolver-1.5.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1ed0f5e49d0ceff8b72190824d9e59c062fbbc02c231b853112c78474b3f5ec2", size = 66487, upload-time = "2026-08-28T10:25:55.899Z" }, + { url = "https://files.pythonhosted.org/packages/47/8b/40d33ffd2f378094ed462e9a9a0907e59d4de9845e65a59561272da350d4/kiwisolver-1.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:77a4c8187a5948d7f8795adb765a3c7b553d07d86d88e43038fc32fc1fb9a3f3", size = 64673, upload-time = "2026-08-28T10:25:57.048Z" }, + { url = "https://files.pythonhosted.org/packages/ab/43/86aacc027959108b4c66eeae8b73cedb057dfa6eb3a335d05ad65197081c/kiwisolver-1.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:74ad5c3dad54a4641b4c28cd15ded70899d04459c6c7aeacafea716be97cce6d", size = 1477992, upload-time = "2026-08-28T10:25:58.479Z" }, + { url = "https://files.pythonhosted.org/packages/ed/40/b1d0369048c79733a32c8abb0f2718532e6630641368e33a81384246e844/kiwisolver-1.5.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21e46b23a2da695c364124817bc01d970effd5483147f8d66a6a7167e3f6b851", size = 1278821, upload-time = "2026-08-28T10:26:00.121Z" }, + { url = "https://files.pythonhosted.org/packages/61/a1/fa71c1792272ff9461432715ae60ffb7e11a4d7ac3bf68961b9cab6c60cf/kiwisolver-1.5.1-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:75d9b1cf8258462dbdc1eeda718c96ea7f079324c09067f6daabfcf37712b7fe", size = 1296805, upload-time = "2026-08-28T10:26:01.868Z" }, + { url = "https://files.pythonhosted.org/packages/a3/49/3f0bd94af8e06ecc47eb834b195a06f05d48711ceb2352c56d6835160f0e/kiwisolver-1.5.1-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fca690b00c4c48f6c2a547b0160ed511357093a4e4c9b47e0fadf3128066d89", size = 1346109, upload-time = "2026-08-28T10:26:03.59Z" }, + { url = "https://files.pythonhosted.org/packages/a4/44/3afe6ef9cf06d61220953a8963e94eca978491be1d9547cb01d82a1efa08/kiwisolver-1.5.1-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:876bbfd276473d3daffe30e8c975df4ed9429967b41a6cb362dbb5155b6f13ad", size = 988252, upload-time = "2026-08-28T10:26:05.306Z" }, + { url = "https://files.pythonhosted.org/packages/e4/30/a12bd7a7285a211e1747c3eec77b8c614dbfcc1dad942f7611a1a6921ae5/kiwisolver-1.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f942903fde7363d1d879057ec5de01310efda2597161784d752fa9953a01a71a", size = 2228846, upload-time = "2026-08-28T10:26:07.312Z" }, + { url = "https://files.pythonhosted.org/packages/a1/6b/233e2958abf0dab7b18d07e52f286e02e519d7651bfbbe97af9347564109/kiwisolver-1.5.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c90d3022d8a94778939cda8638c6c8da8fa757b8958dad7ec868ce29c87681b8", size = 2325583, upload-time = "2026-08-28T10:26:09.093Z" }, + { url = "https://files.pythonhosted.org/packages/42/8e/7673060a27b01405b580058510adef34687069d229800239f5e44682d4d0/kiwisolver-1.5.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8a34616dc2521cc8dc1d7d081734da63539f021ac0450ce950908340c6e7aa2f", size = 1978221, upload-time = "2026-08-28T10:26:11.127Z" }, + { url = "https://files.pythonhosted.org/packages/3f/7b/1b882fc1a8b4a0bb8084e7d1d85004116d08c92c0705d31f2928dec607f2/kiwisolver-1.5.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:8bf4df63592c2a66b4f8edc5df2544998c288aa02f96ce0acd880cd1de8c8127", size = 2491819, upload-time = "2026-08-28T10:26:13.348Z" }, + { url = "https://files.pythonhosted.org/packages/39/9c/426deb49e62c5f69464b64bbeca064d3b758a7506b8913d986ef34f4619c/kiwisolver-1.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d09037ca068d784ebc4aec290ef952ca27ac15dd9c0b5801a88c6e1096b83e6b", size = 2295520, upload-time = "2026-08-28T10:26:15.042Z" }, + { url = "https://files.pythonhosted.org/packages/f5/22/deabbb3ad6d918d74b7831b2d8ae7151b09d21c87974e5ee8a456f58c94c/kiwisolver-1.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:dc23390afe9f4ef9ac3bcc72a03a56eebbde03f4c571a32cb38f859cff9a6524", size = 70758, upload-time = "2026-08-28T10:26:16.504Z" }, + { url = "https://files.pythonhosted.org/packages/b9/71/02fa5c2fd92068bb8952847e70ee6c5cb280e7febe11653d17812acc53dd/kiwisolver-1.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:186884a58486651e3c217b6acea0a53eaa9498fdd472057c46f2f0fb5c25aad5", size = 68329, upload-time = "2026-08-28T10:26:17.658Z" }, + { url = "https://files.pythonhosted.org/packages/dd/87/2d5dfad0daf17dcc18d98c48ed2332fc3f051cf599e60be6182a30dd4cf1/kiwisolver-1.5.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0324cd2567259b7a095f6cf18a52b0ffc6f3de9e69528ff1bc0e7a37bd43ff1a", size = 62337, upload-time = "2026-08-28T10:26:18.778Z" }, + { url = "https://files.pythonhosted.org/packages/08/c8/83e1624f15d6262b470dbcc80b09979fd4d5b2ea3ddfc6b6e3327e235726/kiwisolver-1.5.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:74ea337e0ec3f6f342a36a4f1b5cd94dd9affddcd28ba9aae2905af932ee8c6b", size = 64513, upload-time = "2026-08-28T10:26:19.909Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2d/827ec30eb07f528c08d8459ffb318ae91a56d793ee8acbea8b491f0ff906/kiwisolver-1.5.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:ee9df1f0d77b9c6e94f4ac0fec533fbddd5ea3a327807f18d7b069ae019ded80", size = 66287, upload-time = "2026-08-28T10:26:21.078Z" }, + { url = "https://files.pythonhosted.org/packages/53/11/5c43a562529dad8def4b81e5e1877c612a7e0298105a5939b3b409d2079c/kiwisolver-1.5.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fc271a6f0a2126958f4090e5507b9da5848927dae331f8f763bd4aa642b3d2cd", size = 123940, upload-time = "2026-08-28T10:26:22.475Z" }, + { url = "https://files.pythonhosted.org/packages/64/db/9bd6c505c95128c258a55236bfbb3a7a3fb6023f863316b6d7d9f3c69052/kiwisolver-1.5.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9b3092d8992a1d69b7a59c3e39f35e1b9be327a17f68a7c35fc17329e337d6f2", size = 66493, upload-time = "2026-08-28T10:26:23.743Z" }, + { url = "https://files.pythonhosted.org/packages/e1/f4/b3007a3ed5c9be73f81161140684cf7d9bdb9c4b632f5f484d2a1c713fb9/kiwisolver-1.5.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c2306e8bb53601979fcb3fa09cc65e031876d9ae01eff2fcbcd7a84ef94d5bc1", size = 64720, upload-time = "2026-08-28T10:26:24.95Z" }, + { url = "https://files.pythonhosted.org/packages/a1/13/08188f0cafa3a800403e4ff62b9aad4e7a17f9c4c7e080dc8f18c64794cf/kiwisolver-1.5.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:18a0cfb124546a4c2e6087c5f3029c7f44b37c85b142e0ced71f73a7599ac208", size = 1475867, upload-time = "2026-08-28T10:26:26.393Z" }, + { url = "https://files.pythonhosted.org/packages/8a/3e/053bdc3c9abdb8f2606225eda398adca25c0c91ab90add8222a69db65ee0/kiwisolver-1.5.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:34ec467940442c9943016fb2d4c81d1ba84351eeca2f1a78f8bc87f1ba0d414c", size = 1282865, upload-time = "2026-08-28T10:26:28.118Z" }, + { url = "https://files.pythonhosted.org/packages/5e/64/a44c341b36b610588cc2f1e89b3cae072a3119aa8be578e90987cd640751/kiwisolver-1.5.1-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a83ee7107df13abe42a54a6654670eef9bb39425cf2e27f65e0007465e1286ab", size = 1300865, upload-time = "2026-08-28T10:26:30.125Z" }, + { url = "https://files.pythonhosted.org/packages/60/5e/7e7d716dca38c714478b741257a5b4a321d9932b8d851551a136dcaf3984/kiwisolver-1.5.1-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bebb89489b279b2f5661bbbb2abcc87bcd4a46607bb4a5c966f04f1db6b8df9a", size = 1348071, upload-time = "2026-08-28T10:26:31.829Z" }, + { url = "https://files.pythonhosted.org/packages/10/1a/2b98fdda8bf45b7be317e48ed12393d44334394d315c74b81f4a14c0e31b/kiwisolver-1.5.1-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:509735237ae0d849e8a843551d423d2500d2e0a9ac1611a145658b29c0fb9f85", size = 992191, upload-time = "2026-08-28T10:26:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/b6/55/d893f5ede0e50f9e3fcf01f6015f42ec7d9cd221e26772701fe4a98745f9/kiwisolver-1.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:824c3d763a05ea9e9003610145186b0e9848c7584a5575c79bac5a8e7cd80bad", size = 2233854, upload-time = "2026-08-28T10:26:35.282Z" }, + { url = "https://files.pythonhosted.org/packages/b1/82/f85f6279555a6ee1639fef7bfe83adb037a03e11a6fc9eaa54b8d0380339/kiwisolver-1.5.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:1fff05e239575b1481b6ed1a782f6fad616efbf1f0b1f44e6e85c4dfe426e483", size = 2330621, upload-time = "2026-08-28T10:26:36.9Z" }, + { url = "https://files.pythonhosted.org/packages/56/31/e11aea078f66fc2fffcc179d38ca90d9da97652a241b64519169742ba46a/kiwisolver-1.5.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0627b9bceb9c3cdcf12b8a18655eedfed2692b038df27423383c120d0b7dc2d6", size = 1982848, upload-time = "2026-08-28T10:26:39.01Z" }, + { url = "https://files.pythonhosted.org/packages/af/ea/2956b63bf5140ca46aa2c2818e6aa03e2d5754dd2fa41db1c6b28922940c/kiwisolver-1.5.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:8a708a47ade1fe19e8371d5da076bac0dd4b0a5a7985ad6c637f7f7e361b6baa", size = 2494850, upload-time = "2026-08-28T10:26:40.837Z" }, + { url = "https://files.pythonhosted.org/packages/11/d1/3829542258d8b3fc0898d221e7ef0e2c83eca0d348709bb8dbe54f3d4005/kiwisolver-1.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:007a5553dfc4f4e8d184f588a0200e2cd4b63a59cc8796df3c39909e679dc7a0", size = 2298067, upload-time = "2026-08-28T10:26:42.803Z" }, + { url = "https://files.pythonhosted.org/packages/4e/0e/49522e1ab5788cbaf63a26fbd3b851f9028616828c961b8a31b35cb96df8/kiwisolver-1.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:f4167e87b397f273dc2356fcf1eaf50a6bac51e6105f45103ef7129c8efb0255", size = 72282, upload-time = "2026-08-28T10:26:44.268Z" }, + { url = "https://files.pythonhosted.org/packages/f5/b6/22e7ca5315d363e6f81c9f37c9472e12e7b298731e77c0428e6a911a2c39/kiwisolver-1.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:5c490db2168a508088f59140dd392556a54b8bd1048fc6383c8baff13c359673", size = 69855, upload-time = "2026-08-28T10:26:45.725Z" }, + { url = "https://files.pythonhosted.org/packages/30/8c/03a9cfbe871964c8758a816eb03ac96c806da2795a9a7cd9bf9648bfb594/kiwisolver-1.5.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4d4ca09bf13cff792b1884f64b98ee6c2467930d632233be25c56b442d99f10e", size = 126289, upload-time = "2026-08-28T10:26:47.023Z" }, + { url = "https://files.pythonhosted.org/packages/f2/e3/14ce3041ca79dff9c9d884ca00c7bf32374e76028a865a9ecd99b4f5a517/kiwisolver-1.5.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:44b8faef94f1857e77fa0238f3390ff1ac51d2ea20a487e2e452a59fd2b5f5ca", size = 67709, upload-time = "2026-08-28T10:26:48.268Z" }, + { url = "https://files.pythonhosted.org/packages/af/c4/45030471a66ec8ef042e9f96ffe1d522c9ab12da180186a0898966fc1385/kiwisolver-1.5.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2ae70bc59790d2af72a3f76f24b272403e135070340281108b447cb77ea70819", size = 65909, upload-time = "2026-08-28T10:26:49.523Z" }, + { url = "https://files.pythonhosted.org/packages/42/73/17dce073a6ae259bb32cf9d686c4079d2e538868bc45967462bf33df914a/kiwisolver-1.5.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:43844c1a7ad6d723d5b5b4c4fc7f5bd399c40e288120d16257c7c9e8765c6e85", size = 1584907, upload-time = "2026-08-28T10:26:50.933Z" }, + { url = "https://files.pythonhosted.org/packages/8c/84/ae3c75909f507283cbfcc7e916c7e822579ef962020b97e6882b27b4478f/kiwisolver-1.5.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22d5e5aaad6be121f2515765e3b1c444352cb8eb4c86510801db8f2e50757316", size = 1392474, upload-time = "2026-08-28T10:26:52.638Z" }, + { url = "https://files.pythonhosted.org/packages/34/31/8bcc83caad5bce8fa4577152389848bf6bc110e51e573a2b4e7c2aa34c89/kiwisolver-1.5.1-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3fa5855898f6d3d01b72ccd48a2d65cbdee301251603fefe34e2025bddba219c", size = 1405246, upload-time = "2026-08-28T10:26:54.248Z" }, + { url = "https://files.pythonhosted.org/packages/55/72/220345537d790cf4ae54f8acfff4b5cc2468e0702a384d651cf7a771c63e/kiwisolver-1.5.1-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d66a64dd5dec136040ec2ae94aa026a912ee60fdd45bc28d3db30037fd809e88", size = 1456099, upload-time = "2026-08-28T10:26:56.042Z" }, + { url = "https://files.pythonhosted.org/packages/cd/10/3725fd2398f66d18c34b4e0f81a8d03764cd4f4f089f58a527f0b4428086/kiwisolver-1.5.1-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:9e51c119992ea8820706871c30a4642ec76de20ae82f9b50b9a45517d8e9f810", size = 1073695, upload-time = "2026-08-28T10:26:57.658Z" }, + { url = "https://files.pythonhosted.org/packages/86/cb/28d6e09e66b93e4588b2e6b7d84d020ccefea09e2f4de788510a07efeab7/kiwisolver-1.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:70ed9a45c7484d2b30cdacf60d220f494a1763b9fec1ad03285c6553fa0889f2", size = 2335355, upload-time = "2026-08-28T10:26:59.202Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b3/a0f31d5e4e40af7dc97c36b8a74fdd3a36cf3c8bbd098da9a23466ff6a94/kiwisolver-1.5.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:98b208a7cc42c803445ef551d6753cc42a5ea13e9cab1ee66cd8b9cb70195330", size = 2426524, upload-time = "2026-08-28T10:27:01.181Z" }, + { url = "https://files.pythonhosted.org/packages/2f/c9/728f63bd58c72cafdc79fc306abeeac7391bec03b757a48dadeb30906521/kiwisolver-1.5.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c6834b92dd2428e2dd85ef3d85f723d3c12f20aaf43a2ddd4f944ca25d833408", size = 2063430, upload-time = "2026-08-28T10:27:03.06Z" }, + { url = "https://files.pythonhosted.org/packages/84/df/ce188b96f92f9a2c958231da140768918cba53c9713dc887b82f85462118/kiwisolver-1.5.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:5d142e352eb13facc7dd047489aebdff6ba78576c239f1ea04931979caaf0567", size = 2597513, upload-time = "2026-08-28T10:27:05.072Z" }, + { url = "https://files.pythonhosted.org/packages/69/d6/76947c8203768968382e5bd74d9cc95654746703a61ea53015f2c74a2e06/kiwisolver-1.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f9b1c4900736e489a812c529100de4b8fb617d4db075e931e213c57424b83d9b", size = 2394488, upload-time = "2026-08-28T10:27:07.423Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d4/14b21e4eb203c4d15425e8b6a2c625a320b4a1f2f7557eead63ffc30ffb7/kiwisolver-1.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:5978c3340f16a35c30f8ab2fa7bcf559973c55f1a5ef6970e1f621acf3c4db13", size = 75404, upload-time = "2026-08-28T10:27:08.892Z" }, + { url = "https://files.pythonhosted.org/packages/cb/f5/53157899fc7f45f76421b77b99eb1639dd0f83f26ff9d76300c96bb4a3b0/kiwisolver-1.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ca307d6c259e5c98d3cb9ade55342b47a6839762caf2536f3d7b46ee660cc82e", size = 72946, upload-time = "2026-08-28T10:27:10.944Z" }, + { url = "https://files.pythonhosted.org/packages/75/62/f786c3a27f181fa339d851a77e266d208e776b9883cabc40a5b041a31b5a/kiwisolver-1.5.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:bb7c99f0673c03017a3ee01e54a5c2617a05468b11eabe513b0080e063ed95b1", size = 62420, upload-time = "2026-08-28T10:27:12.308Z" }, + { url = "https://files.pythonhosted.org/packages/02/cd/58a91ed25fbad0facdf503297b03768efab04bdf3141e5e3b49a34be7443/kiwisolver-1.5.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:0d8924877ce22e17326a99a418c3c82037da078df3c6a260b13eca677444e6e7", size = 64577, upload-time = "2026-08-28T10:27:13.502Z" }, + { url = "https://files.pythonhosted.org/packages/f6/5c/d501ef5a0958b226eac28306d24d5e5f114be0ace50e19cabae7b6b3b197/kiwisolver-1.5.1-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:534f02c1abb31ed6dbd3515545285c330b2f12d00fdb1fdb71658b9ca5a13a6a", size = 66284, upload-time = "2026-08-28T10:27:14.813Z" }, + { url = "https://files.pythonhosted.org/packages/a3/a6/8fbecaf4fc18c02f31f05e47a84c010a80e3ec391ed2f0bdade1d62b5954/kiwisolver-1.5.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:cea20da04494e662b83c872683bf4ff2345206043d036315ed0e924b652e7294", size = 124031, upload-time = "2026-08-28T10:27:16.193Z" }, + { url = "https://files.pythonhosted.org/packages/7c/c6/bf3090d2983b4204347cbdbe952116e7c3b2abf62b4e33e50167a13e75ee/kiwisolver-1.5.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:7fd82debf43c6acd0a94359d232f6bb516ee13f269a7993736a9ac9f988bb5d9", size = 66489, upload-time = "2026-08-28T10:27:17.506Z" }, + { url = "https://files.pythonhosted.org/packages/85/de/562dddef55fdd7c291da8626d6619e72b5fc0870e6ccca0e149a5731e7f3/kiwisolver-1.5.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:18170a77ddfecf40ec60d0928268dc95880c881864e015a8f34094ed18b9b9ad", size = 64806, upload-time = "2026-08-28T10:27:18.673Z" }, + { url = "https://files.pythonhosted.org/packages/89/b1/ba7b9c0164ce1cf62bf2872db63b8483289cf0f3110d6f9390eb09e409ed/kiwisolver-1.5.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ca7f6fe0f37ca978a1e5eb7a3a68e6413f417e78e838324947ffd420202b198b", size = 1482211, upload-time = "2026-08-28T10:27:20.039Z" }, + { url = "https://files.pythonhosted.org/packages/13/dc/34da54efb4976616d45c20aae32d70e89d6e7395ed908029154d1609ef22/kiwisolver-1.5.1-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5b973887ff782cfd6b67c9904ad8ca542e0bc5e4961503408b423b5a688b4d38", size = 1283739, upload-time = "2026-08-28T10:27:21.82Z" }, + { url = "https://files.pythonhosted.org/packages/5a/3a/30ffb62bee646e266e98a1b5cd276d9c75b6116fbfcb87c1190838c1b6df/kiwisolver-1.5.1-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f76fc85bd054c806960f917ec0f329e24e436f1712267d90588e4c39890caa63", size = 1301681, upload-time = "2026-08-28T10:27:23.876Z" }, + { url = "https://files.pythonhosted.org/packages/a9/8d/13c70be22a8506880b35fdc38dca36629613bc493405c79f4037f2cd2bb9/kiwisolver-1.5.1-cp315-cp315-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:828f75af2b0080c8a972e75f649ab46af008e92c6104a57a759157200b835b75", size = 1349159, upload-time = "2026-08-28T10:27:25.899Z" }, + { url = "https://files.pythonhosted.org/packages/82/0e/993972b8ec6767f47cd69818fb3a5ff14510557d29f7d1a839be7574fa1b/kiwisolver-1.5.1-cp315-cp315-manylinux_2_39_riscv64.whl", hash = "sha256:431dc224a1a92a5c8f582d96e505196a3b5997a7271076678da2dfde67b77e9a", size = 997613, upload-time = "2026-08-28T10:27:27.507Z" }, + { url = "https://files.pythonhosted.org/packages/36/82/ca26eddd2eda2420dfc56693449c1f821f78b485da9cbde9904c03af3f93/kiwisolver-1.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:61e9a64c7635095a6bfe483e2ff055d437c59bd45f3617a228b37277f0185d62", size = 2235109, upload-time = "2026-08-28T10:27:30.113Z" }, + { url = "https://files.pythonhosted.org/packages/c7/84/97d920881e10840b8d7c7185620298e3e4c88820b05514e3a15a258b08a6/kiwisolver-1.5.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:3c24cd69455e1b00ddf770c13b6e2c33e07d6dc3f2d34add0bf9277c5c6bbd46", size = 2331207, upload-time = "2026-08-28T10:27:32.795Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ce/34d74b8f25acc58800f4c09268371e8d6159cf0f1206f1e4dc7835629b48/kiwisolver-1.5.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:27add358abe374ebaa3b8763ef380bc99051b5a4b18d94878366a9e4f59efef0", size = 1986696, upload-time = "2026-08-28T10:27:34.628Z" }, + { url = "https://files.pythonhosted.org/packages/0e/01/f892644014612527aef7031d3306a2ffc60b3cb044f802c1561f8e5e14f3/kiwisolver-1.5.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:255605693a483db7bd5c79f60437f7bf658f7f520d61aa42722e32257c941951", size = 2496400, upload-time = "2026-08-28T10:27:36.818Z" }, + { url = "https://files.pythonhosted.org/packages/32/6d/d8284e66e697026536e5f418b9cfe56567bffd3c775e3ecfbae373605854/kiwisolver-1.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:7d38b0c279c3032e8c9cc013b405c6df8e1668dbf15465779aa7f15f61201812", size = 2302967, upload-time = "2026-08-28T10:27:38.803Z" }, + { url = "https://files.pythonhosted.org/packages/46/0a/69a355e27f32ba50d5b6369949b6a1702e122f5277c89bc76d452b81c1c4/kiwisolver-1.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:958254518717542d02d0688d0d20cbf771da5e415e6f49543f92481c850a4540", size = 72283, upload-time = "2026-08-28T10:27:40.491Z" }, + { url = "https://files.pythonhosted.org/packages/ef/d8/7a95be90c33dcdd52204d4aa6384d731443225b887283bbd8b61e7931f6c/kiwisolver-1.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:da3275833be0edbaf4830fae08bae3dc7219f40ce0c37eaa6c25825957e06612", size = 69860, upload-time = "2026-08-28T10:27:41.835Z" }, + { url = "https://files.pythonhosted.org/packages/31/f8/9bc493e7f5707788ba7f621902c68f82dc3a7ba03c78fbd337b026cef1ed/kiwisolver-1.5.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:470d420f98d368d6f010633a20659b544c5fdfa5329e6b70219f2ef08fd4a7ef", size = 126336, upload-time = "2026-08-28T10:27:43.379Z" }, + { url = "https://files.pythonhosted.org/packages/d1/82/3aea86b3f99712db825e9ac5631bf99571e818a8b8961ff98cebd798413e/kiwisolver-1.5.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:83f78128fa28705fa85d01c59771c72fe81c11bd0e6155edbb9f818983a7d761", size = 67698, upload-time = "2026-08-28T10:27:44.613Z" }, + { url = "https://files.pythonhosted.org/packages/84/7d/8daafc5d2e7f9c47a4f78f8865d86d2a9cf399c2a85f86c44a993594410c/kiwisolver-1.5.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:9506e892bcc3b409831d363c6f53e5985e1c8d1f6f6b0256d00358684ff85378", size = 65945, upload-time = "2026-08-28T10:27:45.932Z" }, + { url = "https://files.pythonhosted.org/packages/3d/c9/51dc974d9130da70a8c47a96160123443d387ffe1b6b833d6f91d9429339/kiwisolver-1.5.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cea90547bfd93807e0013a004dc76552be44fad3bc1cc2b38610a9e889ed098f", size = 1588030, upload-time = "2026-08-28T10:27:47.678Z" }, + { url = "https://files.pythonhosted.org/packages/e6/86/f3e1a730e7a995149d8d3ff9e313b6d8a17b2cf1d98a8eff139dc30463fb/kiwisolver-1.5.1-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e8e4d953faaded9ec7ede36824e9814082d22d4c7b1eafbfa079ecba8cd0d076", size = 1390760, upload-time = "2026-08-28T10:27:49.676Z" }, + { url = "https://files.pythonhosted.org/packages/4f/3b/8ba25a2b5a0d2375e046f1b72de5179513f0be95aba6e7b094c89303929f/kiwisolver-1.5.1-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7e9c01d3dd7ceba4d1d436cc021d40d592466e40b9bc7f5d83dc4e98a5c9cd8c", size = 1403279, upload-time = "2026-08-28T10:27:51.242Z" }, + { url = "https://files.pythonhosted.org/packages/18/d0/278d5cb8be812740027d5ca0a7eda0c375488a88d6dce0fa60fcc2591ad2/kiwisolver-1.5.1-cp315-cp315t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37f801b5d7cc0e5a548921308e059fd2b057bb42972b591cfa3049f95423c4ed", size = 1454429, upload-time = "2026-08-28T10:27:53.213Z" }, + { url = "https://files.pythonhosted.org/packages/94/37/bcbab41063ec284c1d200efe5087cf087798c2f8916960aa8a20dd303290/kiwisolver-1.5.1-cp315-cp315t-manylinux_2_39_riscv64.whl", hash = "sha256:e68e151428b5384f766cd25739bf77c7e4a3dc93b5ded7a12118d9fbfdf78ab6", size = 1073225, upload-time = "2026-08-28T10:27:55.089Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c5/ab79dcdf5ae28909a51210ae0a1c579e97ff997b3466414f0d04c0994583/kiwisolver-1.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:8f8fddb8e323bd6eee4e54e69a39243beab22689070f4c66b472c4cc88bb89d8", size = 2334335, upload-time = "2026-08-28T10:27:56.78Z" }, + { url = "https://files.pythonhosted.org/packages/8d/8e/71d047468a189041d9c93f3b76844b924f9793b188c44bd149fa258912da/kiwisolver-1.5.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:3cc210010fd2f438a3ed430b45f1b501fd13a8618bf984dc2c5ce5b69b78752e", size = 2424982, upload-time = "2026-08-28T10:27:58.639Z" }, + { url = "https://files.pythonhosted.org/packages/7d/37/1347461bbea6d0e1f0580b94ef603b18e72c2be5f667fa1653867361a00b/kiwisolver-1.5.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:b5664603a253efd3a75716d793d1d3a6a82723b61dc6db767b2460bbbeec4c0f", size = 2062857, upload-time = "2026-08-28T10:28:00.407Z" }, + { url = "https://files.pythonhosted.org/packages/1f/87/c7f0976c9cd0d127643351bc0c9929e0b8899d7f49d4ec238cd909e39c42/kiwisolver-1.5.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:a7b85b2cc6ea45e5f7e8c9a30bc9fabd47cda09106cbb4b967335c3e6c43b69d", size = 2596022, upload-time = "2026-08-28T10:28:02.183Z" }, + { url = "https://files.pythonhosted.org/packages/e3/9a/59a6f6ae6f938c15076be2c21b6cedea973d71bb1349ec84fa485fab82cf/kiwisolver-1.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:ab620eb663952455271ac37f9aaad86b73c969c02f11f53cea405b38e96a4300", size = 2395634, upload-time = "2026-08-28T10:28:03.977Z" }, + { url = "https://files.pythonhosted.org/packages/e9/2d/8982c1fb7926da5bb7ed60318c3665b5c3f941447271ac982960a11b8637/kiwisolver-1.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:cb6fae641357ed2f6e533c0d3c6504a4a5703621a50c89459e46051d56b61140", size = 75379, upload-time = "2026-08-28T10:28:06.307Z" }, + { url = "https://files.pythonhosted.org/packages/07/78/ba7b6dfa1708b82b373ac056928a30c545d5c1a627df9839dcec3c6c1881/kiwisolver-1.5.1-cp315-cp315t-win_arm64.whl", hash = "sha256:b390aec180a7c054919c04898835e1c77bced23ea8383eb2c570213bf25d1a86", size = 73011, upload-time = "2026-08-28T10:28:07.578Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c4/1407df7512a5b36cc79840e01710dc575733c461b13ab866cae77eaf87f3/kiwisolver-1.5.1-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:482676e5bd48d70ac99d9fc78863469845421e01184fa83f1f9366dc49f7e974", size = 134002, upload-time = "2026-08-28T10:28:08.881Z" }, + { url = "https://files.pythonhosted.org/packages/16/45/c37a21ad5c0ab581a93c55ad544721aaa1f0ae94edb29c6a678a23d013e6/kiwisolver-1.5.1-graalpy312-graalpy250_312_native-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:072bdb15a3c19a5b5dbc8f8fb1f4e1884bf4f3507eeb4cc6334401274d37a5c0", size = 194292, upload-time = "2026-08-28T10:28:11.06Z" }, + { url = "https://files.pythonhosted.org/packages/a6/c1/69f00d627949580e43d57af0aa465df46868d7c29801c137a55374101294/kiwisolver-1.5.1-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:a5a00665d1a0e26763a7338d7e911d4598fbc1d50dd0d6b7919b7dc6c5d6569f", size = 73362, upload-time = "2026-08-28T10:28:12.449Z" }, + { url = "https://files.pythonhosted.org/packages/ba/f1/8bae9fac0f1837679ed59a5dd7e97e7eb943b738defa7cc0117ea0d107dc/kiwisolver-1.5.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:a5716a33bfabb2c6ce27b6cf03253467b3804f83e215f4d202685cf93c6c9874", size = 59543, upload-time = "2026-08-28T10:28:13.794Z" }, + { url = "https://files.pythonhosted.org/packages/c3/3e/42e3639d32ef3ac6ede80c39dc1d1df6224927409960d8f762b6cb504efe/kiwisolver-1.5.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:685929988b208a911f1285e2f8ed54210b0d681a3dc0f03e00d599d291986e7e", size = 57504, upload-time = "2026-08-28T10:28:15.074Z" }, + { url = "https://files.pythonhosted.org/packages/e1/12/f4beaaceb740b96c363a8fa6f34dbdf37a58d9e5f15416427d7bf89c552a/kiwisolver-1.5.1-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e4e49f7e1a4e7191bdf9dc67a974db714501b1fc52c24324103d06a86abd5c08", size = 79885, upload-time = "2026-08-28T10:28:16.345Z" }, + { url = "https://files.pythonhosted.org/packages/02/24/18d8a755acdae79c37ca2f1a925795aab67b9456c30c8569c377ea3abc77/kiwisolver-1.5.1-pp310-pypy310_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f0a887b6565bbfe80efde2b7f6e8890d7d9bbdb11bdb17028a3690c32fe0621f", size = 77582, upload-time = "2026-08-28T10:28:17.719Z" }, + { url = "https://files.pythonhosted.org/packages/df/a9/b86dde4f553ef74db0f9c32398614e24b2e8ec1d29a6ca04ae080d7dd29b/kiwisolver-1.5.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:1983f0974a750a6f6556f368ba11105d1d8369c735b944747c9f12ae5aea7aae", size = 98321, upload-time = "2026-08-28T10:28:19.087Z" }, + { url = "https://files.pythonhosted.org/packages/b5/1d/59ba570b1774e95e97fde3a0981b2e22118a7a495f73bf74cedc538566a0/kiwisolver-1.5.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:416ba7ff9f233b7036689bb5a3783537e838ad483f63558d2a800f75afe738b1", size = 59450, upload-time = "2026-08-28T10:28:20.383Z" }, + { url = "https://files.pythonhosted.org/packages/22/98/a6849f04dc18b5400e8b98affa2cd8fd86ed583085f036e57b32e571f4fa/kiwisolver-1.5.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:8af9b142ad719ae3a911ebf616bc4b78b32bbab84d6a40d3ad2f129670509957", size = 57400, upload-time = "2026-08-28T10:28:21.632Z" }, + { url = "https://files.pythonhosted.org/packages/f4/ce/a7dc71353dd06a4cbe02222773f52d4a28c81e5a452a75797f8ed113dc99/kiwisolver-1.5.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5daa1f19e097050b9c4d9a78fcc9263cb96c9dfae08037ddc1b7c4ad1889f2a2", size = 79891, upload-time = "2026-08-28T10:28:22.936Z" }, + { url = "https://files.pythonhosted.org/packages/10/b1/d61c61a84ff85d1a36a99df2c152b59ffedb1d356c598902aba44abcdb60/kiwisolver-1.5.1-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cdaeeb6c350106df6bf9d873395973e5f066a9713200b72cd64f55d0a3eafab6", size = 77605, upload-time = "2026-08-28T10:28:24.322Z" }, + { url = "https://files.pythonhosted.org/packages/d3/52/5aef56f21a460a6e43ab3cdfc7697d59d7b87deb0ec97a0f7b91aa4a521b/kiwisolver-1.5.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:17851e5dad4484be0cbccbde3b15331deae036de9aebd45eed964487802b172f", size = 98465, upload-time = "2026-08-28T10:28:25.696Z" }, +] + +[[package]] +name = "lazy-loader" +version = "0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/ac/21a1f8aa3777f5658576777ea76bfb124b702c520bbe90edf4ae9915eafa/lazy_loader-0.5.tar.gz", hash = "sha256:717f9179a0dbed357012ddad50a5ad3d5e4d9a0b8712680d4e687f5e6e6ed9b3", size = 15294, upload-time = "2026-03-06T15:45:09.054Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/a1/8d812e53a5da1687abb10445275d41a8b13adb781bbf7196ddbcf8d88505/lazy_loader-0.5-py3-none-any.whl", hash = "sha256:ab0ea149e9c554d4ffeeb21105ac60bed7f3b4fd69b1d2360a4add51b170b005", size = 8044, upload-time = "2026-03-06T15:45:07.668Z" }, +] + +[[package]] +name = "librosa" +version = "0.11.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.11.*'", + "python_full_version < '3.11'", +] +dependencies = [ + { name = "audioread" }, + { name = "decorator" }, + { name = "joblib" }, + { name = "lazy-loader" }, + { name = "msgpack" }, + { name = "numba" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version != '3.11.*'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "pooch" }, + { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version != '3.11.*'" }, + { name = "scikit-learn", version = "1.9.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version != '3.11.*'" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "soundfile" }, + { name = "soxr" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/64/36/360b5aafa0238e29758729e9486c6ed92a6f37fa403b7875e06c115cdf4a/librosa-0.11.0.tar.gz", hash = "sha256:f5ed951ca189b375bbe2e33b2abd7e040ceeee302b9bbaeeffdfddb8d0ace908", size = 327001, upload-time = "2025-03-11T15:09:54.884Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/ba/c63c5786dfee4c3417094c4b00966e61e4a63efecee22cb7b4c0387dda83/librosa-0.11.0-py3-none-any.whl", hash = "sha256:0b6415c4fd68bff4c29288abe67c6d80b587e0e1e2cfb0aad23e4559504a7fa1", size = 260749, upload-time = "2025-03-11T15:09:52.982Z" }, +] + +[[package]] +name = "librosa" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13'", + "python_full_version == '3.12.*'", +] +dependencies = [ + { name = "decorator" }, + { name = "joblib" }, + { name = "lazy-loader" }, + { name = "msgpack" }, + { name = "numba" }, + { name = "numpy", version = "2.5.2", source = { registry = "https://pypi.org/simple" } }, + { name = "pooch" }, + { name = "scikit-learn", version = "1.9.0", source = { registry = "https://pypi.org/simple" } }, + { name = "scipy", version = "1.18.1", source = { registry = "https://pypi.org/simple" } }, + { name = "soundfile" }, + { name = "soxr" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8e/c1/ce66b20953b7370421dd13d22b4e04e401346c824ceacb9f4e722eec84dc/librosa-1.0.0.tar.gz", hash = "sha256:73ed480d4022e436e85dfa6f6b06ff38a259b9210039ac99939cd64854b61a57", size = 377965, upload-time = "2026-08-11T15:37:37.013Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/14/0b2e4cc8017796dc8f69de740708601832c1e8d5377166eedb4db0a92264/librosa-1.0.0-py3-none-any.whl", hash = "sha256:5910a6c0e1b2e494b92758c1615a7acbd0515a2315e138927ba2982f2af88857", size = 294628, upload-time = "2026-08-11T15:37:35.455Z" }, +] + +[[package]] +name = "llvmlite" +version = "0.49.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/27/72ae94ea5c8f7349ec1c229d4cd058feb799cbd0833ad6d1b47c919b37b7/llvmlite-0.49.0.tar.gz", hash = "sha256:00f16db782f4a13c78c5804aedc434e46794a77e89999a168f9401106270e50a", size = 194467, upload-time = "2026-08-11T16:26:00.489Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/0d/daceb212c44cad1115b2d05dd55beafe23ff06627344adb4ded0c661bb1a/llvmlite-0.49.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:ee81e96c15a6f870918f1eb60c913551c16aa23defb4f5f1acfa660d6a0aaac2", size = 40479229, upload-time = "2026-08-11T16:22:56.104Z" }, + { url = "https://files.pythonhosted.org/packages/72/2c/eb42378b4f3afc71f9fe172d01f30135dc1d54c7fd95cf76d5445d6f7809/llvmlite-0.49.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:854941c2267fd4fc5b2ce02b8af8ecdffa79fb7784591d3a89370322039ea09f", size = 59890659, upload-time = "2026-08-11T16:23:03.359Z" }, + { url = "https://files.pythonhosted.org/packages/4f/dc/fe880ac1eb93c09b6c9a0539ad18c98778386978a0e20a13a55788044ad2/llvmlite-0.49.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da7b64474ac15ca595efa2644d5c6836638ccf70709fad3aba3fc56a55966928", size = 58344482, upload-time = "2026-08-11T16:23:12.122Z" }, + { url = "https://files.pythonhosted.org/packages/59/f6/5c18be29145cfca1d9e859e55a3c586a8c5a821825017b04c7999cd166c9/llvmlite-0.49.0-cp310-cp310-win_amd64.whl", hash = "sha256:b352c14353330c879e339b8f8d7491d565fe94242697714a24e80bd757202384", size = 41865252, upload-time = "2026-08-11T16:23:20.532Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d0/ab52de2328e97ca96cdf0331a5f774796bddc420a51768f4501193f80cbb/llvmlite-0.49.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:4b0e710880b7cc910392bd6b9f1bbf468fed99b182e4420d51598f36114b3dce", size = 40479230, upload-time = "2026-08-11T16:23:28.744Z" }, + { url = "https://files.pythonhosted.org/packages/1f/80/0989432d12b7c86a6f5f380eb92eca7de779af9b34dedbd311b694d7da8d/llvmlite-0.49.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a8c0fc9d624bdc30a3d2db11eb2fb98f80fb209d20b37604eda516cd9b699cf4", size = 59890659, upload-time = "2026-08-11T16:23:37.346Z" }, + { url = "https://files.pythonhosted.org/packages/58/e9/76859ca36aaa460b6ae0508e01637f0e9bdb9b59faaa4637ade3b94bbcca/llvmlite-0.49.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20496a5c9fdb8179fb9300e7d19f6782555d98aeeb4a322264aa7fd99f980618", size = 58344482, upload-time = "2026-08-11T16:23:44.199Z" }, + { url = "https://files.pythonhosted.org/packages/7d/49/47cd23e05d52d117b6119871ec299adedc9d8d332a2296964d9b2adc06d9/llvmlite-0.49.0-cp311-cp311-win_amd64.whl", hash = "sha256:6a5b06c1b5fc4ae4c9b169b065f42b719448ef1f873687ef224ef69969b75ec3", size = 41865253, upload-time = "2026-08-11T16:23:50.198Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ae/3f699ebe3590e15e023a6372dd147526fd8ec398aacf9ceb844e854964a8/llvmlite-0.49.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:b541c8fac3450db7574d1f53cf9dff83f285bfed9d69bf81fe71fc2a7d4f97fe", size = 40479231, upload-time = "2026-08-11T16:23:56.773Z" }, + { url = "https://files.pythonhosted.org/packages/be/3c/e97f69c62a2d972066d9a2612ce1f3de313035ac897a5b9f787cad8b55f7/llvmlite-0.49.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6acba646d88abbc87d5c113a3d62c1fbf8b8fee11c6493f516803e30f21ae870", size = 59890658, upload-time = "2026-08-11T16:24:05.451Z" }, + { url = "https://files.pythonhosted.org/packages/69/e6/e942ee08605fc0526ff3854260c384d8315a5830e16c4c2a5aebc14dc9bf/llvmlite-0.49.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4ec8ad805e7515cb8440a690eb3cef4d34acb29eef80b705ec4e1c1ad3c43c68", size = 58344481, upload-time = "2026-08-11T16:24:13.503Z" }, + { url = "https://files.pythonhosted.org/packages/84/49/2a44871cac6b5a2fd4aabd68cfdaf6de9a5c7edb36dee5d47b77bda4b50f/llvmlite-0.49.0-cp312-cp312-win_amd64.whl", hash = "sha256:3a9c9e3af4e214acfefa4f73ebe7bc3fb35854a62b654edb3953f5ae33c08ba3", size = 41865543, upload-time = "2026-08-11T16:24:20.41Z" }, + { url = "https://files.pythonhosted.org/packages/7d/85/0b536a3c59f2636d9dd51dda832b6c1d0ffec37608429dedf128664918f1/llvmlite-0.49.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:039fa4054a06f537fb39248d4472284ca96be311a142ec09e69f95630ab469cc", size = 40479230, upload-time = "2026-08-11T16:24:27.295Z" }, + { url = "https://files.pythonhosted.org/packages/8e/1c/ca8ba47b057b793099784475499771780ec46839f2782f753a7079d23520/llvmlite-0.49.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ddc7aecd4f56397ed6e8f120ec5dcd5a1a8f0e6032ca4af413462792d4dca2e3", size = 59890659, upload-time = "2026-08-11T16:24:35.595Z" }, + { url = "https://files.pythonhosted.org/packages/de/af/9526dfdd33a923f33e29a18b8f9801ee7ee4b7397e88d28192c1024c4a75/llvmlite-0.49.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d3dee64784201b64c13a8df62c48a4f4218858faaa65889866bb29bdc243c038", size = 58344482, upload-time = "2026-08-11T16:24:45.79Z" }, + { url = "https://files.pythonhosted.org/packages/96/7f/9f5afcf6476b228d6b170408f377a0c4f91477fc1fc91f8141088b45bf46/llvmlite-0.49.0-cp313-cp313-win_amd64.whl", hash = "sha256:a1b414dc6b164738ec39dd8987cea73829057b7dd92fc6d91b52838385fc1dd2", size = 41865544, upload-time = "2026-08-11T16:24:53.962Z" }, + { url = "https://files.pythonhosted.org/packages/25/d1/16599b8c9f21802448059482eab48a9e74086dc56b901a677ba355565e64/llvmlite-0.49.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:80a84683d04516bb51da1bbeebddaf2c2f558809c93078a8f91807909ae331f8", size = 40479230, upload-time = "2026-08-11T16:25:01.513Z" }, + { url = "https://files.pythonhosted.org/packages/3a/61/0b23849141a4c4e7091fcd158ebb45195896974bebca3e58fee7cad4b4f4/llvmlite-0.49.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4281a0171d66d2098adce4ba706b8c550b1b10718650f682d64cde16e84e4de5", size = 59890659, upload-time = "2026-08-11T16:25:08.733Z" }, + { url = "https://files.pythonhosted.org/packages/c4/92/628692b74b31e27af9ba7e8ba651941ee4956959d5478123c453f59aad4a/llvmlite-0.49.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b095f15fb12c4d90495df5b1a3772b4732cc408398b204a787dbedd370e09c69", size = 58344479, upload-time = "2026-08-11T16:25:15.731Z" }, + { url = "https://files.pythonhosted.org/packages/96/8a/412fc273521b02cbfe0b5f8ad56cc696385f6eaeecdb9e9ae6a90111d98d/llvmlite-0.49.0-cp314-cp314-win_amd64.whl", hash = "sha256:294e2f0b70aef8f92d0ae7b203e2609f08beb39437eee73de59a21669331aae9", size = 42986588, upload-time = "2026-08-11T16:25:22.534Z" }, + { url = "https://files.pythonhosted.org/packages/fc/15/f47cf45c00c8b165ac3d268502dcb21d900e86f27fd338268a66ce922ab0/llvmlite-0.49.0-cp314-cp314-win_arm64.whl", hash = "sha256:95d1071023ed858b79f6971954fd7cc1f5dbcbab987718a4ccbe1411e47d0b81", size = 37441881, upload-time = "2026-08-11T16:25:28.324Z" }, + { url = "https://files.pythonhosted.org/packages/e8/2e/eafd488766d1c02413cba24f7b22acb9b3ccdfd8407e98d30eb16bac4e2a/llvmlite-0.49.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:f3f2ff0aeb17d34fcce9f79b99baac441cfd3efa41b83e233ca4530a72381f72", size = 40479230, upload-time = "2026-08-11T16:25:35.125Z" }, + { url = "https://files.pythonhosted.org/packages/98/07/a2c4f04e2111ccc274b4d5e3331398a9dcf6d6e5e55d6444b1ad9d6381cf/llvmlite-0.49.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d5555ea1d63928481cbf7fcb1d67452b216c7e5b393a4eb7aa1401e67f2a4fc4", size = 59890658, upload-time = "2026-08-11T16:25:43.294Z" }, + { url = "https://files.pythonhosted.org/packages/80/f9/7b7b50f80b4585bcd78675ff3110c256877b11df32a8cde284f851762f57/llvmlite-0.49.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32adb84fdaae28aeb86fdb6253084ee707ee157289a2e98fe3caf48a62bee82", size = 58344482, upload-time = "2026-08-11T16:25:51.527Z" }, + { url = "https://files.pythonhosted.org/packages/d8/c6/32d68bfbf1d0c36888530ef6fd72864861af23dc546302b41033471a8c3a/llvmlite-0.49.0-cp314-cp314t-win_amd64.whl", hash = "sha256:be637e465010bc9c50f070468f7f1cf5385e92fee364d192dd5e6cea790ecba9", size = 42986602, upload-time = "2026-08-11T16:25:57.69Z" }, +] + +[[package]] +name = "manim" +version = "0.19.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +dependencies = [ + { name = "av", version = "13.1.0", source = { registry = "https://pypi.org/simple" } }, + { name = "beautifulsoup4" }, + { name = "click" }, + { name = "cloup" }, + { name = "decorator" }, + { name = "isosurfaces" }, + { name = "manimpango" }, + { name = "mapbox-earcut" }, + { name = "moderngl" }, + { name = "moderngl-window" }, + { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" } }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } }, + { name = "pillow" }, + { name = "pycairo" }, + { name = "pydub" }, + { name = "pygments" }, + { name = "rich" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" } }, + { name = "screeninfo" }, + { name = "skia-pathops" }, + { name = "srt" }, + { name = "svgelements" }, + { name = "tqdm" }, + { name = "typing-extensions" }, + { name = "watchdog" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8c/5f/b69881c389032d7da2666bee0a022b6535eefc7cd634bc3e55c213d6cb8c/manim-0.19.1.tar.gz", hash = "sha256:6be7da7e973b212739d2fbfe6af4501068b8a63fdd9407660a609ae12e57b580", size = 543401, upload-time = "2025-12-01T07:21:29.704Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/e1/c02f07aa65b9c8c9f70c0ccd47c2191f27e3c7788653d365945c63fb9eb5/manim-0.19.1-py3-none-any.whl", hash = "sha256:f7201bbdc4b4f9c649cd3a13ecdf98345057dd2810227517738b523c07927228", size = 642273, upload-time = "2025-12-01T07:21:31.271Z" }, +] + +[[package]] +name = "manim" +version = "0.21.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13'", + "python_full_version == '3.12.*'", + "python_full_version == '3.11.*'", +] +dependencies = [ + { name = "audioop-lts", marker = "python_full_version >= '3.13'" }, + { name = "av", version = "18.1.0", source = { registry = "https://pypi.org/simple" } }, + { name = "beautifulsoup4" }, + { name = "click" }, + { name = "cloup" }, + { name = "decorator" }, + { name = "isosurfaces" }, + { name = "manimpango" }, + { name = "mapbox-earcut" }, + { name = "moderngl" }, + { name = "moderngl-window" }, + { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" } }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "2.5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "pillow" }, + { name = "pycairo" }, + { name = "pydub" }, + { name = "pygments" }, + { name = "rich" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "scipy", version = "1.18.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "screeninfo" }, + { name = "skia-pathops" }, + { name = "srt" }, + { name = "svgelements" }, + { name = "tqdm" }, + { name = "typing-extensions" }, + { name = "watchdog" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/2e/08d46ab1eb765f054cc9e4f64e95896d6fc53642c8f0e29c6c5c29028fe7/manim-0.21.0.tar.gz", hash = "sha256:2f3d719e726c9abf49b1bea503fb17510ee2063dab53915ab44a2c47c32f9a13", size = 580380, upload-time = "2026-08-10T21:58:34.544Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/b9/52a764c6fd593055cd401ca1f5bae9701e0fca750ae7627d38a48869fc32/manim-0.21.0-py3-none-any.whl", hash = "sha256:021001097bb568372fa8f5d1dcf46ba085c8502c10efb2312ff45f19f7facb60", size = 679365, upload-time = "2026-08-10T21:58:32.824Z" }, +] + +[[package]] +name = "manimpango" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/55/d360e73eb4d04b102cef399ddebcba486a9b6c1977a26fe710beffd52e95/manimpango-0.6.1.tar.gz", hash = "sha256:59a00bbf8e99dab5f94341087c88e609fe946e79724627429cf59da84cbd40bf", size = 4080834, upload-time = "2025-10-23T06:04:48.564Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/67/aafe619a0b24a5d085cd8f3332083cc7a3fb5150a187b713a4d46cadaf3a/manimpango-0.6.1-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:237da434f9d9296ae06c29ebcefe0f9d7a70abbc5806def3de438af2a10c62e5", size = 7754340, upload-time = "2025-10-23T06:04:07.414Z" }, + { url = "https://files.pythonhosted.org/packages/33/37/35e6bbf23b8d5bf619e6b99c0d70532cd024f31ab295d337ed1f518af3aa/manimpango-0.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9f955c62df4719832062bc0148c2c47641658ed124980a5bf36b3bae2f621c5b", size = 7064696, upload-time = "2025-10-23T06:04:09.282Z" }, + { url = "https://files.pythonhosted.org/packages/ac/97/2e2910a7665872418760980ec3f4310d27363ff697e59e2fb2673f8e77e4/manimpango-0.6.1-cp310-cp310-win32.whl", hash = "sha256:674bc10ff569ec1d1f5f9a2a09e2cf565c1c62e4e926ced4f1761af47db2d376", size = 3628301, upload-time = "2025-10-23T06:04:10.986Z" }, + { url = "https://files.pythonhosted.org/packages/59/82/58d800fa5261a7822bac56fc272dd8f504963c7ff26156b1deb6ab72683b/manimpango-0.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:bdb923fd4548344246076b47d6bda4327a0a4c2164bcfebb5295552d925eaaed", size = 4202858, upload-time = "2025-10-23T06:04:12.588Z" }, + { url = "https://files.pythonhosted.org/packages/c1/fb/3aaddb9eeff88c9b4343faae54a0188f090c1806b64cd5fc15286fe4683b/manimpango-0.6.1-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:6c9e43b38d516287116e1a95577366d338e3b895491707783fef9d0dcce910a3", size = 7753953, upload-time = "2025-10-23T06:04:14.135Z" }, + { url = "https://files.pythonhosted.org/packages/4a/c7/0c9296ffa7770ddcb24c88b9c4aebc45dd2971d47e31df2fa810d99e3fca/manimpango-0.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c2c7c04dc080f64fa8f22e33b8085050e04aa89ca147b0350ddb277a7a620cf5", size = 7064562, upload-time = "2025-10-23T06:04:15.865Z" }, + { url = "https://files.pythonhosted.org/packages/78/72/91bf645cd71dff74d7a3e3d650465ce956024d3eadbb202ff6dab0c8feb7/manimpango-0.6.1-cp311-cp311-win32.whl", hash = "sha256:87500d54800e85bf9ccec0beee4cc26c7e9b62ae58e53e41e1155f3b0d5dce66", size = 3626993, upload-time = "2025-10-23T06:04:17.299Z" }, + { url = "https://files.pythonhosted.org/packages/62/c1/fc116c73e6b1b2dabb9c70996d8b01b3dfa70b06f4c6ac28e21e157d9c4d/manimpango-0.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:c9576c8fd8072e9a204baf67bcf609ed94677a0a824a06dcfab508759bbb89cb", size = 4203812, upload-time = "2025-10-23T06:04:19.2Z" }, + { url = "https://files.pythonhosted.org/packages/27/1c/440a6cb359b2b0fd2ba54f615af3b8d84d26f66a1e3a72ed70e812ffeedc/manimpango-0.6.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2039787f156a6ce2135c2ebccd4199a1859b45945e7b8d2bf46423ae1c3bcf11", size = 7754764, upload-time = "2025-10-23T06:04:21.075Z" }, + { url = "https://files.pythonhosted.org/packages/d3/6b/14c90dd8bce1f98fc4f4bbc708985b10106a793b25cfac9df3d7bd26396e/manimpango-0.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:412a876be8221aaf76a80401746ed7f42865779ac6f5ad5a220ff935bbc7e455", size = 7065699, upload-time = "2025-10-23T06:04:23.003Z" }, + { url = "https://files.pythonhosted.org/packages/ce/85/ddf7bdb96660c63b07306c8b074444f30d7f233dbec206a99e74828f926d/manimpango-0.6.1-cp312-cp312-win32.whl", hash = "sha256:81dd186c5cabd682afe56d43e88f7438bffd093061f858eb95bf8a773bfc2000", size = 3626854, upload-time = "2025-10-23T06:04:24.65Z" }, + { url = "https://files.pythonhosted.org/packages/5e/ef/dc48832fa8d2d867ba88a9dade038a148aaf9c3e4bf5f4539e3969f79560/manimpango-0.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:5bf98ec864c3ade022211f88dd22e6da9d72f1b1ba7b20091b149e45a5e0d5ba", size = 4203288, upload-time = "2025-10-23T06:04:26.129Z" }, + { url = "https://files.pythonhosted.org/packages/23/ee/01cd6b3087a5ead9483add3023f46c135bddd7773d506b771b44bbe0830b/manimpango-0.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:79aafe6373cb31dcdd1ee505407ec2bea18e74aed877f93dcd27ec9d88584472", size = 7751685, upload-time = "2025-10-23T06:04:28.457Z" }, + { url = "https://files.pythonhosted.org/packages/70/93/905ac20a9190655870131940dadb5121645cf8a23e6ea0f04be7f4604f2e/manimpango-0.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1049bda27ca504d24b48bec21756f4d8a3310ea7bc690167c54d05e97b8f7639", size = 7062629, upload-time = "2025-10-23T06:04:30.059Z" }, + { url = "https://files.pythonhosted.org/packages/76/88/b2de7d2d3a0331cf79992f8c6da0f0abd873d4ec9f024cad5992f1cf7a6d/manimpango-0.6.1-cp313-cp313-win32.whl", hash = "sha256:8da8238147b96737b36725b0fc386dc1dd7901516dfd7e73b2e7d34cd5d934f1", size = 3626705, upload-time = "2025-10-23T06:04:31.527Z" }, + { url = "https://files.pythonhosted.org/packages/3b/ed/a7a57491b26e8fb85ac0be9c39b53a6690f3c39beaf06d3715cb6916932b/manimpango-0.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:e90d6b926d0e673ce624963d44cc28c55a368b6074108ab885a43305baeae26c", size = 4203874, upload-time = "2025-10-23T06:04:32.981Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c0/318410215b409465a237c126a3e1f612c432d33b037b2f997870df327434/manimpango-0.6.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:318d14087901ee27ed6d0cfef0335aed57db217f8de831c8ca4e4c52b7805b45", size = 7751895, upload-time = "2025-10-23T06:04:34.874Z" }, + { url = "https://files.pythonhosted.org/packages/d1/83/f0a05e3e6312ca9673bc8b470c6a743cf2d0bd7f08b218efc993226e72e4/manimpango-0.6.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ffe2f8353857f0ca9a16894430d871051da450bc2d19beb969cf8b57b032f580", size = 7063387, upload-time = "2025-10-23T06:04:36.877Z" }, + { url = "https://files.pythonhosted.org/packages/13/76/0748323341647e8c338bd7af6fb527ffdc43d676003b57743740bdfcffcc/manimpango-0.6.1-cp314-cp314-win32.whl", hash = "sha256:c294ab801c8ffc217342dfeff7aeca97cbea86cd8b4e6e8c9a8d7da020b820e1", size = 3737933, upload-time = "2025-10-23T06:04:38.775Z" }, + { url = "https://files.pythonhosted.org/packages/2b/e9/42b995549cfd137780045e44b3bfba2cb1247b18bd2fb9f97079048e78ac/manimpango-0.6.1-cp314-cp314-win_amd64.whl", hash = "sha256:d8b0ee675eb30bbb67097cecb2be469847c3ccf574291f2bd43360caeb5f62ed", size = 4357908, upload-time = "2025-10-23T06:04:40.641Z" }, +] + +[[package]] +name = "mapbox-earcut" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "numpy", version = "2.5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/7b/bbf6b00488662be5d2eb7a188222c264b6f713bac10dc4a77bf37a4cb4b6/mapbox_earcut-2.0.0.tar.gz", hash = "sha256:81eab6b86cf99551deb698b98e3f7502c57900e5c479df15e1bdaf1a57f0f9d6", size = 39934, upload-time = "2025-11-16T18:41:27.251Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/d3/5222339a8fad091bf64f2e3041e48606d69d69f0609a7632ca17a8a05d5a/mapbox_earcut-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:c9a1dab7529f8e54bdb377f908e56f1e2b9a7e27ed168c64d3c7c38ed04ac201", size = 55920, upload-time = "2025-11-16T18:40:09.254Z" }, + { url = "https://files.pythonhosted.org/packages/19/e4/88d06e83ab75db2f4ae140a1e03ad8f84b02ac8af585dd61108aba73b8ed/mapbox_earcut-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5e5953098ea198253c8a40e2f282ca5b04d50ec2b9661e20c4cd2b2be39f0bb0", size = 52557, upload-time = "2025-11-16T18:40:10.536Z" }, + { url = "https://files.pythonhosted.org/packages/22/88/abefd244ea049e42334c5f7a9e3b58f4ec3c84d063119ba3c8d27ff31932/mapbox_earcut-2.0.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:efe5fd5de409e3b6d13907e73f295c8f1d63bdb6b8ca155dde4c93865796eafe", size = 56950, upload-time = "2025-11-16T18:40:11.905Z" }, + { url = "https://files.pythonhosted.org/packages/3c/e2/11122fddd086b930502eb4a954735da0f75e9d658fdab2d9e5914b9ebd2a/mapbox_earcut-2.0.0-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd04da6edbca1dd68ddbfac2398a95c763f35d7317fed227fde5b3aff1253b18", size = 59618, upload-time = "2025-11-16T18:40:13.017Z" }, + { url = "https://files.pythonhosted.org/packages/e8/fd/e62195729daa3111fe95404a99c7a6b3aa174800373d10111b7e7278a789/mapbox_earcut-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8bdb8881e857d6d9277df696e9cfb8749c00d6162021d9359cba9da58dfdd4f5", size = 153021, upload-time = "2025-11-16T18:40:14.294Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6a/d39ebaaa9010ea6c9f4d468f8812b1a1b31a40fba4f02ff29bc1bf321c30/mapbox_earcut-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6e2d1bf5af90d5857955775b4d8ea15b02e172f2a8f194bba50ff95f8ff3e80e", size = 157736, upload-time = "2025-11-16T18:40:16.344Z" }, + { url = "https://files.pythonhosted.org/packages/20/00/6a59cdb8d8c1bf7e3cc92f0404f68fdb1a3cb0bbb0837af0dbb93d6290a6/mapbox_earcut-2.0.0-cp310-cp310-win32.whl", hash = "sha256:5b0aa63dd890d712343095b05eb7b60e071912ad3ced1fc4187d6a6a739677bc", size = 51564, upload-time = "2025-11-16T18:40:17.852Z" }, + { url = "https://files.pythonhosted.org/packages/bc/7b/af69669c959d8f7fd1bd49c15deace2360bf6a79dad7bf9f7a7f1c137da6/mapbox_earcut-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b1355f13af89ea815b32f59a5455db295c965d51ab501bde0459cddc010a7149", size = 56793, upload-time = "2025-11-16T18:40:18.953Z" }, + { url = "https://files.pythonhosted.org/packages/07/9f/fbd15d9e348e75e986d6912c4eab99888106b7e5fb0a01e765422f7cd464/mapbox_earcut-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:9b5040e79e3783295e99c90277f31c1cbaddd3335297275331995ba5680e3649", size = 55773, upload-time = "2025-11-16T18:40:20.045Z" }, + { url = "https://files.pythonhosted.org/packages/72/40/be761298704fbbaa81c5618bb306f1510fb068e482f6a1c8b3b6c1b31479/mapbox_earcut-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1cf43baafec3ef1e967319d9b5da96bc6ddf3dbb204b6f3535275eda4b519a72", size = 52444, upload-time = "2025-11-16T18:40:21.501Z" }, + { url = "https://files.pythonhosted.org/packages/5a/0b/0c0c08db9663238ffb82c48259582dc0047a3255d98c0ac83c48026b7544/mapbox_earcut-2.0.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a283531847f603dd9d69afb75b21bd009d385ca9485fcd3e5a7fa5db1ccd913", size = 56803, upload-time = "2025-11-16T18:40:22.891Z" }, + { url = "https://files.pythonhosted.org/packages/f0/4a/86796859383d7d11fa5d4bcf1983f94c6cbb9eeb60fb3bab527fec4b32fa/mapbox_earcut-2.0.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ab697676f4cec4572d4e941b7a3429a6687bf2ac6e8db3f3781024e3239ae3a0", size = 59403, upload-time = "2025-11-16T18:40:24.021Z" }, + { url = "https://files.pythonhosted.org/packages/6c/db/adaf981ab3bcfcf993ef317636b1f27210d6834bb1e8d63db6ad7c08214a/mapbox_earcut-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f1bdac76e048f4299accf4eaf797079ddfc330442e7231c15535ed198100d6c5", size = 152876, upload-time = "2025-11-16T18:40:25.588Z" }, + { url = "https://files.pythonhosted.org/packages/d2/83/86417974039e7554c9e1e55c852a7e9c2a1390d64675eb85d70e5fa7eb37/mapbox_earcut-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4a6945b23f859bef11ce3194303d17bd371c86b637e7029f81b1feaff3db3758", size = 157548, upload-time = "2025-11-16T18:40:27.202Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4c/c82a292bb21e5c651d81334123db2d654c5c9d19b2197080d3429dc1e49a/mapbox_earcut-2.0.0-cp311-cp311-win32.whl", hash = "sha256:8e119524c29406afb5eaa15e933f297d35679293a3ca62ced22f97a14c484cb5", size = 51424, upload-time = "2025-11-16T18:40:28.415Z" }, + { url = "https://files.pythonhosted.org/packages/30/57/6c39d7db81f72a3e4814ef152c8fb8dfe275dc4b03c9bfa073d251e3755f/mapbox_earcut-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:378bbbb3304e446023752db8f44ecd6e7ef965bcbda36541d2ae64442ba94254", size = 56662, upload-time = "2025-11-16T18:40:29.863Z" }, + { url = "https://files.pythonhosted.org/packages/f4/d6/a1ef6e196b3d6968bf6546d4f7e54c559f9cff8991fdb880df0ba1618f52/mapbox_earcut-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:6d249a431abd6bbff36f1fd0493247a86de962244cc4081b4d5050b02ed48fb1", size = 50505, upload-time = "2025-11-16T18:40:30.992Z" }, + { url = "https://files.pythonhosted.org/packages/8d/93/846804029d955c3c841d8efff77c2b0e8d9aab057d3a077dc8e3f88b5ea4/mapbox_earcut-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:db55ce18e698bc9d90914ee7d4f8c3e4d23827456ece7c5d7a1ec91e90c7122b", size = 55623, upload-time = "2025-11-16T18:40:32.113Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f6/cc9ece104bc3876b350dba6fef7f34fb7b20ecc028d2cdbdbecb436b1ed1/mapbox_earcut-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:01dd6099d16123baf582a11b2bd1d59ce848498cf0cdca3812fd1f8b20ff33b7", size = 52028, upload-time = "2025-11-16T18:40:33.516Z" }, + { url = "https://files.pythonhosted.org/packages/88/6e/230da4aabcc56c99e9bddb4c43ce7d4ba3609c0caf2d316fb26535d7c60c/mapbox_earcut-2.0.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d5a098aae26a52282bc981a38e7bf6b889d2ea7442f2cd1903d2ba842f4ff07", size = 56351, upload-time = "2025-11-16T18:40:35.217Z" }, + { url = "https://files.pythonhosted.org/packages/1a/f7/5cdd3752526e91d91336c7263af7767b291d21e63c89d7190a60051f0f87/mapbox_earcut-2.0.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de35f241d0b9110ad9260f295acedd9d7cc0d7acfe30d36b1b3ee8419c2caba1", size = 59209, upload-time = "2025-11-16T18:40:36.634Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a2/b7781416cb93b37b95d0444e03f87184de8815e57ff202ce4105fa921325/mapbox_earcut-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6cb63ab85e2e430c350f93e75c13f8b91cb8c8a045f3cd714c390b69a720368a", size = 152316, upload-time = "2025-11-16T18:40:38.147Z" }, + { url = "https://files.pythonhosted.org/packages/c1/74/396338e3d345e4e36fb23a0380921098b6a95ce7fb19c4777f4185a5974e/mapbox_earcut-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fb3c9f069fc3795306db87f8139f70c4f047532f897a3de05f54dc1faebc97f6", size = 157268, upload-time = "2025-11-16T18:40:39.753Z" }, + { url = "https://files.pythonhosted.org/packages/56/2c/66fd137ea86c508f6cd7247f7f6e2d1dabffc9f0e9ccf14c71406b197af1/mapbox_earcut-2.0.0-cp312-cp312-win32.whl", hash = "sha256:eb290e6676217707ed238dd55e07b0a8ca3ab928f6a27c4afefb2ff3af08d7cb", size = 51226, upload-time = "2025-11-16T18:40:41.018Z" }, + { url = "https://files.pythonhosted.org/packages/b8/84/7b78e37b0c2109243c0dad7d9ba9774b02fcee228bf61cf727a5aa1702e2/mapbox_earcut-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:5ef5b3319a43375272ad2cad9333ed16e569b5102e32a4241451358897e6f6ee", size = 56417, upload-time = "2025-11-16T18:40:42.173Z" }, + { url = "https://files.pythonhosted.org/packages/75/7f/cd7195aa27c1c8f2b9d38025a5a8663f32cd01c07b648a54b1308ab26c15/mapbox_earcut-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:a4a3706feb5cc8c782d8f68bb0110c8d551304043f680a87a54b0651a2c208c3", size = 50111, upload-time = "2025-11-16T18:40:43.334Z" }, + { url = "https://files.pythonhosted.org/packages/8b/7c/c5dd5b255b9828ba5df729e62fdd470a322c938f07ef392ca03c0592bb3a/mapbox_earcut-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:582329a81bd36cf0f82e443c395bcb8cfdb10caddafec76acaebac7c20bf1c31", size = 55619, upload-time = "2025-11-16T18:40:44.44Z" }, + { url = "https://files.pythonhosted.org/packages/1a/3f/03f23eac9831e7d0d8da3d6993695a9a3724659c94e9997f6b7aaccc199d/mapbox_earcut-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d2ac5f610b3e44a3a0c4df06b5552d503b4f1c2c409eeca20dbe05112bd60955", size = 52023, upload-time = "2025-11-16T18:40:45.857Z" }, + { url = "https://files.pythonhosted.org/packages/39/f3/a92ccee494b3e437e4bd81ecd358e39d231dc90af010d6c43930506c10ad/mapbox_earcut-2.0.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:58cc88513b87734b243d86f0d3fb87e96e0a78d9abd8fd615c55f766dd63f949", size = 56357, upload-time = "2025-11-16T18:40:47.27Z" }, + { url = "https://files.pythonhosted.org/packages/03/30/e54ececd0403a5495c340b693075abec92a6d17dc44283b6cb059534f7ed/mapbox_earcut-2.0.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:40218d887798451932f3c335992834aa807c35cd497c6e0733470fdbd77f9521", size = 59215, upload-time = "2025-11-16T18:40:48.682Z" }, + { url = "https://files.pythonhosted.org/packages/6e/e1/8fbff13a074c1fbf702b30ce7ec4d878bc664d659c1c2b1697831f4ea3a8/mapbox_earcut-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:39fa5cfa0e855b028ec9b0200c88ebfa252448f343ce2f67b6fc07fe1f22a3ae", size = 152304, upload-time = "2025-11-16T18:40:49.85Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d5/c757030b3cb3a9f2278ded6f7312d2b9d3761db6f3da8d395f7f7303dd66/mapbox_earcut-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:476b558473b8a43f238d46e819bc0f830c427842ec5feb19e23b4dcac8ad2455", size = 157270, upload-time = "2025-11-16T18:40:51.093Z" }, + { url = "https://files.pythonhosted.org/packages/96/63/589c6decb1f032d8811f1066da552f0a718830f592e6d6539fa4c3c766b8/mapbox_earcut-2.0.0-cp313-cp313-win32.whl", hash = "sha256:8c2d125c182acbc490b39503c0dec4f937bae180d0849a26bcea0ee4a76024bd", size = 51207, upload-time = "2025-11-16T18:40:52.285Z" }, + { url = "https://files.pythonhosted.org/packages/76/75/a79a6020c46d4f07731e88ec5cc9324f6b43343aba835def1dc0bf59fecf/mapbox_earcut-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:e049e6a37c228d7a9cb2f54ae405aa21d35c5175d849530fb32064ddb38ad5ab", size = 56416, upload-time = "2025-11-16T18:40:53.474Z" }, + { url = "https://files.pythonhosted.org/packages/ce/5f/83e878c2b3e9e6db1f60b598a2cc5ed4c2b5bc8d281575c964869414a159/mapbox_earcut-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:8a833d73d63d4b6291bbd8b4d2f551e87f663282cdc547ecbbd9b423849ee996", size = 50103, upload-time = "2025-11-16T18:40:54.954Z" }, + { url = "https://files.pythonhosted.org/packages/96/fc/f1b74324c83f510213ff91eb8b1d2697ad5a12418c5fba966e80f1104a5f/mapbox_earcut-2.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ad1dc141797037b7d4c9d8d2e52b9665b36294913a8ec31008b282d1a95b9bdc", size = 55728, upload-time = "2025-11-16T18:40:56.098Z" }, + { url = "https://files.pythonhosted.org/packages/7b/59/053c04e29c4bd22157d3b6255f1e5c19c46cb7a594c4314298bdcbca723f/mapbox_earcut-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0f0f5c6f5ed8ffdce8efe6a003ba598089d0ee07eabd41868db183be50484f9f", size = 52063, upload-time = "2025-11-16T18:40:57.227Z" }, + { url = "https://files.pythonhosted.org/packages/a6/77/acc2d553c3bb8c769535a280545bb7d9608141e90511a2e6215a54611776/mapbox_earcut-2.0.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:82cd92775f37fd1e4b8464c5e74a00e87130eecc55ee3df2492b8ca2bdf6ef3e", size = 56522, upload-time = "2025-11-16T18:40:58.349Z" }, + { url = "https://files.pythonhosted.org/packages/1a/f5/627dd6defd3c1a2b3069e9e27482aa04d268c841735e576c1e22848a34f6/mapbox_earcut-2.0.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:626ffc1310e0cc8910283e4ac3139e5fb0458f18f2c4874162f66159951933ff", size = 59204, upload-time = "2025-11-16T18:41:00.095Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3e/819185542ab095ba1244ad65ececb3edcde6fd0111248a0f9318d695bfcf/mapbox_earcut-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ea951d764a356cad95b23fef950d8aa3b44b933795ad09d977fea7d4dbe377c3", size = 152550, upload-time = "2025-11-16T18:41:01.233Z" }, + { url = "https://files.pythonhosted.org/packages/a9/ad/85e0f815e4774b90ad6761bce55c80d13ee21b2a24014b0be0d5010b0049/mapbox_earcut-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:df1f217624abb5e02ecabcbd84369de970b8d8bc1e4e7c164c1cfcaddad76ca3", size = 157322, upload-time = "2025-11-16T18:41:02.866Z" }, + { url = "https://files.pythonhosted.org/packages/27/4c/0f56369e7a000d2f3177d17baf34263559b206ae524fcd0c4c5d1d960dab/mapbox_earcut-2.0.0-cp314-cp314-win32.whl", hash = "sha256:6fa61307d38b50fc9bd5449c00dbae46d270a32b372c6fc3b8af4b85c85746e4", size = 52916, upload-time = "2025-11-16T18:41:04.122Z" }, + { url = "https://files.pythonhosted.org/packages/c2/9d/8c557dd9b3d9fe2344f5bd5ff3bb0b2a42ed6addb7e43ca4358051743b04/mapbox_earcut-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:0da20ed3c81b240450118773bcedfac34e70a56998f66147222c46f4356fff67", size = 57713, upload-time = "2025-11-16T18:41:05.204Z" }, + { url = "https://files.pythonhosted.org/packages/3b/ec/678c5553938d3a29d02dd41dd898672267f054afc4e2821958dee6ec86ce/mapbox_earcut-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:847e74bd5878e4c64793dc100f9288f5443f87c55c3fe391fd90509029136ff6", size = 51872, upload-time = "2025-11-16T18:41:06.323Z" }, + { url = "https://files.pythonhosted.org/packages/18/37/94f2d973669cbfef811e536713fe56ec012ba74e5f8795a832337b1866a3/mapbox_earcut-2.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ddc9e7175fc903185c64afbbf91febee56b50787dd0962fce2bfb4f20cf80d27", size = 56447, upload-time = "2025-11-16T18:41:07.443Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1c/e0afcc82659cc1727a7e59c4f9e9880bbc3f048a4a5325772b44d4a91dfd/mapbox_earcut-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6dc8a7568066af9a858018d6d92b7e77e164578f9fcd79093f1cbe4ec203461b", size = 53154, upload-time = "2025-11-16T18:41:08.618Z" }, + { url = "https://files.pythonhosted.org/packages/6c/2d/9845281c8c35da2bea733b8c2df5b9fe694e73e7b05fe8a1d4c3c439a1bc/mapbox_earcut-2.0.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6abc5340edd9b433ab2dab2ee033082a199d5c51cce445124626c0040ec0d81b", size = 56285, upload-time = "2025-11-16T18:41:09.728Z" }, + { url = "https://files.pythonhosted.org/packages/97/8e/eeea762a519490662b8f480e2b35bf03701b0bcc5a446b62a4c5a1500b06/mapbox_earcut-2.0.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df7afdd8078a9aa28f469d9242531d304e09a4b14e514f048e021a949f3777b4", size = 58601, upload-time = "2025-11-16T18:41:10.872Z" }, + { url = "https://files.pythonhosted.org/packages/b9/67/932f80aa6af9bc1a317b6119052c74f327d81e00b457003a049e324b810c/mapbox_earcut-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1a286f73e612a46cafd6d6c843365265090517af16823e2f37277c13cd8b6f09", size = 154924, upload-time = "2025-11-16T18:41:12.104Z" }, + { url = "https://files.pythonhosted.org/packages/87/38/5db4a91f9f90cbb447be61da5468a2955fad3a840ae4c7dbde789b09d45a/mapbox_earcut-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8d081fe1d00dc553e3e68c02fc395324aad0d8ed955f3ff59289264c9b21ace4", size = 159194, upload-time = "2025-11-16T18:41:13.364Z" }, + { url = "https://files.pythonhosted.org/packages/6b/03/de3843b13fe854a010fb2f8b25551d4d5fe1c879ff2e7c8d7d8d7d735a8e/mapbox_earcut-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:13049ca96431bbc7ef7fd7780dd1872209ca11a5c1977f7aa91a1b574a8af863", size = 54143, upload-time = "2025-11-16T18:41:14.564Z" }, + { url = "https://files.pythonhosted.org/packages/9a/89/fbdee5a56ba51df9be6098b5428636ad75aa994e98d8bec6113d5cba401e/mapbox_earcut-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6ace78e4fdba3b8cbb7768d44d77a981698305862a07f94bbb6f5cc16659adb4", size = 60833, upload-time = "2025-11-16T18:41:15.694Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, +] + +[[package]] +name = "matplotlib" +version = "3.10.9" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +dependencies = [ + { name = "contourpy", version = "1.3.2", source = { registry = "https://pypi.org/simple" } }, + { name = "cycler" }, + { name = "fonttools" }, + { name = "kiwisolver" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } }, + { name = "packaging" }, + { name = "pillow" }, + { name = "pyparsing" }, + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/1b/4be5be87d43d327a0cf4de1a56e86f7f84c89312452406cf122efe2839e6/matplotlib-3.10.9.tar.gz", hash = "sha256:fd66508e8c6877d98e586654b608a0456db8d7e8a546eb1e2600efd957302358", size = 34811233, upload-time = "2026-04-24T00:14:13.539Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/6f/340b04986e67aac6f66c5145ce68bf72c64bed30f92c8913499a6e6b8f99/matplotlib-3.10.9-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77210dce9cb8153dffc967efaae990543392563d5a376d4dd8539bebcb0ed217", size = 8296625, upload-time = "2026-04-24T00:11:43.376Z" }, + { url = "https://files.pythonhosted.org/packages/bb/2f/127081eb83162053ebb9678ceac64220b93a663e0167432566e9c7c82aab/matplotlib-3.10.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1e7698ac9868428e84d2c967424803b2472ff7167d9d6590d4204ed775343c3b", size = 8188790, upload-time = "2026-04-24T00:11:46.556Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b7/d8bcec2626c35f96972bff656299fef4578113ea6193c8fdad324710410c/matplotlib-3.10.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1aa972116abb4c9d201bf245620b433726cb6856f3bef6a78f776a00f5c92d37", size = 8769389, upload-time = "2026-04-24T00:11:48.959Z" }, + { url = "https://files.pythonhosted.org/packages/12/49/b78e214a527ea732033b7f4d37f7afb504d74ba9d134bd47938230dfb8b1/matplotlib-3.10.9-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae2f11957b27ce53497dd4d7b235c4d4f1faf383dfb39d0c5beb833bff883294", size = 9589657, upload-time = "2026-04-24T00:11:51.915Z" }, + { url = "https://files.pythonhosted.org/packages/5f/15/5246f7b43beae19c74dfee651d58d6cc8112e06f77adb4e88cc04f2e3a23/matplotlib-3.10.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b049278ddce116aaa1c1377ebf58adea909132dfce0281cf7e3a1ea9fc2e2c65", size = 9651983, upload-time = "2026-04-24T00:11:54.766Z" }, + { url = "https://files.pythonhosted.org/packages/75/77/5acecfe672ba0fa1b8c0454f69ce155d1e6fc5852fa7206bf9afaf767121/matplotlib-3.10.9-cp310-cp310-win_amd64.whl", hash = "sha256:82834c3c292d24d3a8aae77cd2d20019de69d692a34a970e4fdb8d33e2ea3dda", size = 8199701, upload-time = "2026-04-24T00:11:58.389Z" }, + { url = "https://files.pythonhosted.org/packages/4c/8c/290f021104741fea63769c31494f5324c0cd249bf536a65a4350767b1f22/matplotlib-3.10.9-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:68cfdcede415f7c8f5577b03303dd94526cdb6d11036cecdc205e08733b2d2bb", size = 8306860, upload-time = "2026-04-24T00:12:01.207Z" }, + { url = "https://files.pythonhosted.org/packages/51/18/325cd32ece1120d1da51cc4e4294c6580190699490183fc2fe8cb6d61ec5/matplotlib-3.10.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfca0129678bd56379db26c52b5d77ed7de314c047492fbdc763aa7501710cfb", size = 8199254, upload-time = "2026-04-24T00:12:04.239Z" }, + { url = "https://files.pythonhosted.org/packages/79/db/e28c1b83e3680740aa78925f5fb2ae4d16207207419ad75ea9fe604f8676/matplotlib-3.10.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8e436d155fa8a3399dc62683f8f5d0e2e50d25d0144a73edd73f82eec8f4abfb", size = 8777092, upload-time = "2026-04-24T00:12:06.793Z" }, + { url = "https://files.pythonhosted.org/packages/55/fa/3ce7adfe9ba101748f465211660d9c6374c876b671bdb8c2bb6d347e8b94/matplotlib-3.10.9-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:56fc0bd271b00025c6edfdc7c2dcd247372c8e1544971d62e1dc7c17367e8bf9", size = 9595691, upload-time = "2026-04-24T00:12:09.706Z" }, + { url = "https://files.pythonhosted.org/packages/36/c4/6960a76686ed668f2c60f84e9799ba4c0d56abdb36b1577b60c1d061d1ec/matplotlib-3.10.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a5a6104ed666402ba5106d7f36e0e0cdca4e8d7fa4d39708ca88019e2835a2eb", size = 9659771, upload-time = "2026-04-24T00:12:12.766Z" }, + { url = "https://files.pythonhosted.org/packages/7e/0d/271aace3342157c64700c9ff4c59c7b392f3dbab393692e8db6fbe7ab96c/matplotlib-3.10.9-cp311-cp311-win_amd64.whl", hash = "sha256:d730e984eddf56974c3e72b6129c7ca462ac38dc624338f4b0b23eb23ecba00f", size = 8205112, upload-time = "2026-04-24T00:12:15.773Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ee/cb57ad4754f3e7b9174ce6ce66d9205fb827067e48a9f58ac09d7e7d6b77/matplotlib-3.10.9-cp311-cp311-win_arm64.whl", hash = "sha256:51bf0ddbdc598e060d46c16b5590708f81a1624cefbaaf62f6a81bf9285b8c80", size = 8132310, upload-time = "2026-04-24T00:12:18.645Z" }, + { url = "https://files.pythonhosted.org/packages/35/c6/5581e26c72233ebb2a2a6fed2d24fb7c66b4700120b813f51b0555acf0b6/matplotlib-3.10.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f0c3c28d9fbcc1fe7a03be236d73430cf6409c41fb2383a7ac52fe932b072cb1", size = 8319908, upload-time = "2026-04-24T00:12:21.323Z" }, + { url = "https://files.pythonhosted.org/packages/b7/18/4880dd762e40cd360c1bf06e890c5a97b997e91cb324602b1a19950ad5ce/matplotlib-3.10.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41cb28c2bd769aa3e98322c6ab09854cbcc52ab69d2759d681bba3e327b2b320", size = 8216016, upload-time = "2026-04-24T00:12:23.4Z" }, + { url = "https://files.pythonhosted.org/packages/32/91/d024616abdba99e83120e07a20658976f6a343646710760c4a51df126029/matplotlib-3.10.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ae20801130378b82d647ff5047c07316295b68dc054ca6b3c13519d0ea624285", size = 8789336, upload-time = "2026-04-24T00:12:26.096Z" }, + { url = "https://files.pythonhosted.org/packages/5c/04/030a2f61ef2158f5e4c259487a92ac877732499fb33d871585d89e03c42d/matplotlib-3.10.9-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6c63ebcd8b4b169eb2f5c200552ae6b8be8999a005b6b507ed76fb8d7d674fe2", size = 9604602, upload-time = "2026-04-24T00:12:29.052Z" }, + { url = "https://files.pythonhosted.org/packages/fc/c2/541e4d09d87bb6b5830fc28b4c887a9a8cf4e1c6cee698a8c05552ae2003/matplotlib-3.10.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d75d11c949914165976c621b2324f9ef162af7ebf4b057ddf95dd1dba7e5edcf", size = 9670966, upload-time = "2026-04-24T00:12:32.131Z" }, + { url = "https://files.pythonhosted.org/packages/04/a1/4571fc46e7702de8d0c2dc54ad1b2f8e29328dea3ee90831181f7353d93c/matplotlib-3.10.9-cp312-cp312-win_amd64.whl", hash = "sha256:d091f9d758b34aaaaa6331d13574bf01891d903b3dec59bfff458ef7551de5d6", size = 8217462, upload-time = "2026-04-24T00:12:35.226Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d0/2269edb12aa30c13c8bcc9382892e39943ce1d28aab4ec296e0381798e81/matplotlib-3.10.9-cp312-cp312-win_arm64.whl", hash = "sha256:10cc5ce06d10231c36f40e875f3c7e8050362a4ee8f0ee5d29a6b3277d57bb42", size = 8136688, upload-time = "2026-04-24T00:12:37.442Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d3/8d4f6afbecb49fc04e060a57c0fce39ea51cc163a6bd87303ccd698e4fa6/matplotlib-3.10.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b580440f1ff81a0e34122051a3dfabb7e4b7f9e380629929bde0eff9af72165f", size = 8320331, upload-time = "2026-04-24T00:12:39.688Z" }, + { url = "https://files.pythonhosted.org/packages/63/d9/9e14bc7564bf92d5ffa801ae5fac819ce74b925dfb55e3ebde61a3bbad3e/matplotlib-3.10.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b1b745c489cd1a77a0dc1120a05dc87af9798faebc913601feb8c73d89bf2d1e", size = 8216461, upload-time = "2026-04-24T00:12:42.494Z" }, + { url = "https://files.pythonhosted.org/packages/8a/17/4402d0d14ccf1dfc70932600b68097fbbf9c898a4871d2cbbe79c7801a32/matplotlib-3.10.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8f3bcac1ca5ed000a6f4337d47ba67dfddf37ed6a46c15fd7f014997f7bf865f", size = 8790091, upload-time = "2026-04-24T00:12:44.789Z" }, + { url = "https://files.pythonhosted.org/packages/3e/0b/322aeec06dd9b91411f92028b37d447342770a24392aa4813e317064dad5/matplotlib-3.10.9-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a8d66a55def891c33147ba3ba9bfcabf0b526a43764c818acbb4525e5ed0838", size = 9605027, upload-time = "2026-04-24T00:12:47.583Z" }, + { url = "https://files.pythonhosted.org/packages/74/88/5f13482f55e7b00bcfc09838b093c2456e1379978d2a146844aae05350ad/matplotlib-3.10.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d843374407c4017a6403b59c6c81606773d136f3259d5b6da3131bc814542cc2", size = 9671269, upload-time = "2026-04-24T00:12:50.878Z" }, + { url = "https://files.pythonhosted.org/packages/c5/e0/0840fd2f93da988ec660b8ad1984abe9f25d2aed22a5e394ff1c68c88307/matplotlib-3.10.9-cp313-cp313-win_amd64.whl", hash = "sha256:f4399f64b3e94cd500195490972ae1ee81170df1636fa15364d157d5bdd7b921", size = 8217588, upload-time = "2026-04-24T00:12:53.784Z" }, + { url = "https://files.pythonhosted.org/packages/47/b9/d706d06dd605c49b9f83a2aed8c13e3e5db70697d7a80b7e3d7915de6b17/matplotlib-3.10.9-cp313-cp313-win_arm64.whl", hash = "sha256:ba7b3b8ef09eab7df0e86e9ae086faa433efbfbdb46afcb3aa16aabf779469a8", size = 8136913, upload-time = "2026-04-24T00:12:56.501Z" }, + { url = "https://files.pythonhosted.org/packages/9b/45/6e32d96978264c8ca8c4b1010adb955a1a49cfaf314e212bbc8908f04a61/matplotlib-3.10.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:09218df8a93712bd6ea133e83a153c755448cf7868316c531cffcc43f69d1cc9", size = 8368019, upload-time = "2026-04-24T00:12:58.896Z" }, + { url = "https://files.pythonhosted.org/packages/86/0a/c8e3d3bba245f0f7fc424937f8ff7ef77291a36af3edb97ccd78aa93d84f/matplotlib-3.10.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:82368699727bfb7b0182e1aa13082e3c08e092fa1a25d3e1fd92405bff96f6d4", size = 8264645, upload-time = "2026-04-24T00:13:01.406Z" }, + { url = "https://files.pythonhosted.org/packages/3d/aa/5bf5a14fe4fed73a4209a155606f8096ff797aad89c6c35179026571133e/matplotlib-3.10.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3225f4e1edcb8c86c884ddf79ebe20ecd0a67d30188f279897554ccd8fded4dc", size = 8802194, upload-time = "2026-04-24T00:13:03.702Z" }, + { url = "https://files.pythonhosted.org/packages/dd/5e/b4be852d6bba6fd15893fadf91ff26ae49cb91aac789e95dde9d342e664f/matplotlib-3.10.9-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de2445a0c6690d21b7eb6ce071cebad6d40a2e9bdf10d039074a96ba19797b99", size = 9622684, upload-time = "2026-04-24T00:13:06.647Z" }, + { url = "https://files.pythonhosted.org/packages/4c/3d/ed428c971139112ef730f62770654d609467346d09d4b62617e1afd68a5a/matplotlib-3.10.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:b2b9516251cb89ff618d757daec0e2ed1bf21248013844a853d87ef85ab3081d", size = 9680790, upload-time = "2026-04-24T00:13:10.009Z" }, + { url = "https://files.pythonhosted.org/packages/e7/09/052e884aaf2b985c63cb79f715f1d5b6a3eaa7de78f6a52b9dbc077d5b53/matplotlib-3.10.9-cp313-cp313t-win_amd64.whl", hash = "sha256:e9fae004b941b23ff2edcf1567a857ed77bafc8086ffa258190462328434faf8", size = 8287571, upload-time = "2026-04-24T00:13:13.087Z" }, + { url = "https://files.pythonhosted.org/packages/f4/38/ae27288e788c35a4250491422f3db7750366fc8c97d6f36fbdecfc1f5518/matplotlib-3.10.9-cp313-cp313t-win_arm64.whl", hash = "sha256:6b63d9c7c769b88ab81e10dc86e4e0607cf56817b9f9e6cf24b2a5f1693b8e38", size = 8188292, upload-time = "2026-04-24T00:13:15.546Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e6/3bd8afd04949f02eabc1c17115ea5255e19cacd4d06fc5abdde4eeb0052c/matplotlib-3.10.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:172db52c9e683f5d12eaf57f0f54834190e12581fe1cc2a19595a8f5acb4e77d", size = 8321276, upload-time = "2026-04-24T00:13:18.318Z" }, + { url = "https://files.pythonhosted.org/packages/41/86/86231232fff41c9f8e4a1a7d7a597d349a02527109c3af7d618366122139/matplotlib-3.10.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97e35e8d39ccc85859095e01a53847432ba9a53ddf7986f7a54a11b73d0e143f", size = 8218218, upload-time = "2026-04-24T00:13:20.974Z" }, + { url = "https://files.pythonhosted.org/packages/85/8f/becc9722cafc64f5d2eb0b7c1bf5f585271c618a45dbd8fabeb021f898b6/matplotlib-3.10.9-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aba1615dabe83188e19d4f75a253c6a08423e04c1425e64039f800050a69de6b", size = 9608145, upload-time = "2026-04-24T00:13:23.228Z" }, + { url = "https://files.pythonhosted.org/packages/32/5d/f7e914f7d9325abff4057cee62c0fa70263683189f774473cbfb534cd13b/matplotlib-3.10.9-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34cf8167e023ad956c15f36302911d5406bd99a9862c1a8499ea6f7c0e015dc2", size = 9885085, upload-time = "2026-04-24T00:13:25.849Z" }, + { url = "https://files.pythonhosted.org/packages/a5/fd/fa69f2221534e80cc5772ac2b7d222011a2acafc2ec7216d5dd174c864ae/matplotlib-3.10.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:59476c6d29d612b8e9bb6ce8c5b631be6ba8f9e3a2421f22a02b192c7dd28716", size = 9672358, upload-time = "2026-04-24T00:13:28.906Z" }, + { url = "https://files.pythonhosted.org/packages/ab/1a/5a4f747a8b271cbb024946d2dd3c913ab5032ba430626f8c3528ada96b4b/matplotlib-3.10.9-cp314-cp314-win_amd64.whl", hash = "sha256:336b9acc64d309063126edcdaca00db9373af3c476bb94388fe9c5a53ad13e6f", size = 8349970, upload-time = "2026-04-24T00:13:31.904Z" }, + { url = "https://files.pythonhosted.org/packages/64/dc/95d60ecaefe30680a154b52ea96ab4b0dab547f1fd6aa12f5fb655e89cae/matplotlib-3.10.9-cp314-cp314-win_arm64.whl", hash = "sha256:2dc9477819ffd78ad12a20df1d9d6a6bd4fec6aaa9072681465fddca052f1456", size = 8272785, upload-time = "2026-04-24T00:13:34.511Z" }, + { url = "https://files.pythonhosted.org/packages/70/a0/005d68bc8b8418300ce6591f18586910a8526806e2ab663933d9f20a41e9/matplotlib-3.10.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:da4e09638420548f31c354032a6250e473c68e5a4e96899b4844cf39ddea23fe", size = 8367999, upload-time = "2026-04-24T00:13:36.962Z" }, + { url = "https://files.pythonhosted.org/packages/22/05/1236cc9290be70b2498af20ca348add76e3fffe7f67b477db5133a84f3ea/matplotlib-3.10.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:345f6f68ecc8da0ca56fad2ea08fde1a115eda530079eca185d50a7bc3e146c6", size = 8264543, upload-time = "2026-04-24T00:13:39.851Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c2/071f5a5ff6c5bd63aaaf2f45c811d9bf2ced94bde188d9e1a519e21d0cba/matplotlib-3.10.9-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4edcfbd8565339aa62f1cd4012f7180926fdbe71850f7b0d3c379c175cd6b66c", size = 9622800, upload-time = "2026-04-24T00:13:42.296Z" }, + { url = "https://files.pythonhosted.org/packages/95/57/da7d1f10a85624b9e7db68e069dd94e58dc41dbf9463c5921632ecbe3661/matplotlib-3.10.9-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6be157fe17fc37cb95ac1d7374cf717ce9259616edec911a78d9d26dae8522d4", size = 9888561, upload-time = "2026-04-24T00:13:45.026Z" }, + { url = "https://files.pythonhosted.org/packages/67/b2/ef8d6bb59b0edb6c16c968b70f548aa13b54348972def5aa6ac85df67145/matplotlib-3.10.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4e42042d54db34fda4e95a7bd3e5789c2a995d2dad3eb8850232ee534092fbbf", size = 9680884, upload-time = "2026-04-24T00:13:48.066Z" }, + { url = "https://files.pythonhosted.org/packages/61/1c/d21bfeb9931881ebe96bcfcff27c7ae4b160ae0ec291a714c42641a56d75/matplotlib-3.10.9-cp314-cp314t-win_amd64.whl", hash = "sha256:c27df8b3848f32a83d1767566595e43cfaa4460380974da06f4279a7ec143c39", size = 8432333, upload-time = "2026-04-24T00:13:51.008Z" }, + { url = "https://files.pythonhosted.org/packages/78/23/92493c3e6e1b635ccfff146f7b99e674808787915420373ac399283764c2/matplotlib-3.10.9-cp314-cp314t-win_arm64.whl", hash = "sha256:a49f1eadc84ca85fd72fa4e89e70e61bf86452df6f971af04b12c60761a0772c", size = 8324785, upload-time = "2026-04-24T00:13:53.633Z" }, + { url = "https://files.pythonhosted.org/packages/2c/2b/0e92ad0ac446633f928a1563db4aa8add407e1924faf0ded5b95b35afb27/matplotlib-3.10.9-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1872fb212a05b729e649754a72d5da61d03e0554d76e80303b6f83d1d2c0552b", size = 8293058, upload-time = "2026-04-24T00:13:56.339Z" }, + { url = "https://files.pythonhosted.org/packages/4b/23/74682fd369f5299ceda438fea2a0662e6383b85c9383fb9cdfcf04713e07/matplotlib-3.10.9-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:985f2238880e2e69093f588f5fe2e46771747febf0649f3cf7f7b7480875317f", size = 8186627, upload-time = "2026-04-24T00:13:58.623Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e8/368aab88f3c4cd8992800f31abfe0670c3e47540ba20a97e9fdbcde594b3/matplotlib-3.10.9-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6640f75af2c6148293caa0a2b39dd806a492dd66c8a8b04035813e33d0fd2585", size = 8764117, upload-time = "2026-04-24T00:14:01.684Z" }, + { url = "https://files.pythonhosted.org/packages/63/e2/9f66ca6a651a52abfe0d4964ce01439ed34f3f1e119de10ff3a07f403043/matplotlib-3.10.9-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:42fb814efabe95c06c1994d8ab5a8385f43a249e23badd3ba931d4308e5bca20", size = 8304420, upload-time = "2026-04-24T00:14:04.57Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e8/467c03568218792906aa87b5e7bb379b605e056ed0c74fe00c051786d925/matplotlib-3.10.9-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f76e640a5268850bfda54b5131b1b1941cc685e42c5fa98ed9f2d64038308cba", size = 8197981, upload-time = "2026-04-24T00:14:07.233Z" }, + { url = "https://files.pythonhosted.org/packages/6f/87/afead29192170917537934c6aff4b008c805fff7b1ccea0c79120d96beda/matplotlib-3.10.9-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3fc0364dfbe1d07f6d15c5ebd0c5bf89e126916e5a8667dd4a7a6e84c36653d4", size = 8774002, upload-time = "2026-04-24T00:14:09.816Z" }, +] + +[[package]] +name = "matplotlib" +version = "3.11.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13'", + "python_full_version == '3.12.*'", + "python_full_version == '3.11.*'", +] +dependencies = [ + { name = "contourpy", version = "1.3.3", source = { registry = "https://pypi.org/simple" } }, + { name = "cycler" }, + { name = "fonttools" }, + { name = "kiwisolver" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "2.5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "pyparsing" }, + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/64/f9a391af28f518b11ad45a8a712353c94a0aefce09d3703200e5c54b610a/matplotlib-3.11.1.tar.gz", hash = "sha256:69647db5746941c793d6e445a4cd349323ffb87d9cc958c2ad84a659b4832d30", size = 32612045, upload-time = "2026-07-18T03:39:46.63Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/d0/791aa183dd88491555cf7d4be0b52b0bcf6c3c2a2c22c815a2e819bf53e2/matplotlib-3.11.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:b7cf158e7add54a8d51ac9b5a84abd6d4e13ed4951b4f25f1c5139f41c2addb2", size = 9440302, upload-time = "2026-07-18T03:38:03.844Z" }, + { url = "https://files.pythonhosted.org/packages/35/74/82bbdf683a301f4478384c8aaba6903631a2ca18294b2d7655c9a542bffb/matplotlib-3.11.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d2ace7273b9a5061a3b420918a16fae1f2dc5dfee1abcc13aba71b5d94b1820c", size = 9268549, upload-time = "2026-07-18T03:38:06.144Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f0/9b4298911303f74e6d83e64a81d996c0616405ec95046fac7f17e4258b9e/matplotlib-3.11.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aee55e9041211bf84302ab55ec3965df18dd90ae19f8b58332a7feaf208bfe83", size = 10024922, upload-time = "2026-07-18T03:38:08.236Z" }, + { url = "https://files.pythonhosted.org/packages/84/6f/0bc3c3d05b021db44c14bc379a7c0df7d57302aa15380c16fd4e63fd6a9b/matplotlib-3.11.1-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f4bdeea33a8d15a071dbfe6d119451b1d719c733ac666d65357082901a9099", size = 10832170, upload-time = "2026-07-18T03:38:10.276Z" }, + { url = "https://files.pythonhosted.org/packages/db/4d/e375f39acdb2af5a9342730618608e39790ec842e6f1b392863028781459/matplotlib-3.11.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b4c78ceb2f11bcac7389d305cda17aeb1f4586a857854ab5780bd3dd8dbfc407", size = 10916701, upload-time = "2026-07-18T03:38:12.512Z" }, + { url = "https://files.pythonhosted.org/packages/bc/be/fa26ed085b41298f64a8f9b7592c671bbf1acc8b0df124c1c5de96b859f8/matplotlib-3.11.1-cp311-cp311-win_amd64.whl", hash = "sha256:7f33a781e12b1e53b278deb2f5373c2e55ec4f10727be3440c0cfb5cda9f944f", size = 9315331, upload-time = "2026-07-18T03:38:14.949Z" }, + { url = "https://files.pythonhosted.org/packages/b6/f3/eb5bdf3b6e191b200db298b08bbc1638b7f3c82cdc8680f9d88bf72559ae/matplotlib-3.11.1-cp311-cp311-win_arm64.whl", hash = "sha256:67e4c3cd578c65ebd81bdc09a1b6592ceafee6dfafe116dc85dfcb647b5bbb18", size = 9003475, upload-time = "2026-07-18T03:38:17.205Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6c/7ef7ebcb2bd9739b2b66b18b076e077f44bb46fdbe28ca0506edb3c62c79/matplotlib-3.11.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e15ef41507f3d525f46154ac9e3ae785dacde9f20e593a25de8986267892ef74", size = 9453849, upload-time = "2026-07-18T03:38:19.593Z" }, + { url = "https://files.pythonhosted.org/packages/eb/f8/6d0c312c8d9738e7d9677f09fe5c986b3239e651a7b73a2deb38b65e4a71/matplotlib-3.11.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:21a67b961a6d597bca54fae826cd20695ba4a6e4d05424a08da6e13e3176fd6b", size = 9283113, upload-time = "2026-07-18T03:38:21.95Z" }, + { url = "https://files.pythonhosted.org/packages/c9/cf/b4ad2cc81b6672ea29ea04e64e350a9f9b493b0908ccd884c67eeff8f7b2/matplotlib-3.11.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ba8f811b8ddfac493734d6af0b2dff96919d0c28ca0d641858dab4262777c6ea", size = 10035615, upload-time = "2026-07-18T03:38:24.315Z" }, + { url = "https://files.pythonhosted.org/packages/88/90/4e10e033d9b66589d8ed98b84c95cdbb57033d57c1f41339d7393dbd2f2e/matplotlib-3.11.1-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c52f7ad20ef476806ed212380b1d54d20310c8b86bdc2c9a68b51f0024a44472", size = 10842559, upload-time = "2026-07-18T03:38:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/88/eb/799612d0f8cd3e816a10fec59329fca52cd2353264df80378dfc541ae855/matplotlib-3.11.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8b14eb22961fe865efb0e4ff167e333e428908b00115a8d800ccb65ee108e481", size = 10927532, upload-time = "2026-07-18T03:38:28.532Z" }, + { url = "https://files.pythonhosted.org/packages/88/89/56649bbaa2fd12e20f3be03dbcc135b0c8676d88bac17977599e3eb442a0/matplotlib-3.11.1-cp312-cp312-win_amd64.whl", hash = "sha256:88a2a27dd9691ae448dfae4b26f59036be90c3c28757edd3553a29559d00859f", size = 9333886, upload-time = "2026-07-18T03:38:30.477Z" }, + { url = "https://files.pythonhosted.org/packages/c1/11/4d124efbbad677b7b7552f6f85a3bd432d4232f95400cea98fcd2ae36ef3/matplotlib-3.11.1-cp312-cp312-win_arm64.whl", hash = "sha256:480194afceca4df2f137c2721227d3cba67121fbf4397b69cee7f83714b0a58a", size = 9007545, upload-time = "2026-07-18T03:38:32.833Z" }, + { url = "https://files.pythonhosted.org/packages/04/6c/4798363b7fb5644e309fe1fac30216e9146c9f70859d80d588c18caf5317/matplotlib-3.11.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6771b0cd7838c6a857a7209814158c0ad09bfef878db3033dd82d70ad101f191", size = 9454341, upload-time = "2026-07-18T03:38:35.001Z" }, + { url = "https://files.pythonhosted.org/packages/59/98/6acadbe7f98df19d274bc107ac58bb439fa75df82c33dc110d71a4a8501f/matplotlib-3.11.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2abdee5ffa2fe11b2d19f7a5c63b785fb7c28cc46c7bc1814156341d9d1a33e1", size = 9283627, upload-time = "2026-07-18T03:38:37.061Z" }, + { url = "https://files.pythonhosted.org/packages/24/ea/65cec46fe241390ccea1b1754207ee28eb71c5ab866bd5f22fe47e538fa4/matplotlib-3.11.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b0a19dcf73406d3746d25a5ed42d713604c9a3e024d129b102852b0d941cb9f3", size = 10035860, upload-time = "2026-07-18T03:38:39.663Z" }, + { url = "https://files.pythonhosted.org/packages/c7/10/63fdccccbabe002fb0960876baabc5e3f24d9c1bb4cfb25651457f74b3a0/matplotlib-3.11.1-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7389b77ed2ab0552f46d9a90b81b7b8e6dfcdc42adc36c37a0865799843e0e3e", size = 10843594, upload-time = "2026-07-18T03:38:42.144Z" }, + { url = "https://files.pythonhosted.org/packages/98/51/a1155945bff7b91381875022ac1522c5dfdac0d006be8e7df389b3134eae/matplotlib-3.11.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c90be0b73568da4f662afac580956a76e308437e641b4a45aa08925eeb67d95f", size = 10927962, upload-time = "2026-07-18T03:38:44.302Z" }, + { url = "https://files.pythonhosted.org/packages/0d/3a/3d5e1f42dc761bf53401a62a83ff93389b37de9d2c093b2a3aa49ac34f1b/matplotlib-3.11.1-cp313-cp313-win_amd64.whl", hash = "sha256:68408341f2312836fbbdf6b3c78047f65b2d8752f5fd221c3e72d348f5b34f8b", size = 9334074, upload-time = "2026-07-18T03:38:46.616Z" }, + { url = "https://files.pythonhosted.org/packages/e2/db/3f5ea5a5b64060ef5e1ff60a19170423e41ce21b8497a6fe15a36e0b43e3/matplotlib-3.11.1-cp313-cp313-win_arm64.whl", hash = "sha256:0c1f44890d435c1b4ef52f701ad5828cb450ea97bcc83918fda6be74965d6cd2", size = 9007662, upload-time = "2026-07-18T03:38:49.112Z" }, + { url = "https://files.pythonhosted.org/packages/98/6e/c7ae5e0531425b69c0826b00ebbc264c85cab853f1cd6e096c9983c2cdc1/matplotlib-3.11.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:5e510088c27a89d53580a752f959146893563e63c330e161d159b0fee652af6f", size = 9503790, upload-time = "2026-07-18T03:38:51.527Z" }, + { url = "https://files.pythonhosted.org/packages/92/79/15be162e0a2ed546939674e2e97d0e33ec2447d86d4d4e611fa295bb178c/matplotlib-3.11.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:1524e2bdd48a93557aa47ddcfe9c225dfdd57d5a01a5c49128c20f0632980ee1", size = 9336148, upload-time = "2026-07-18T03:38:53.564Z" }, + { url = "https://files.pythonhosted.org/packages/6a/7f/36ffe144fc4aacfe0e3ed2318f72b6755d1e73b041d619b4d393e60f5a66/matplotlib-3.11.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:11664c551345553db92e61cae6cf1376f138f8c47cafdf13b64b18f3e3e9e464", size = 10049244, upload-time = "2026-07-18T03:38:55.911Z" }, + { url = "https://files.pythonhosted.org/packages/ab/5f/55812d68c0a840d3a463638f48c00ab1fe338518ec49a640cb6473b444af/matplotlib-3.11.1-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e1f8922ba31959cf6a9dfb51be64b7f7bc582801a3957dc0c2f3afcd3537adf", size = 10860798, upload-time = "2026-07-18T03:38:58.282Z" }, + { url = "https://files.pythonhosted.org/packages/7a/64/cca444b4eb5e6c768c44fc5e1f0b5211f20ca2b282778051996e996a2bdf/matplotlib-3.11.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:83235693abde86e5e0129998f80ee39fc7f58e6d56a88fafb28a9278833e9d5f", size = 10943282, upload-time = "2026-07-18T03:39:00.465Z" }, + { url = "https://files.pythonhosted.org/packages/e5/0f/a49c329d394f2e9ef38506982107e8b04ecf94dd41a9d8423ff82cc737c7/matplotlib-3.11.1-cp313-cp313t-win_amd64.whl", hash = "sha256:9a076f4fc5cdc43fdf510f5981418d25c2db4973418d9f22d8bb3dc8045ada78", size = 9383532, upload-time = "2026-07-18T03:39:02.468Z" }, + { url = "https://files.pythonhosted.org/packages/e4/50/103e86afb806d8f64d04ede14e4cfc09dbfc25f512421ff85fdd6ebd59cf/matplotlib-3.11.1-cp313-cp313t-win_arm64.whl", hash = "sha256:216fbb93a74add02ddb4cb38ef5348f59ac00b3e84567eaf16598772d40e150a", size = 9059665, upload-time = "2026-07-18T03:39:04.607Z" }, + { url = "https://files.pythonhosted.org/packages/35/04/3079499fa8cb661ea66d13d6439d5a3ae6710a7afd5c7f72e08914f275f8/matplotlib-3.11.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:30c492d4ba9448595b6fd8708c6725963f8148e25c0d8842948da5b05f0ee8d3", size = 9456022, upload-time = "2026-07-18T03:39:07.041Z" }, + { url = "https://files.pythonhosted.org/packages/53/a2/69acfe84ec1f32930e801a5782a07fc5c79c8c6599a507b806d859d5da8e/matplotlib-3.11.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ac104be2768ffdd8655db9e71b768cbb45f2b9aa7b450cf1595e8f65d3822319", size = 9285475, upload-time = "2026-07-18T03:39:09.562Z" }, + { url = "https://files.pythonhosted.org/packages/d3/b3/31b15a2ca56d4ddd6aaa1c884c2f51cf9a61cfaf5ca6f6fbd6343d38e6df/matplotlib-3.11.1-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6be943cb68bc6660ead58c55b3aa6366cba2ef7feb06460fbcce32360376f19f", size = 10847102, upload-time = "2026-07-18T03:39:11.532Z" }, + { url = "https://files.pythonhosted.org/packages/64/0d/a17e966e620545c1548125af0b29ac812dd17b197a18a7462ac12fa859ee/matplotlib-3.11.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5af0dcda57d471440a7b5b623e70e0a61003518443d9098f211a96ecfbbc25be", size = 11131087, upload-time = "2026-07-18T03:39:13.764Z" }, + { url = "https://files.pythonhosted.org/packages/97/c5/5e100efdd67abb7de20befaa333612ef9bfc63417fb71398f904f25d083c/matplotlib-3.11.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3d3fd84082b1afbd9398466c81309e20045be20d48fe0fb18c43504d164cbbb2", size = 10929036, upload-time = "2026-07-18T03:39:16.888Z" }, + { url = "https://files.pythonhosted.org/packages/ce/04/d719a0a36930ecc8dfc801ff340f9dcfc4223f8ca5d39d06b4020032fff8/matplotlib-3.11.1-cp314-cp314-win_amd64.whl", hash = "sha256:9601a1e90be21e4884c53b4f3dc3ee0544654946f9975258d691f1c2e2f119c6", size = 9489571, upload-time = "2026-07-18T03:39:19.449Z" }, + { url = "https://files.pythonhosted.org/packages/48/65/facabdc2f1f6caba7e856db64dfedddca25f7608df07d96a1c8fd114fd3b/matplotlib-3.11.1-cp314-cp314-win_arm64.whl", hash = "sha256:ae30c6109848ac0f9fa36c5d6270938487614c47ba31860bd5361266dabc5685", size = 9164486, upload-time = "2026-07-18T03:39:21.424Z" }, + { url = "https://files.pythonhosted.org/packages/88/dd/18da6cd01cf96354534f98c468a25380c68ce582a2c9dd0cae12b04af4f2/matplotlib-3.11.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:dadfe80797174e2984aae3be0b77594a3c72d2c0a40fbd4a0de48d2728caf3ae", size = 9504876, upload-time = "2026-07-18T03:39:23.633Z" }, + { url = "https://files.pythonhosted.org/packages/79/b0/f0b63555a18b79d038c81fd6126f35fc4dfce0eaff48d96103348c7cf935/matplotlib-3.11.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:89b193b255f4f6f7948dbcee3691f4f341ab05d9a8874a67b45ddb4182922eda", size = 9336120, upload-time = "2026-07-18T03:39:25.797Z" }, + { url = "https://files.pythonhosted.org/packages/c6/dd/f210ec7c4a6f198d5567237048a93d0811fb5a1f1691f13320e592f95b41/matplotlib-3.11.1-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:191163532cdefcb1571ca38a6d7e6474baccde64495783e6ba47aa07ec4b9bbb", size = 10858033, upload-time = "2026-07-18T03:39:27.999Z" }, + { url = "https://files.pythonhosted.org/packages/ec/d2/d6d5324507c5fbb316db48e258c09c2807f3de03d9af47017e120070926f/matplotlib-3.11.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9fdf1c818ab05d0e74002091ddaf414478a3a449ec9d51c8976d45be7e3a01e2", size = 11141827, upload-time = "2026-07-18T03:39:30.092Z" }, + { url = "https://files.pythonhosted.org/packages/0f/68/3c22e9320bdce2c4d2f1320643ef706db7a24cb7420eea28b97a2d67f5a8/matplotlib-3.11.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b937b9dba5f5f6c1e31c47abe2186c865c0914fd18f2ce0dfc39c9adcef5951d", size = 10943061, upload-time = "2026-07-18T03:39:32.356Z" }, + { url = "https://files.pythonhosted.org/packages/f6/4a/907ed190ee81a9df581e0ed5456134fc0f7cb55ffcfda2f9e54ca900761c/matplotlib-3.11.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f2912f647f3fbe1ccf085f91e213936f9101bead81a5e670565b1f1b3712f4fb", size = 9540074, upload-time = "2026-07-18T03:39:34.789Z" }, + { url = "https://files.pythonhosted.org/packages/23/d4/97c19b77e0a6e3b48581185bb65088f431cd20186076cc0f650a1757ea46/matplotlib-3.11.1-cp314-cp314t-win_arm64.whl", hash = "sha256:54d47b8ae8b579633a3902ca5b4ad6c1e132a5626d64447b2e22a66394e79987", size = 9213472, upload-time = "2026-07-18T03:39:37.141Z" }, + { url = "https://files.pythonhosted.org/packages/ee/38/ceb1d637c4db6d06141f3739e93af3321e7caaabe69b57ae48ffe3ee95b1/matplotlib-3.11.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:427258425f9a3fc4ed79a91f9e9b9aaf5a82cb6571e85dc14063cc6fbb993741", size = 9438045, upload-time = "2026-07-18T03:39:39.491Z" }, + { url = "https://files.pythonhosted.org/packages/89/25/72ad8b58602d3a6ef1dfc4b65ecd01634ab65a2bdf494c9fe0e966dbf081/matplotlib-3.11.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:1ac697e591c11b6ad04679a73c2d2f9980fe9d9f0311fb414a2e329706343dfb", size = 9266127, upload-time = "2026-07-18T03:39:41.597Z" }, + { url = "https://files.pythonhosted.org/packages/8a/6d/69552382fcc8e93d1f2763ef2665980a900a48b7f3a4c57ed290726d1cbc/matplotlib-3.11.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e4b9ac2f1f607ecda2af90a5232beee2af7582fce1cc30c4b6a1b012dc21ee99", size = 10019439, upload-time = "2026-07-18T03:39:43.78Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "moderngl" +version = "5.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "glcontext" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/52/540e2f8c45060bb2709f56eb5a44ae828dfcc97ccecb342c1a7deb467889/moderngl-5.12.0.tar.gz", hash = "sha256:52936a98ccb2f2e1d6e3cb18528b2919f6831e7e3f924e788b5873badce5129b", size = 193232, upload-time = "2024-10-17T12:36:28.002Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/4b/988c5f4ef55bf0c62a0b63a4b32fe6d6dd755d12f5951e8fd56e8a30d3f6/moderngl-5.12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:105cdee2ee29a7e6ebbc76cf178941503656a185c6945933bc7ef395ba8e65a7", size = 111803, upload-time = "2024-10-17T12:36:34.271Z" }, + { url = "https://files.pythonhosted.org/packages/97/40/272d38d9d096d591db08e4aa1c983a8ed34cd1717b6524a88539b0df4341/moderngl-5.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:18deb8bebd0a4277d92c76dbedf8e4b4b68bf0a8a878404c6b26aed750890d3b", size = 109223, upload-time = "2024-10-17T12:36:35.957Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5c/a6f2743c8ae1c95cd6531510bf76fa6be650a5e0a1dffdf138b5ed186f61/moderngl-5.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f0c4f7c42425177168938386a4fabd734ca3bbb5de2d1fd1176cfa6f980fc13", size = 291432, upload-time = "2024-10-17T12:36:37.122Z" }, + { url = "https://files.pythonhosted.org/packages/2b/2d/b6bfc4fe7df343a4f410030dbd8261566282049e56e9ffc3cbd2e626656e/moderngl-5.12.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:878cdf593204d85c020305f21d306f979353a67c932b9df58ea936f6e5ad13e7", size = 265422, upload-time = "2024-10-17T12:36:38.767Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ae/35f8f4c833aca90870110509dddad641f5adf9597da15b3c9e7c75db319f/moderngl-5.12.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:00d94f9cb485d87c85088edad624201e152d8ac401793a024b16cd9e2bc4dbf6", size = 1342266, upload-time = "2024-10-17T12:36:40.405Z" }, + { url = "https://files.pythonhosted.org/packages/b9/4b/2e0b6d0e8a5273da93ed82c8eb91b54a37413b1b9d65bb223d67b595fc96/moderngl-5.12.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:edd91057b8d76beebac0d7b0c741466ee8f37eaf3c07856785c2872afe0b35ac", size = 1270055, upload-time = "2024-10-17T12:36:42.243Z" }, + { url = "https://files.pythonhosted.org/packages/48/2f/b590f86d42630fbbaffd83f1890ef6278ec12d5cb336b6a2efcbb3c2ca21/moderngl-5.12.0-cp310-cp310-win32.whl", hash = "sha256:3d066eae2eb44e81bd7addf565adebc041bdee119e7ac6e4f95831d6f327a938", size = 101044, upload-time = "2024-10-17T12:36:43.94Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6d/f9b77797d3a0f3e5b67a75092c6c63b26360042fd8e43be017c706e308a8/moderngl-5.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:76d966194d51852f48c42a6e786a4520f1e1be5f93e2626423d673663422d559", size = 108272, upload-time = "2024-10-17T12:36:44.995Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ea/569c2c08bfef84f4acf633a8e6d956f4f75cfaa8832d7d812dbf2ff6843a/moderngl-5.12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:28cdba5dcf2d03c89bb25dc3b2f5770ac4104470ed5bbe680a15494fa52a537d", size = 111802, upload-time = "2024-10-17T12:36:47.377Z" }, + { url = "https://files.pythonhosted.org/packages/00/ef/98f36133ab010ce9831b75a16e75a627c12a4c1d6ef2e353eca1769a1e09/moderngl-5.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dad93893e3fcb2410bfd31e854f20e1370b4fbafa07a737f1046f5fbd29ba0f4", size = 109227, upload-time = "2024-10-17T12:36:49.24Z" }, + { url = "https://files.pythonhosted.org/packages/c2/bb/ab371acacd2497bddb5f02b209e3bfae452b2be59d0cf8fa728a3b87de1f/moderngl-5.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fc0f8788bc84433d2124e9a4893adbe40f93c7d213abb8ad7b909540cb0161f", size = 293468, upload-time = "2024-10-17T12:36:50.8Z" }, + { url = "https://files.pythonhosted.org/packages/0d/9e/7ebf2b98da310c90c2b295e91b6c25f864f0f5583ce86bee72d387cb577a/moderngl-5.12.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6efd3fe0d2c9652af21e2c1f5a936a2b971abac5bdd777da7182a54962466cab", size = 267348, upload-time = "2024-10-17T12:36:52.526Z" }, + { url = "https://files.pythonhosted.org/packages/61/0a/87fb24f4cd2aa07150b84fbf400edf4d8a8f71784cbf064f1ed92b756fea/moderngl-5.12.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6f3bd2d534fc081cde30545b84ebca63aef847ba8bd533217b9a37f565614ade", size = 1343953, upload-time = "2024-10-17T12:36:54.601Z" }, + { url = "https://files.pythonhosted.org/packages/83/42/11b0306e630d9a38b8bac20563b326f6d5fba4dfc45cc90b0666ed3e7141/moderngl-5.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:eaa3de9446c6febec4d5f888e6f1a4e9398bc5a5ea70b1570ea447213641d4a6", size = 1271832, upload-time = "2024-10-17T12:36:56.383Z" }, + { url = "https://files.pythonhosted.org/packages/6b/dd/74d300275fe4834e63b8d70450801f206d005f2047898d4eb2a30efc3913/moderngl-5.12.0-cp311-cp311-win32.whl", hash = "sha256:9fdb76f1fd890db67727c8cdee4db2ee6319068c7ce92be0308366f8745e28ab", size = 101043, upload-time = "2024-10-17T12:36:58.281Z" }, + { url = "https://files.pythonhosted.org/packages/7f/08/5f615a4605d343cd5c1112d2b175270e6a5586008bc10a85b822c340cf86/moderngl-5.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:0c210e8d52a60025f6586ca015c39feb1e57e6dc792c3ff44800f6493a541b1a", size = 108279, upload-time = "2024-10-17T12:37:01.125Z" }, + { url = "https://files.pythonhosted.org/packages/3c/66/31161e81bc85ca3cdbb9d94f703f21575e4ae9a2919e9d1af98fc7fdb1ba/moderngl-5.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2750547707c1ec3790dfbeb9c90fb808672ff13f61cac392c706ba09fda10db0", size = 112101, upload-time = "2024-10-17T12:37:02.267Z" }, + { url = "https://files.pythonhosted.org/packages/84/b2/7229a89a40d33a95119a7c64c7ee36a6a6e376c57c39fb577ea513602f37/moderngl-5.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c5c2a5fe06c7021183d9274df798f25516409c8d55898c324dae8a0b2de10144", size = 109377, upload-time = "2024-10-17T12:37:03.843Z" }, + { url = "https://files.pythonhosted.org/packages/d7/96/bcb5141eae24474d80b8157b0c3055d25fa75f9804d4abb4a514695bbba9/moderngl-5.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6c4972f3ddd10a3de6c30311da2c25bc493d023796e16c5d4e0f8bd6d5770be", size = 296394, upload-time = "2024-10-17T12:37:04.967Z" }, + { url = "https://files.pythonhosted.org/packages/b8/79/a9998ddf6757f4f15888b0a106d80a64a8c8991a8ce5c14047830704b9e6/moderngl-5.12.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a4d497ec6a3f6afa9ebd0be816d9bfe2fe20fec2105acfb88d956619c3ed8eb4", size = 270548, upload-time = "2024-10-17T12:37:07.57Z" }, + { url = "https://files.pythonhosted.org/packages/17/f4/313dc301db936b231035b961e004f1914c2954bdcdf4985e24bff15e7ed5/moderngl-5.12.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2f3d240e9bc5d83257378bae59f8f35638b89d22bb003cf674b88fd7932161ce", size = 1345817, upload-time = "2024-10-17T12:37:09.105Z" }, + { url = "https://files.pythonhosted.org/packages/8c/6f/7b3587e7e3ae633b8c85038f035dfeb348ebf805de4beb01241c59c6b97c/moderngl-5.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6fa667d560d842e778e2a5968305fb78f9781616a11b1b93acd2562f97262ccf", size = 1274968, upload-time = "2024-10-17T12:37:11.092Z" }, + { url = "https://files.pythonhosted.org/packages/4f/e8/0f3b3cd1be7b0a93f33dc613f76a42021d1393b4949c5b6a1ca2a01c6772/moderngl-5.12.0-cp312-cp312-win32.whl", hash = "sha256:0a02fddd54dccee1ca6060bfed75a2e6a17dd3ee06920fac418506d8a8233849", size = 101221, upload-time = "2024-10-17T12:37:12.642Z" }, + { url = "https://files.pythonhosted.org/packages/56/85/35498b1821cf31c731b1882168db8924207ff3c06d8f0da53e1cc373a89d/moderngl-5.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:8698a59ad03539a2982125b7998efc1c107ba31d5d03437b6fcd72cb2c226922", size = 108525, upload-time = "2024-10-17T12:37:13.816Z" }, + { url = "https://files.pythonhosted.org/packages/39/13/cf493bdc3cb4f7a6b4fb357e683404dc8a97d19f53d501e4afdd679538e2/moderngl-5.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f6efb432f5164f871471d1da36e3a4be9dc3efd7a1e48d0ac6b751e556af5d02", size = 112118, upload-time = "2024-10-17T12:37:14.892Z" }, + { url = "https://files.pythonhosted.org/packages/a9/1f/1d84bba5f42fb19ce240d08d5434fe8e2f34341e68bb9fa89336b6cbdcfc/moderngl-5.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9b09d8d15b2eaab41c8646a664429ec86af225fa25096758497cd212489d2e1e", size = 109371, upload-time = "2024-10-17T12:37:15.992Z" }, + { url = "https://files.pythonhosted.org/packages/be/ae/bda0b95878e2b36eac66f64d88c08e6c8ea759607f7d40e843a21c2f4f32/moderngl-5.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:071042dd4846e58cbe204cf49341b62cd209fdcb6d48018feb5a61c66707fcb2", size = 296206, upload-time = "2024-10-17T12:37:17.204Z" }, + { url = "https://files.pythonhosted.org/packages/28/bc/93dc73251bcdb9c0f6f5c9a1d97ec2134672c307c68e6106948eab1f73d4/moderngl-5.12.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91db8302ac7f5d7a82a967388677e1378ff078f1e16d05da37ce77f4633b93b1", size = 270443, upload-time = "2024-10-17T12:37:18.44Z" }, + { url = "https://files.pythonhosted.org/packages/2e/a1/bd72c788b16c2392d3e1ebb570e56d7f871eaa1854f57917c0f131acb365/moderngl-5.12.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:51971d65ec96a212a814350c8b324ae0754353e1b61826d1a06aa2d060df170e", size = 1345675, upload-time = "2024-10-17T12:37:20.077Z" }, + { url = "https://files.pythonhosted.org/packages/b2/ec/6aff8fa267d9f80e4d32b7a606fdf9f0563103441a7dbaa4f53e272a4ada/moderngl-5.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d56827360c19e831e986243b5daaf6a51006f1ec0d5372084ad446308763d19f", size = 1274906, upload-time = "2024-10-17T12:37:22.372Z" }, + { url = "https://files.pythonhosted.org/packages/9f/4d/dc3ff763c125080e71b1095875f5dcc80949402019abc073bdfdbed1f4c2/moderngl-5.12.0-cp313-cp313-win32.whl", hash = "sha256:caa432c12b138a6c9571719075c4d103bdc2504cd31aeda38a00ad10fcf268cb", size = 101215, upload-time = "2024-10-17T12:37:23.833Z" }, + { url = "https://files.pythonhosted.org/packages/2c/8b/0a264732e0ee49fca109e98ec28f4d0c326ffc31466aa6e9668e8961aabb/moderngl-5.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:e34d1cd38f7998258f76a08bb5e87f351ec653b7ea1928b2711f8719c10cefd1", size = 108514, upload-time = "2024-10-17T12:37:25.304Z" }, +] + +[[package]] +name = "moderngl-window" +version = "3.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "moderngl" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "numpy", version = "2.5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "pillow" }, + { name = "pyglet" }, + { name = "pyglm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/02/92e235891300c901f59647112a0267a07454f58aeb2041aa44f6b85f9cb3/moderngl_window-3.1.1.tar.gz", hash = "sha256:29c2827505f87399f3461d480b2778910fddeebe44ea803301215cf212a6c1bc", size = 353495, upload-time = "2025-01-19T10:07:56.133Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/41/d3503f07de8ad3597165f88b07e1064629f82e65385c4307875b2f6b137f/moderngl_window-3.1.1-py3-none-any.whl", hash = "sha256:e3b3ac2b4e23afcbfdac1971318a4db893bed3ba6a8fbde3367b1226af39b2e5", size = 382381, upload-time = "2025-01-19T10:07:53.606Z" }, +] + +[[package]] +name = "msgpack" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/44/ea2100ec54d30c46ee9dba10a3bfb79b655e96c6df237238a3234c75869b/msgpack-1.2.2.tar.gz", hash = "sha256:9eb0b0e602064527a045ea28c4f174ed69383587e29cebe28947e3b84106eb2a", size = 187025, upload-time = "2026-08-27T10:03:47.793Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/40/181c8944b28f779ed0b2587d24cc0ccf1bc87248204105327140aa20d63d/msgpack-1.2.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7afa5431f6f3487c584187ca6c8e2a34e9b106529893b3e720eabb068f6ac970", size = 83554, upload-time = "2026-08-27T10:01:34.182Z" }, + { url = "https://files.pythonhosted.org/packages/03/f7/a6bf145f7d3eb734b3d97fa295f8007a586799ef56b456c8b27bff62caac/msgpack-1.2.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a9b4cf3685a135666d27d0d7a73fece74e2fad01d9b508fded89e843512f0e90", size = 83857, upload-time = "2026-08-27T10:01:35.816Z" }, + { url = "https://files.pythonhosted.org/packages/93/1a/77d32a60a80ee67016e77b13bec07e85f1929a92f046da044591c7eb01c9/msgpack-1.2.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4710d881d8fb047deed2485707409116722af2b992d3fefd73c7667c4e350839", size = 398390, upload-time = "2026-08-27T10:01:37.693Z" }, + { url = "https://files.pythonhosted.org/packages/3f/f5/ae93f85b063d744731cb285528210cd950333b167cacc7b2f96e1420a475/msgpack-1.2.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:58ce37a4a54577115922385d37201d9a44d66d0167dfbbf4770a2e9bf8ea7ba3", size = 407183, upload-time = "2026-08-27T10:01:39.499Z" }, + { url = "https://files.pythonhosted.org/packages/17/92/d91a08a913a3bbafbede5b9dbf48e4e517f7d92510877b6b02730060ea85/msgpack-1.2.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:86f173a584f72f6164801f31866d22a581f60c991572cf922aed9ab8eb422b77", size = 375978, upload-time = "2026-08-27T10:01:41.234Z" }, + { url = "https://files.pythonhosted.org/packages/b5/a4/27400c101a96115fe8484cad57b6c5eb4f9ffba080a1d9be62ec9174bf67/msgpack-1.2.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e05a94a0442de86818a30281c6cc2cb9cc7aa148386fd3541c4d4774b73cb3a9", size = 389566, upload-time = "2026-08-27T10:01:42.779Z" }, + { url = "https://files.pythonhosted.org/packages/44/92/541e9fa4623587767623788b38d11fc78d402acb1421962a13d3ace48bef/msgpack-1.2.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:9bd3d1557c3fe1a095068210708a03e3e4795973392af6f4047060e70abd9a6c", size = 372583, upload-time = "2026-08-27T10:01:44.572Z" }, + { url = "https://files.pythonhosted.org/packages/0e/03/c1e0035e1f923f548b7016a2fef5afea431cdae95d397c5aa52ed75dde05/msgpack-1.2.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:46ec851571d8f1b6e29794ebb9dd36f785008da6d14f57c702e60781d6caf648", size = 404507, upload-time = "2026-08-27T10:01:46.167Z" }, + { url = "https://files.pythonhosted.org/packages/ca/49/7efba0fd604059f0dc8f57ba2867bda4d57bab6921a12e975ecfbd49284c/msgpack-1.2.2-cp310-cp310-win32.whl", hash = "sha256:1f3af0baafd184436501004828bb3df64eeb2fc49dfe9d89abcf604956094563", size = 64848, upload-time = "2026-08-27T10:01:47.797Z" }, + { url = "https://files.pythonhosted.org/packages/57/1d/b41e96ff441d46890b9d5982959aea4d15c5d322f92740dbef16e9dfa908/msgpack-1.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:82b1bdf293267afaadcc608b125e7fc6576bb0785a60c4fa7d07c7ab76ed76ec", size = 70927, upload-time = "2026-08-27T10:01:49.199Z" }, + { url = "https://files.pythonhosted.org/packages/53/fd/a05ba8f84c5951c9aec2a19c1c81f6c4a67b8bec80af604ac5b23ccfa019/msgpack-1.2.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9d7fb25b4442fae0cb2590272d06ab4f6caa526ee36a994edb81e946b874813e", size = 83498, upload-time = "2026-08-27T10:01:50.62Z" }, + { url = "https://files.pythonhosted.org/packages/0f/df/e20bcf5c149890545334743b212eb4b82e1a25fe0a34f99753a1755bfab5/msgpack-1.2.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7fe374ba76eb0ecca13a1703daa8fa85825a6ddddbb52d4c1a732fa524194683", size = 83896, upload-time = "2026-08-27T10:01:51.898Z" }, + { url = "https://files.pythonhosted.org/packages/ac/c3/00dcd902d66a641b9ba350783feb482ea5c1ca4a7ff6629db0c10c0ea982/msgpack-1.2.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9b0c1f2aa7b0026b4bd50718100e8b04175e4f36e160aa852502377b5e572e7", size = 413259, upload-time = "2026-08-27T10:01:53.296Z" }, + { url = "https://files.pythonhosted.org/packages/93/15/17374efe9793f5332c7d4727ab40539f95a1dc9df653531795daca8c4281/msgpack-1.2.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f11e09f10210a91c169e39c7a5a1f9090eaa73ad75555fafad5023c3053c47ba", size = 422907, upload-time = "2026-08-27T10:01:54.786Z" }, + { url = "https://files.pythonhosted.org/packages/d3/af/2b567d684f912fedcefe3f7c37de604716ffa99336bd432688f9f040df92/msgpack-1.2.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8b1415d02e9bf722672af8a90f90813265a0cd0b14163187261e54a5592bc949", size = 389248, upload-time = "2026-08-27T10:01:56.55Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fd/d8533fed473cc3e309a701e851d0e5fe36ada5552a3899025f5c69fbe877/msgpack-1.2.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:42fd9260416885b4815caca5bdd14dfd5dda6cdade732d6c09104ef8f6228761", size = 407099, upload-time = "2026-08-27T10:01:58.357Z" }, + { url = "https://files.pythonhosted.org/packages/d6/1b/57906337bfee0ead554571dc203ea17c3fad26d51e5eca6271ecd983f73b/msgpack-1.2.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:336525cc2688e43ea77dfb1a4ce012c8cde561835913801dbfcfdcf4111d8abb", size = 387201, upload-time = "2026-08-27T10:02:00.109Z" }, + { url = "https://files.pythonhosted.org/packages/de/0f/5d1e6d68e516621697a9262b24917d678793e838cf3f331ed4656b3e959d/msgpack-1.2.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cdb6cc6e1127d15879c47a8b3270716243da82d3e7feab1f5946872c75b3d60f", size = 420765, upload-time = "2026-08-27T10:02:01.573Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a6/07f9a4f3324d55c3567ab2a7e8d5325291bc95a31a374bb390a21b7c4e24/msgpack-1.2.2-cp311-cp311-win32.whl", hash = "sha256:cf66fb38703e61a486b01b56d43bb1f50698fbe99b6bd90feba10f24fab60b3b", size = 64785, upload-time = "2026-08-27T10:02:03.01Z" }, + { url = "https://files.pythonhosted.org/packages/84/13/f748f0d59f355d196e71a0b32d48d386a9bd311f94d954e666cf7e5b2572/msgpack-1.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:0883a1578168929fd1640fbbc4614773f1a130e419a8a817dc2918d9af1b651c", size = 71258, upload-time = "2026-08-27T10:02:04.375Z" }, + { url = "https://files.pythonhosted.org/packages/46/a6/10d979c4e76b18a9b9ebbd6499ff863474ffe5955028ea27e09b66f6833c/msgpack-1.2.2-cp311-cp311-win_arm64.whl", hash = "sha256:4955accbd87f27beebef5f3ecc27503aa74cb016fb4f640868e749fd93194a35", size = 65860, upload-time = "2026-08-27T10:02:05.735Z" }, + { url = "https://files.pythonhosted.org/packages/31/78/90c15bebb1a72667349ca62d4507e9d9369e7f8f76b95f490b823d3622e5/msgpack-1.2.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a4348705be86e029d04e741cf9ed0dfe03e942d7d3b92e838fa80d3aa2c3ebc", size = 84275, upload-time = "2026-08-27T10:02:07.106Z" }, + { url = "https://files.pythonhosted.org/packages/88/88/c2b6d8e81571da87aa232c0e34a3f3a0e618e6235892065ec82d1d81fc7a/msgpack-1.2.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0a652ceeededf71d3fa40c303a02a149d42338d310162367b91c539d4bd6e0a3", size = 83970, upload-time = "2026-08-27T10:02:08.488Z" }, + { url = "https://files.pythonhosted.org/packages/da/c0/d3ede9f5d16acb4c05a9281859f1e99ef9f877a928eb78454c37f70db001/msgpack-1.2.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90986cc9aab9d7d1d8f38bcbf65d3f7ac83bdd90c35765db7d691b4829698cba", size = 409401, upload-time = "2026-08-27T10:02:09.877Z" }, + { url = "https://files.pythonhosted.org/packages/41/f0/29f591bea185616cf417645ac03bd3ad9b317483ad8572160e325f7fe777/msgpack-1.2.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:77c2e018417dc1d66f235e383877ee885b60ade9d29e494dd581e08af2cb1923", size = 420619, upload-time = "2026-08-27T10:02:11.526Z" }, + { url = "https://files.pythonhosted.org/packages/4e/8e/c70c8c9180c5ddf4440eb8658ebead98e22e7686fbf84f6b165031430750/msgpack-1.2.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0e91332144f69bc3018c91232fac26da580ef748fb8eaddd7914d4458001cc4f", size = 379747, upload-time = "2026-08-27T10:02:13.345Z" }, + { url = "https://files.pythonhosted.org/packages/50/9a/f10ce11fa62700c9ab87a22e65b9ca272f7f673ddd31aeb2de6ae272ad35/msgpack-1.2.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3e915d390d7068b257ca8b62f3fc59fad135c8631d1017ab03b0b924b07c5367", size = 398944, upload-time = "2026-08-27T10:02:15.006Z" }, + { url = "https://files.pythonhosted.org/packages/82/fe/d7be978456ff8552e69a8e270d882e7530e01513c096b293d83df03753ea/msgpack-1.2.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c522420d78db2431887d45b518e304d86e27b9ad0b30f24e3806a6ad5d8bdbfc", size = 373979, upload-time = "2026-08-27T10:02:16.618Z" }, + { url = "https://files.pythonhosted.org/packages/be/af/91b0d8d3fb3063e259daee3ea8515cea6282f68f4b0e5f0b6fea25762c6e/msgpack-1.2.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4b554d8164ebb526892194f71dcd96ef1fefe0c250087498785d3ffc04a80be3", size = 417781, upload-time = "2026-08-27T10:02:18.293Z" }, + { url = "https://files.pythonhosted.org/packages/8e/3c/ce8e9efe1fd9e95c78b3705e4300ba7feba3dc6c00fb76259895db155518/msgpack-1.2.2-cp312-cp312-win32.whl", hash = "sha256:0e3315de5a4b2920ccef48d96b4448025e064a10d0f5a250f6584477d839c8d4", size = 65267, upload-time = "2026-08-27T10:02:19.869Z" }, + { url = "https://files.pythonhosted.org/packages/85/98/a33b8b4af14e3476bb0da1b8c36ef7a0f28dcf95db1c5e68ff88cb89d591/msgpack-1.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:b68614fba0570349833b7dd999ff0aed4e5cc8d9eb6e3a7d4527be33c65e33d3", size = 72275, upload-time = "2026-08-27T10:02:21.141Z" }, + { url = "https://files.pythonhosted.org/packages/df/5e/2f323a33a6aba5bd4b2d8b430e4fab21d92cd91c093b49ee287bc166ee54/msgpack-1.2.2-cp312-cp312-win_arm64.whl", hash = "sha256:59d5b93efa45fd09f620d0c9ba81cde339a2c9937af3eea42ee9653094ce6640", size = 65488, upload-time = "2026-08-27T10:02:22.575Z" }, + { url = "https://files.pythonhosted.org/packages/1f/eb/42f31c5a48811787ff59a9869721f70a49654d65ab6c455f4463c39b044e/msgpack-1.2.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8b2a281b556f120a43e591ea39915741b7ad54d4727b9c4350a0a11692252533", size = 83911, upload-time = "2026-08-27T10:02:24.06Z" }, + { url = "https://files.pythonhosted.org/packages/33/54/10c6c16ddba8a5112e3680176b838e3694e4aad7284f9daa6d6d70d98817/msgpack-1.2.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1e8cdd1f3e7cc52c751092a9bf740e81e6919ab109cd376ae2d965dad0bbae34", size = 83734, upload-time = "2026-08-27T10:02:25.613Z" }, + { url = "https://files.pythonhosted.org/packages/d7/75/35823e4419df8792191b2a17ae3fe71b41d02c162b2c491c94d1a87f0caa/msgpack-1.2.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1814f92306ae7862908e9ece7cfd90e0dc87ded3e89b6ae7ffdd1175d6376fdc", size = 405635, upload-time = "2026-08-27T10:02:27.012Z" }, + { url = "https://files.pythonhosted.org/packages/6e/d3/6592e4064619b04f2dd0054c5fa13e37e3d55eb26044483d871fadb2f46b/msgpack-1.2.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d24b38a825bcca41bb956de50eb98451ef291304a8607fad99e619043d3e79b9", size = 417332, upload-time = "2026-08-27T10:02:28.776Z" }, + { url = "https://files.pythonhosted.org/packages/e3/a1/b21c6818a545e9a4a976ac954a5c250eecde9a02e0ec82f415473dab1324/msgpack-1.2.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:34e83e345194a2a51d8bd447dea9de2104f91e75b247f4735f14f04529f0746b", size = 374378, upload-time = "2026-08-27T10:02:30.678Z" }, + { url = "https://files.pythonhosted.org/packages/03/8b/7ada15c7b64151d6dbb562d1b091520efb2c37acf2403b1d4ae13797b27d/msgpack-1.2.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:682804bf31e43d46e51a9a33bd575b51e839d715ce6bd5612c055f7b28ad637b", size = 395809, upload-time = "2026-08-27T10:02:32.322Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f7/96283e50f7020df4dfeacc55612b7a210c8cdf0dda48bc262f1f9b3e4c49/msgpack-1.2.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:9b659d77f8726fa5e7038967dda6b68d53cf34472c094cfa5b845454713b90d5", size = 373495, upload-time = "2026-08-27T10:02:33.832Z" }, + { url = "https://files.pythonhosted.org/packages/cc/fe/1548dede9d9ca482f2d424a2e110a9705d4e02627a16b8bc8d10ce0208a2/msgpack-1.2.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4d9a562aec0a92fe536da2e533d313b3d2a6b929157b1dec7ff623446dc0a8ab", size = 414360, upload-time = "2026-08-27T10:02:35.396Z" }, + { url = "https://files.pythonhosted.org/packages/77/9d/4419b8f86c219174b1fb8bbd7faaf84a548935f7b1916d028401b9433417/msgpack-1.2.2-cp313-cp313-win32.whl", hash = "sha256:a4161eee7799863aee237c35c90427861f7b994416dd81ae829f560b0a81bdcd", size = 65196, upload-time = "2026-08-27T10:02:37.007Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f8/593f5caf0dacab41cde1564c5f0419e61af55ec9628006205e8fd5eb5e03/msgpack-1.2.2-cp313-cp313-win_amd64.whl", hash = "sha256:b07c03f0da7e5279170df7745ddc732d526c8a198208936ec1a95c11ed2b2d5f", size = 72203, upload-time = "2026-08-27T10:02:38.28Z" }, + { url = "https://files.pythonhosted.org/packages/bc/9e/c6ef92046b4a2bbb9d3aa0cb581cbf4a4051afccf6e5fb301a1bd3086f39/msgpack-1.2.2-cp313-cp313-win_arm64.whl", hash = "sha256:d13d07efbf655f9ae7a2352b630c52727b359005b21ba08a507585c9ac8c0896", size = 65435, upload-time = "2026-08-27T10:02:39.534Z" }, + { url = "https://files.pythonhosted.org/packages/5e/50/3e92c403346652cabd08cb8faceef847bae917ea3b3c81b64a5b6d09ed41/msgpack-1.2.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e497ee34e8a3342bbde51b27c22d8db05a651df3361dd3daef5b3ab0d66f3e04", size = 84315, upload-time = "2026-08-27T10:02:41.181Z" }, + { url = "https://files.pythonhosted.org/packages/b6/dc/8efe6dd96a12ab043930cb4cffb40b6e7f061491d6ec7a3d2b75ef1fda42/msgpack-1.2.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0dd9173c5ebaf5ecc5ca86e7ae1db92934e1d57b856f3dd90698941431f4fd77", size = 84634, upload-time = "2026-08-27T10:02:42.621Z" }, + { url = "https://files.pythonhosted.org/packages/1b/89/996573095bf7b038c04dd65ddbc4f1a4d381b0f7a44ff9186f3c7b8325c2/msgpack-1.2.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8dc4487097571f7311188c3eca2a3e86cd1f1db4c37c7a017bcc3fd38486cbfe", size = 404194, upload-time = "2026-08-27T10:02:44.096Z" }, + { url = "https://files.pythonhosted.org/packages/b6/4e/46f5a5d949dbd054dab60cb15aac7ac6ae6774c134532893414689bf2f53/msgpack-1.2.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:73b0e05c32c3cfc3cd84994908e57430c0ebc6813abf905d3f18ff115d54df3f", size = 412343, upload-time = "2026-08-27T10:02:45.747Z" }, + { url = "https://files.pythonhosted.org/packages/da/e8/739a94197358a313307e6e9e7d8d22ef66add39222de911a44161aa96920/msgpack-1.2.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aa1120c653b76d8eafa50423b5eba06b5c9737f8692c74fa3afe03e84b8978ea", size = 372620, upload-time = "2026-08-27T10:02:47.578Z" }, + { url = "https://files.pythonhosted.org/packages/03/d4/09b92e1fcdccea9466bfae45455367ac52362ae445d96a602e51b7a8df73/msgpack-1.2.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccfd880988f8438d1c91c77d7edc58e70f4d2012e999167bc154c64c6f06ea6b", size = 394603, upload-time = "2026-08-27T10:02:49.172Z" }, + { url = "https://files.pythonhosted.org/packages/47/db/d11bd6f258a60703dcdc7a3772818ad0c2f602ee4c2acfb24088c6c3ebc3/msgpack-1.2.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:6195257a107bf25872ef84aab7295078271eea3ac6413f0506b631f6c9586ed5", size = 372666, upload-time = "2026-08-27T10:02:50.886Z" }, + { url = "https://files.pythonhosted.org/packages/71/cf/fbbbac0c6e5fbb9d51abc23e3b5fe8620f5c01e0588797cf664a623bb9e1/msgpack-1.2.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b8dd6c71d20c28d2d0eb0c51e7cccf3584afde3b1364f6629596186c9025bd54", size = 410889, upload-time = "2026-08-27T10:02:52.51Z" }, + { url = "https://files.pythonhosted.org/packages/94/60/8366558da954095e04e7fbc351f9387d87a682feaee9a235ceda966f794b/msgpack-1.2.2-cp314-cp314-win32.whl", hash = "sha256:d242f3c4ccf55b056e6cf901720dccde58f1df117898f2bbf3bcd6e38ec7c248", size = 66774, upload-time = "2026-08-27T10:02:53.984Z" }, + { url = "https://files.pythonhosted.org/packages/4b/3d/1ce873c8057c65e4fbb076ffe1c99c9ae39d90a00a2540d7b06c652a292f/msgpack-1.2.2-cp314-cp314-win_amd64.whl", hash = "sha256:1510f24612d4b983dff6935d9273e02c320cfd525727fbcb58836a75f589fdbc", size = 73424, upload-time = "2026-08-27T10:02:55.277Z" }, + { url = "https://files.pythonhosted.org/packages/d5/55/e36f2a33e38657f33850d74e0bf256838a0d45802c298cc501a32bffcc08/msgpack-1.2.2-cp314-cp314-win_arm64.whl", hash = "sha256:7826f16edc763e768404f55605ef85dfcf5857e729c1ed29e0d7c180be4fe6d8", size = 67657, upload-time = "2026-08-27T10:02:56.493Z" }, + { url = "https://files.pythonhosted.org/packages/64/58/7e764b957bae80ae281a9cb28761068c8bae8d5c6ac0873e43cc69d176c7/msgpack-1.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f466049b8e1ec0854287bbe9a074316826fe0e08dcf707245f98b1ae49e92650", size = 86594, upload-time = "2026-08-27T10:02:57.796Z" }, + { url = "https://files.pythonhosted.org/packages/ae/f0/250f5985b6ee533e60d357571a808aaae03c54118294dc3db7158e27feb1/msgpack-1.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f6b6f8deb07d49090e1808c6ef9cb7d23ca17bef3aa6ed3e5e03df16606e60c", size = 87374, upload-time = "2026-08-27T10:02:59.256Z" }, + { url = "https://files.pythonhosted.org/packages/bd/2c/126ec8f187877c5f688631c543d1d3a3d75b2e66b83fb9de3ed7c13a39b6/msgpack-1.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b542ffc0a5c531eedc40419f291f1bd659aa8d4223408a5b51c88a2796083fd3", size = 428157, upload-time = "2026-08-27T10:03:00.9Z" }, + { url = "https://files.pythonhosted.org/packages/95/21/d2d81d50aaedb14147d01f22094185794db3ad8a8791b60afacba0627c89/msgpack-1.2.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d095df2627e5dd59ac7b0c5ad627a671c76e6020171e03cbe4621a61f0562c3", size = 426669, upload-time = "2026-08-27T10:03:02.457Z" }, + { url = "https://files.pythonhosted.org/packages/c6/fc/f7d484ee5b572719608e7ffad569bea22ff11309a96ca2fae85eec94226b/msgpack-1.2.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ffdd2f4950daf7815490f23087963e3420175b9609520b7ff5df64d351159c22", size = 380625, upload-time = "2026-08-27T10:03:04.244Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c4/b924cbd5516676f4e612329f18602a833bd055ffbe27f808eeba0f01bfea/msgpack-1.2.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:652d1bf13d01bac8fd569def0fe76745e55bcda01e30aa6332d5947ea3788839", size = 411328, upload-time = "2026-08-27T10:03:05.869Z" }, + { url = "https://files.pythonhosted.org/packages/27/9d/0c1d9683a951a80f270c3b7dac1022c18b9307617344dd44d904135d5e12/msgpack-1.2.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9bf452ff4d4981f25a18e9476e002bcc9263e7928024aa4d7148e25f7be3f929", size = 377892, upload-time = "2026-08-27T10:03:07.37Z" }, + { url = "https://files.pythonhosted.org/packages/06/bb/bf22338cdd22e0b40c8f28468cea5f3d9c320244c095d8303364bc012c41/msgpack-1.2.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:55faa6f8395e23b848c535ad5dcb96b3462f37f5e7f4ac500d500434f7345da7", size = 419426, upload-time = "2026-08-27T10:03:09Z" }, + { url = "https://files.pythonhosted.org/packages/7d/42/6d02c19a01abd8d7ce817c321d2ee6af1a8e24d584dca619d1b6576a83bf/msgpack-1.2.2-cp314-cp314t-win32.whl", hash = "sha256:419a45c67a5c04213172a14b1864657e014665b77d7081b107a51707923dd39e", size = 71810, upload-time = "2026-08-27T10:03:10.498Z" }, + { url = "https://files.pythonhosted.org/packages/af/df/fda3a204415dab0a8c0db5461ef7205416ea52bd8581c5cafd361be07f3b/msgpack-1.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:935b1cfad9b908b0fa845010f4271df4c2f04e1cd26e3f18acd61a45f93c9e36", size = 78919, upload-time = "2026-08-27T10:03:12.016Z" }, + { url = "https://files.pythonhosted.org/packages/63/d4/4b4b0ef25a86deca91feaf7252ca885ba4f2ada40461379120122a04fe96/msgpack-1.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11e8c421e117d1c36728b423d0402555cccbf0c6f53e288f0e75b6b12100d70f", size = 71925, upload-time = "2026-08-27T10:03:13.332Z" }, + { url = "https://files.pythonhosted.org/packages/3b/92/4b44bc8f3243ef8cf9cb5368c17a299d45b9df858f6dfdd98a0482dbbb37/msgpack-1.2.2-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:e1b99ad34613d5f8477fa5cf99bc4eaeaf27965588007c102370cd9a78fe9de5", size = 84293, upload-time = "2026-08-27T10:03:14.718Z" }, + { url = "https://files.pythonhosted.org/packages/80/05/c992bb65744665a41b5bf531fc0e1619bae0901f57738228ded90023c151/msgpack-1.2.2-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:0fbc1bed8a535389b41882cfae66376e248cd1680eaa94fd83193c73e1d24986", size = 84490, upload-time = "2026-08-27T10:03:16.12Z" }, + { url = "https://files.pythonhosted.org/packages/d7/bf/7f53b9e6709a4df7f9b9b81dc65f9dfaa32caf65bee94986ec2cb8fa07f1/msgpack-1.2.2-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:06d95f61de7afe4f4ff908a6feebfcb070d0582ac87c9cf3cedf8551cf634516", size = 405332, upload-time = "2026-08-27T10:03:17.692Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5a/305c4dca14b50d0b51fb88ef04ec125b8f0be3e2ce730dcc62dbaa651cc5/msgpack-1.2.2-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b5c696ae7cd7166b3657261adb855b461ff31f07823fdbae9de8bf80adfccc21", size = 416798, upload-time = "2026-08-27T10:03:19.389Z" }, + { url = "https://files.pythonhosted.org/packages/7a/df/a645102b4cdfd9a94201cac4e900e9c1429fc16d86aa311c06eef82528c9/msgpack-1.2.2-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0708afbf6a9587f0bfe479a9825c141d14d91e2f6a5c8103cf28bc96f4edb5d9", size = 377312, upload-time = "2026-08-27T10:03:20.928Z" }, + { url = "https://files.pythonhosted.org/packages/f4/26/c56d8d086d3fb1077bb48092b158b5ea2eee08b279e10c191275f13bc980/msgpack-1.2.2-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:226a62ffe99fe54c5c61d910ec64c3449b7766c3280bd286bf6c94838dde239a", size = 395182, upload-time = "2026-08-27T10:03:22.571Z" }, + { url = "https://files.pythonhosted.org/packages/4d/b5/3d46ba367a565e536d8d2a61eebcee71b1dc803da3ce74a22313b573d6fa/msgpack-1.2.2-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:9fd7f32e2f0fb334e7ecc5adb5cf0458785bd3a9d9d86f950e1715f101cebce5", size = 377945, upload-time = "2026-08-27T10:03:24.151Z" }, + { url = "https://files.pythonhosted.org/packages/a5/2c/d5d2df273ed5306357da25b69400fd8d7a53c4d87d8976604b677484d61c/msgpack-1.2.2-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:9db1ba1c1e6a84245a9dd866265b56b8a1e9461549cc72ed296d8cbfbd32961b", size = 413341, upload-time = "2026-08-27T10:03:25.85Z" }, + { url = "https://files.pythonhosted.org/packages/1b/fb/32613bced3cad47b40b1b73dd04d687121349d83f748efc2575929121903/msgpack-1.2.2-cp315-cp315-win32.whl", hash = "sha256:e2eb7ea0ac3911a7aac9d8aaa36d40f216d99455b3274cd3fac38181bcd910cf", size = 66730, upload-time = "2026-08-27T10:03:27.294Z" }, + { url = "https://files.pythonhosted.org/packages/74/56/d86171f7251015e9312e5a7f9fdd4cf89752fc2114b88fed453d2a040c66/msgpack-1.2.2-cp315-cp315-win_amd64.whl", hash = "sha256:9352e6cdb510a7b1a5d3ccaccec730e82e50cf3484a3af7bdaab19e23b9589ff", size = 73477, upload-time = "2026-08-27T10:03:28.615Z" }, + { url = "https://files.pythonhosted.org/packages/13/1a/56b90f6defef61700b86baca3637c15f62ac0f9b21ab0f16613ab9d1f101/msgpack-1.2.2-cp315-cp315-win_arm64.whl", hash = "sha256:29cc2d5291711a52956a79a51f41c732329df39ad727c886bd8f0b5b9237a808", size = 67660, upload-time = "2026-08-27T10:03:29.895Z" }, + { url = "https://files.pythonhosted.org/packages/cd/20/12751ca0d8ec874701b54c392c2b19f51af8dd1de40a92a10e356f0aaf58/msgpack-1.2.2-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:d886baa46b2532135e7320067e6a44edb09ba5883a6096b0f9c044533984b8a8", size = 86462, upload-time = "2026-08-27T10:03:31.348Z" }, + { url = "https://files.pythonhosted.org/packages/91/4c/cf6d12a3d709fe5f9771dd917c35e6ebcd55597a5b792287382fde056c95/msgpack-1.2.2-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:53679573c75cce5f82359e0bd4e6a97809a6b9a9b7a48fd1ba592f4a82cddc84", size = 87412, upload-time = "2026-08-27T10:03:32.74Z" }, + { url = "https://files.pythonhosted.org/packages/ca/0d/0aac5752d1708dcb458f8754db34a4999514db3df2d2b798b9381293f638/msgpack-1.2.2-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d3c247d457ae9079974c7ce3c665396754a6d2baff7eaa51332212a8a5a3f13b", size = 422057, upload-time = "2026-08-27T10:03:34.124Z" }, + { url = "https://files.pythonhosted.org/packages/81/30/70f281a3685b04aaf235a5237da11b978a02a865a5a479186205177ad676/msgpack-1.2.2-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:352ed831042549cca8be23780e1fe7c9177e65ff02bf183509c4b4d33f671782", size = 422696, upload-time = "2026-08-27T10:03:35.862Z" }, + { url = "https://files.pythonhosted.org/packages/8a/6d/f76e8425efb0aa38988cd778ae290bfa120491d80d26872d88bb52fedb3f/msgpack-1.2.2-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f80361592c13d7226b4379c8941529b63fe1a9d0e05d2de8f3306b70e522b53f", size = 376495, upload-time = "2026-08-27T10:03:37.339Z" }, + { url = "https://files.pythonhosted.org/packages/95/77/0809aa9b52b2868f7d01862dc14073708f0440421a65197b48453480034c/msgpack-1.2.2-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:68df2947921d449f6dcfeafd86cb2cdde13327a8b447534bbe4ee5aaf32a5695", size = 404683, upload-time = "2026-08-27T10:03:38.87Z" }, + { url = "https://files.pythonhosted.org/packages/02/d2/4e5ac915ba120172d210ef00165c5e6276c8a65db3a4a5cf36e946b83e23/msgpack-1.2.2-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:51dd39d23cfdea0400ed3ff2d29d1e83bd951d3aea79dc89be5b701a09edfe23", size = 375087, upload-time = "2026-08-27T10:03:40.486Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e3/8051d53e5495c87c6cf27eb42fb680361017037f87f322bdaf525f71e4a2/msgpack-1.2.2-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:b13b59e66f107cca1ba708dd5307179870ca1b15b19fcee7ccf722e5308d9212", size = 414421, upload-time = "2026-08-27T10:03:42.308Z" }, + { url = "https://files.pythonhosted.org/packages/ba/4e/13783aa7c17414d7186c72c49bc718366f75e49f0ea58d4f81cb63ac3187/msgpack-1.2.2-cp315-cp315t-win32.whl", hash = "sha256:8c6321a414f8b4a8dc43976b2fa8349156434ca9adedd9a187b796f7e1d3d3fc", size = 71790, upload-time = "2026-08-27T10:03:43.715Z" }, + { url = "https://files.pythonhosted.org/packages/5e/9d/1d02994c7ae2603c98100984428ff0f67443572133bc18eca6058f732c1b/msgpack-1.2.2-cp315-cp315t-win_amd64.whl", hash = "sha256:6f53285f20d592ed309ee19e509cc4c77a3bda1db02ad67e8a0949bb227a5a6d", size = 78766, upload-time = "2026-08-27T10:03:45.036Z" }, + { url = "https://files.pythonhosted.org/packages/60/54/89ed16e6f966a050dc78b0e94a545025211b07ce9f4bdfe07dff70c03fc2/msgpack-1.2.2-cp315-cp315t-win_arm64.whl", hash = "sha256:a378e12ccc06d76efde115caf4073b7e5ff3cc18291d1341f9e65fb882e3f754", size = 71819, upload-time = "2026-08-27T10:03:46.375Z" }, +] + +[[package]] +name = "narwhals" +version = "2.25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/7b/6248dada39781db1ab3ebf08943080df0796098515a87f6f8696d14ec744/narwhals-2.25.0.tar.gz", hash = "sha256:62c036c810662bf7820b7737077176313bc59350eeeefb808510f388c743e4b2", size = 677076, upload-time = "2026-08-20T18:10:15.454Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/dc/55481808fd70ef1567cf13540ffd4702af3f74b112e35427564b03f79c2d/narwhals-2.25.0-py3-none-any.whl", hash = "sha256:1f0f403e8c7e4463cde9bfe78b12fdd809e3ae3dda6d9b2f802934fb9c7a6a8f", size = 467373, upload-time = "2026-08-20T18:10:13.834Z" }, +] + +[[package]] +name = "networkx" +version = "3.4.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/fd/1d/06475e1cd5264c0b870ea2cc6fdb3e37177c1e565c43f56ff17a10e3937f/networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1", size = 2151368, upload-time = "2024-10-21T12:39:38.695Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl", hash = "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f", size = 1723263, upload-time = "2024-10-21T12:39:36.247Z" }, +] + +[[package]] +name = "networkx" +version = "3.6.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13'", + "python_full_version == '3.12.*'", + "python_full_version == '3.11.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, +] + +[[package]] +name = "numba" +version = "0.67.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llvmlite" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "numpy", version = "2.5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/90/2544f4e3a61e501d6c9a5418fd4b905323222693d54a02cab0106a0af865/numba-0.67.0.tar.gz", hash = "sha256:cd75aa535b33fa05d9d930b1ae8af9f97a2881e96d72dfb38ec9b78284d9f851", size = 2836515, upload-time = "2026-08-11T23:04:00.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/2e/6e72b3edbb7c7d6b44b2ca9e1b62e91997415d181541ef47fc6957c59bf2/numba-0.67.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:8c0e88acd4341ddf40779db3c0228b9188aca7fcab5f5f3ce9949a1fc71e9a02", size = 2745135, upload-time = "2026-08-11T23:03:08.321Z" }, + { url = "https://files.pythonhosted.org/packages/e5/17/5358f24235ef1a5a80b7e28f3e1baa886c0bcf07dc68557009284e6ba698/numba-0.67.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d6c8e9ba3f9602471e8c6f563ffcce8db8046741f0bafb782a052e41dc6b6861", size = 3821881, upload-time = "2026-08-11T23:03:11.172Z" }, + { url = "https://files.pythonhosted.org/packages/0e/18/2f00694248e32c53812baf3d36a7c656dbdd667c6993087b3da068f74b02/numba-0.67.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:694c81c6560b2b47e5fc1dc39c29175b907adf862d9af0af801453400a022a61", size = 3528397, upload-time = "2026-08-11T23:03:13.107Z" }, + { url = "https://files.pythonhosted.org/packages/7f/39/4175b074929938011bd4b564beb4e0fcffd46252e01f60602b57ffb02b06/numba-0.67.0-cp310-cp310-win_amd64.whl", hash = "sha256:ed333e0af4386294e7f03e550e01411856b6935e717d859225e0a7338c6b6795", size = 2815861, upload-time = "2026-08-11T23:03:15.072Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ed/55ba4e54ee878396de6b18e6533cc4a92fa519e8c82d55cf40f98c0a6831/numba-0.67.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:3fa3d1b27f96f2c0d54513d953d7197886aa1eaa7d2439a0eedc44d993fb181a", size = 2744821, upload-time = "2026-08-11T23:03:17.321Z" }, + { url = "https://files.pythonhosted.org/packages/be/78/3f3c45dbaec3cf02bbb1825731beca50f591227e95143d6bd7a64897641c/numba-0.67.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8c80c847301dc33dc8f84a97a952004023d9a05578ae4512b087176264cc1960", size = 3827182, upload-time = "2026-08-11T23:03:19.684Z" }, + { url = "https://files.pythonhosted.org/packages/a5/24/4e70cb86534283d859c3aea2302da523e41539b98dd6c3c4d0a42af95cda/numba-0.67.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e7a7b0121466f1e9a8a074b0545fe90e16389623abf979b5d7c299dca1294d7e", size = 3532817, upload-time = "2026-08-11T23:03:22.06Z" }, + { url = "https://files.pythonhosted.org/packages/26/4d/23dab7f4233be0fc34f54a169ed85238467cd24d8adf2498e5c12ea19dc7/numba-0.67.0-cp311-cp311-win_amd64.whl", hash = "sha256:cfba1ac34f0363fb1a250a10e97240780d11e05227892f7286b26fbfd0ad58ce", size = 2815700, upload-time = "2026-08-11T23:03:23.812Z" }, + { url = "https://files.pythonhosted.org/packages/0d/58/915cddba90010348ed0444451132fdde9b000bcbaff1582029b5bf115d11/numba-0.67.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:6004d8d5f28d4028687fb2d972d629295b13685943bd2ed5cd8810c3b848e219", size = 2745050, upload-time = "2026-08-11T23:03:25.607Z" }, + { url = "https://files.pythonhosted.org/packages/bb/38/926757caaac18a66f057d7544a63620bf360a07d281c9f7ecadd2aa83963/numba-0.67.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f63d43db06b4756424d6d2484737c902e0ae944a0eec3e8b0b4de2c695b15caa", size = 3884596, upload-time = "2026-08-11T23:03:27.688Z" }, + { url = "https://files.pythonhosted.org/packages/4d/6d/58291dc58da39d98b32db7f044729f6d8d4920cd9622fbab3179b54ff4c4/numba-0.67.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:76d3335aaeffb9dc88309420890e73497a00be08a7530441bc2b58ffe025bfa5", size = 3585290, upload-time = "2026-08-11T23:03:29.684Z" }, + { url = "https://files.pythonhosted.org/packages/6e/63/ab21828b4056afed71f9ecb40f4de26c2c19de731cc001961aca74b79464/numba-0.67.0-cp312-cp312-win_amd64.whl", hash = "sha256:50e2b72406c18cda5dd7431b0082cb85ea94e06c64c33607248fc8bef92cfb81", size = 2815645, upload-time = "2026-08-11T23:03:31.732Z" }, + { url = "https://files.pythonhosted.org/packages/49/dd/bd9fe772f6c84597b76cac229b3f2890f01a2c64fd70e48ceaae10dd65cb/numba-0.67.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:77e1c7173fee57a0d84e006c7e70346689d6cb3e7db503489bae58646b4eff7b", size = 2744872, upload-time = "2026-08-11T23:03:33.649Z" }, + { url = "https://files.pythonhosted.org/packages/a1/1c/c05609739cc41116d36e30cb2b41fb00f126bb52e1b0bac907051ad8a35d/numba-0.67.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9c4953387c77864b596d8296e2cfbdef82b0eea4166ab4864b05d226c51143e0", size = 3892004, upload-time = "2026-08-11T23:03:35.797Z" }, + { url = "https://files.pythonhosted.org/packages/4a/77/a5276ad4178250403e0e2251f3e1f8ac18feac779b0474a8bcb08558490d/numba-0.67.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:88f6e0f5cb6c545e158b6ef0496c01b6d6958a7ccc6634a1576a94bbbab29ff2", size = 3591878, upload-time = "2026-08-11T23:03:37.845Z" }, + { url = "https://files.pythonhosted.org/packages/0f/80/d48f0ba7442516ceb5a1585f0c81d3aa531bc96bfcabcd9f8f925768c426/numba-0.67.0-cp313-cp313-win_amd64.whl", hash = "sha256:b68ad5125fe245339cc8dcc036081fc1ea482c5063387b9612a76ccd83dc91cd", size = 2815504, upload-time = "2026-08-11T23:03:39.736Z" }, + { url = "https://files.pythonhosted.org/packages/d7/16/345b1e4774a08247aafcfdb93d4e8d24a3646366cbe72de33053fc0de1b5/numba-0.67.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:f99f880ff25f418a67f9a1d00d0ddfbc63430f627b523e515085a592a7567f4b", size = 2745088, upload-time = "2026-08-11T23:03:41.864Z" }, + { url = "https://files.pythonhosted.org/packages/1b/36/e614ba2bc0f005ed0f37a6413f08fe705210297ddb9a37a475a8b9fdab61/numba-0.67.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5269245a675abdd3e2c35ec6bb2f250355effa9032514d8f2354f0d2d10854bd", size = 3861040, upload-time = "2026-08-11T23:03:43.842Z" }, + { url = "https://files.pythonhosted.org/packages/40/90/30c42a1dbc4176cf355e8e8be61803732c55597b1332925fe233912a43d9/numba-0.67.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f074a8e23db78490f11a3930c940be758316c10ac5985be83d2f298dc080acf7", size = 3561811, upload-time = "2026-08-11T23:03:46.037Z" }, + { url = "https://files.pythonhosted.org/packages/18/6d/21bd16f770476e394c5e5f504935817967442a71251d6b86c244a2767980/numba-0.67.0-cp314-cp314-win_amd64.whl", hash = "sha256:4d576e62bf2c9370f61312b51573c4bb1f3fe96798bbab56730847a368a316c4", size = 2817421, upload-time = "2026-08-11T23:03:47.922Z" }, + { url = "https://files.pythonhosted.org/packages/95/06/bb41b0e59b9ff52c94a2f01db24f6437df058caebb377b5f372fc343a6a2/numba-0.67.0-cp314-cp314-win_arm64.whl", hash = "sha256:7930748ce8355d2a5a28602abab056a61fdc676d17377f27d17993905428171f", size = 2788885, upload-time = "2026-08-11T23:03:49.967Z" }, + { url = "https://files.pythonhosted.org/packages/10/7c/aa07151fbd0f4283f78de437cc196f9084789be89a2b4de3fdc2f6a4b414/numba-0.67.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:4a2ed006635bbd0fe45681ed49f3b4f4bad1abf0c233bcc5842c9e3a34cabd61", size = 2748150, upload-time = "2026-08-11T23:03:51.755Z" }, + { url = "https://files.pythonhosted.org/packages/74/62/b8174ca95a4cc1a7ba1520767734e016991545590b8fbde521b681701a9f/numba-0.67.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aa5f002f665bec321b950dacaa26ee009e1d720f6ac9d9856eed5efe1caa03a6", size = 3896986, upload-time = "2026-08-11T23:03:53.752Z" }, + { url = "https://files.pythonhosted.org/packages/8c/f9/3a7b6dbf81e01a48958b45ad2239edbc64707522ab17f11f9f18c44bf6d1/numba-0.67.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83ab968b0e0fa744eba03351282dd8000796e6ec8e4518f47bd3ed86c0a20c7b", size = 3614644, upload-time = "2026-08-11T23:03:55.794Z" }, + { url = "https://files.pythonhosted.org/packages/d9/5b/248f5681c121ca853a9f4e39d342a3e01b8a0261b0275853eb3d0d56aa20/numba-0.67.0-cp314-cp314t-win_amd64.whl", hash = "sha256:00c964a5b94d3ae82d83ac162cd610755875b98dadb779fdde06e6bfcdbca47e", size = 2822870, upload-time = "2026-08-11T23:03:58.097Z" }, +] + +[[package]] +name = "numpy" +version = "2.2.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245, upload-time = "2025-05-17T21:27:58.555Z" }, + { url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048, upload-time = "2025-05-17T21:28:21.406Z" }, + { url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542, upload-time = "2025-05-17T21:28:30.931Z" }, + { url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301, upload-time = "2025-05-17T21:28:41.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320, upload-time = "2025-05-17T21:29:02.78Z" }, + { url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050, upload-time = "2025-05-17T21:29:27.675Z" }, + { url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034, upload-time = "2025-05-17T21:29:51.102Z" }, + { url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185, upload-time = "2025-05-17T21:30:18.703Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149, upload-time = "2025-05-17T21:30:29.788Z" }, + { url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620, upload-time = "2025-05-17T21:30:48.994Z" }, + { url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963, upload-time = "2025-05-17T21:31:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743, upload-time = "2025-05-17T21:31:41.087Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616, upload-time = "2025-05-17T21:31:50.072Z" }, + { url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579, upload-time = "2025-05-17T21:32:01.712Z" }, + { url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005, upload-time = "2025-05-17T21:32:23.332Z" }, + { url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570, upload-time = "2025-05-17T21:32:47.991Z" }, + { url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548, upload-time = "2025-05-17T21:33:11.728Z" }, + { url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521, upload-time = "2025-05-17T21:33:39.139Z" }, + { url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866, upload-time = "2025-05-17T21:33:50.273Z" }, + { url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455, upload-time = "2025-05-17T21:34:09.135Z" }, + { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348, upload-time = "2025-05-17T21:34:39.648Z" }, + { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362, upload-time = "2025-05-17T21:35:01.241Z" }, + { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103, upload-time = "2025-05-17T21:35:10.622Z" }, + { url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382, upload-time = "2025-05-17T21:35:21.414Z" }, + { url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462, upload-time = "2025-05-17T21:35:42.174Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618, upload-time = "2025-05-17T21:36:06.711Z" }, + { url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511, upload-time = "2025-05-17T21:36:29.965Z" }, + { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783, upload-time = "2025-05-17T21:36:56.883Z" }, + { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" }, + { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" }, + { url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828, upload-time = "2025-05-17T21:37:56.699Z" }, + { url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006, upload-time = "2025-05-17T21:38:18.291Z" }, + { url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765, upload-time = "2025-05-17T21:38:27.319Z" }, + { url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736, upload-time = "2025-05-17T21:38:38.141Z" }, + { url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719, upload-time = "2025-05-17T21:38:58.433Z" }, + { url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072, upload-time = "2025-05-17T21:39:22.638Z" }, + { url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213, upload-time = "2025-05-17T21:39:45.865Z" }, + { url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632, upload-time = "2025-05-17T21:40:13.331Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532, upload-time = "2025-05-17T21:43:46.099Z" }, + { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885, upload-time = "2025-05-17T21:44:05.145Z" }, + { url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467, upload-time = "2025-05-17T21:40:44Z" }, + { url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144, upload-time = "2025-05-17T21:41:05.695Z" }, + { url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217, upload-time = "2025-05-17T21:41:15.903Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014, upload-time = "2025-05-17T21:41:27.321Z" }, + { url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935, upload-time = "2025-05-17T21:41:49.738Z" }, + { url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122, upload-time = "2025-05-17T21:42:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143, upload-time = "2025-05-17T21:42:37.464Z" }, + { url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260, upload-time = "2025-05-17T21:43:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225, upload-time = "2025-05-17T21:43:16.254Z" }, + { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374, upload-time = "2025-05-17T21:43:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391, upload-time = "2025-05-17T21:44:35.948Z" }, + { url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754, upload-time = "2025-05-17T21:44:47.446Z" }, + { url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476, upload-time = "2025-05-17T21:45:11.871Z" }, + { url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666, upload-time = "2025-05-17T21:45:31.426Z" }, +] + +[[package]] +name = "numpy" +version = "2.4.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.11.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/d0/ad/fed0499ce6a338d2a03ebae59cd15093910c8875328855781952abf6c2fe/numpy-2.4.6.tar.gz", hash = "sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda", size = 20735807, upload-time = "2026-05-18T23:37:14.07Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/49/ec46835a70be8fa6446c495126ac84fdb28cb2558e1620ffb87a10c8b64c/numpy-2.4.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0280e0356c0829a18d9de1cb7eee50ec22ca639878d7240307ca0943d73cd2c4", size = 16969194, upload-time = "2026-05-18T23:33:13.503Z" }, + { url = "https://files.pythonhosted.org/packages/0e/0d/f5957185c0ee2f3e12f78715aa9e3b353fd83633316c8532b38faa37e3f6/numpy-2.4.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:110f8b71aacb688ec69062bb7f6938a0f8acb01b7c1c4beb453c65b6d234584d", size = 14964111, upload-time = "2026-05-18T23:33:17.795Z" }, + { url = "https://files.pythonhosted.org/packages/ad/40/40a40ee0ddf7ceb782c49af278894b686e586d65d8c1889c8b5da01a3d7d/numpy-2.4.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:4cfe66903cc32a9921a6733d96b19bb6abf310397581bbad89c228f5abaf0ee8", size = 5469159, upload-time = "2026-05-18T23:33:20.654Z" }, + { url = "https://files.pythonhosted.org/packages/63/13/f9a8046535cb21deae82f8d03de9617e08882d274fad2539630761888228/numpy-2.4.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8155154c7c691289fe18f510b5d4657c68c67989f293f0535a91360392ff6538", size = 6798936, upload-time = "2026-05-18T23:33:22.987Z" }, + { url = "https://files.pythonhosted.org/packages/33/a8/6fa8c1a345a8c85dbb21932c447bee07c30a2c2a3f31e369c0a84b300147/numpy-2.4.6-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ab0a9c4ffb1a6d95ef519fe4247dba8eb6b18ad93999f76b7f657039acabd47", size = 15966692, upload-time = "2026-05-18T23:33:26.62Z" }, + { url = "https://files.pythonhosted.org/packages/02/03/74fe2a4cb3817d94d86402f2506554130a2f01414e299b5a843e5a8a957f/numpy-2.4.6-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89cd468399cfd2504718f0ba50e410dca55a170b61a02ad92bb18c8a65186e93", size = 16918164, upload-time = "2026-05-18T23:33:29.955Z" }, + { url = "https://files.pythonhosted.org/packages/c5/80/3615be3313f7e7696609bc194b9f0101da809df79e859bdb84e0cd043f46/numpy-2.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c2d37ab77531417474168eb79d6d80b14f821a966818505d03013d0833edb7a8", size = 17322877, upload-time = "2026-05-18T23:33:34.724Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ac/a691e0fe2675e370d0e08ff905adc49a1c8830e8cae03efe4477e92cd55d/numpy-2.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f407cb6b8e9d6d8c626bc73c945db1706035af8fd632295547bf1c9e46d092d6", size = 18651487, upload-time = "2026-05-18T23:33:38.217Z" }, + { url = "https://files.pythonhosted.org/packages/15/a7/9bc1cd626d7bf6869bfedf27b91b6ab5dd607758bf8e959d6fa80c6a59cb/numpy-2.4.6-cp311-cp311-win32.whl", hash = "sha256:ddea102b48f9e339f3948bf22040944184627a30fdf7f858667673b9c5f033c8", size = 6233945, upload-time = "2026-05-18T23:33:41.331Z" }, + { url = "https://files.pythonhosted.org/packages/c5/31/7fc6239c12bce7e931463251cca4426c465e1876ba3cc785402ef4dd8f4e/numpy-2.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:1e254a00cdf42b1e4d5b3d68d33af63268d41340d8885df2ab6470f2e1500147", size = 12608406, upload-time = "2026-05-18T23:33:44.131Z" }, + { url = "https://files.pythonhosted.org/packages/27/83/140f85a466595a16382996a1bf06b2b54bcd597488921b0c9daaeeda72af/numpy-2.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:ed9749eef4cbd126da3dc1d6bcb3a57f5eb7ac6a6484146bdbf743f552dfc577", size = 10479528, upload-time = "2026-05-18T23:33:50.725Z" }, + { url = "https://files.pythonhosted.org/packages/95/2a/3d7b5ac8aac24feaf9ad7ed58f45b0bbc06d37e4338ae84c9f2298b570f9/numpy-2.4.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:001fbb8e08d942dd57599e781f2472269ee7f2755fae407b4f67b2f0b17da3f1", size = 16689119, upload-time = "2026-05-18T23:33:54.065Z" }, + { url = "https://files.pythonhosted.org/packages/ea/12/92c4c131527599e8288d6918e888d88726f84d805d784b771f32408aeaef/numpy-2.4.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ebfb099f8dcf083deef3ac1ca4c1503f387cf76296fcb3816b66f5ecb5f54fdb", size = 14699246, upload-time = "2026-05-18T23:33:57.621Z" }, + { url = "https://files.pythonhosted.org/packages/ad/fe/c0a6b7b2ca128a8fb228575147073b660656734b8ebe4d76c8fd748dcc79/numpy-2.4.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3213d622a0283a39a93d188f3cf72b26862df52fbb4ca3697f51705016523d41", size = 5204410, upload-time = "2026-05-18T23:34:00.302Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d4/9770d14ba719432bb90a421bfd443872ed0f70f7264b64bec12ea363d5fd/numpy-2.4.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:357cc07a6d7b0b182ff02249616a03742827ebb1277546b5c7cd7f7620a45698", size = 6551240, upload-time = "2026-05-18T23:34:02.852Z" }, + { url = "https://files.pythonhosted.org/packages/c9/c6/50a46a6205feba2343f1d6d17438107c5dc491ed1c736e6ea68689fd906b/numpy-2.4.6-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f9fb9157b4ce2971008323afe46053787b526ef624fea915b261468a8421a0f", size = 15671012, upload-time = "2026-05-18T23:34:05.485Z" }, + { url = "https://files.pythonhosted.org/packages/99/60/14115e6364fa676c5397c2ad3004e527e9aa487abf5d0706ec81bbd08529/numpy-2.4.6-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90f9849678c75fe7afa2d348ac842c168b0a4d3d61919687216dfc547976d853", size = 16645538, upload-time = "2026-05-18T23:34:09.265Z" }, + { url = "https://files.pythonhosted.org/packages/ae/c5/693cbe59e57db94d2231fa519ca3978dc9e19da5a8f088588f5c6e947ff2/numpy-2.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c1a2af6c6ef86344a6b0db6b97834208bf598db514f2b155042439b62605601a", size = 17020706, upload-time = "2026-05-18T23:34:13.053Z" }, + { url = "https://files.pythonhosted.org/packages/ef/fc/85b7c4eff9b4966ade25c2273cf7e7012e92366c032058653934b37de044/numpy-2.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5805d5a22fd19c8ccff10a9561f9df94436b0545619ea579db2d3c35294bce2", size = 18368541, upload-time = "2026-05-18T23:34:17.024Z" }, + { url = "https://files.pythonhosted.org/packages/f6/81/e1b27545deedce7f4a0b348618c6b62d74e36a4dc9ccd42f3eb2f85eee32/numpy-2.4.6-cp312-cp312-win32.whl", hash = "sha256:e3eeb0aabd6bd5ce64faae67e9935203a6991b4bc2a485a767fbafb2c5125f45", size = 5962825, upload-time = "2026-05-18T23:34:20.3Z" }, + { url = "https://files.pythonhosted.org/packages/ab/ca/feab00bd44aa5fe1ad2c18f08b4d3bb92e26484b0b1d1443897809ed528c/numpy-2.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:d8e8286dd7cea7895157318d1b91cdacac64c479f3cbc8dce548331728484751", size = 12321687, upload-time = "2026-05-18T23:34:23.095Z" }, + { url = "https://files.pythonhosted.org/packages/63/cf/5a6d34850a39d1093558564f77ee8e8e0bee5061151b8f05a55711001ec7/numpy-2.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:4081eb135ac24158bd51cdfbef16f1c64df7063b1143f24731387137c092bec8", size = 10221482, upload-time = "2026-05-18T23:34:25.876Z" }, + { url = "https://files.pythonhosted.org/packages/fb/82/bdab26d7438c6791ca31b7c024ca37c1eab8b726ba236129005cd4a06e45/numpy-2.4.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:511dbaf848decaaaf4b4ca48032619fb3138710c4bf7da7617765edad1ef96b0", size = 16684648, upload-time = "2026-05-18T23:34:29.41Z" }, + { url = "https://files.pythonhosted.org/packages/1b/30/a80189bcc7f5e4258b3fbc3968d909d1756f54d023299ecc39ad6fdb9ef8/numpy-2.4.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bf162abab1c1a736333192707cef898e735a5ca00f38f27eeedf44b39d9e85eb", size = 14693902, upload-time = "2026-05-18T23:34:33.013Z" }, + { url = "https://files.pythonhosted.org/packages/97/12/70b5d0d7c15e1ebb8a6a84a8caa1d19e181d84fb58bb6d70aca29099dec1/numpy-2.4.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:043191bfa8eab18c776647b62723ac9dddece59743b13f49b2016094129c2b3f", size = 5198992, upload-time = "2026-05-18T23:34:36.132Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8c/ebd2a8f8a83541f8d38cc5667e8c2b69cecfd30da6e45693e8158857d44b/numpy-2.4.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:6180d8b35af935aed8ece3a85e0a43f87393ae0ac87c8d2c8bd2c993f7270ef3", size = 6546944, upload-time = "2026-05-18T23:34:38.484Z" }, + { url = "https://files.pythonhosted.org/packages/bb/c5/7b863a97a91671a0338f4253bd3b5a3d3852f0692dae91711c9f4a10e787/numpy-2.4.6-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72fbe16c6fac95aedf5937fa873445cec2110be35d8a4e9433d7501fd98dae6b", size = 15669392, upload-time = "2026-05-18T23:34:41.257Z" }, + { url = "https://files.pythonhosted.org/packages/a5/9d/3584b9984ca4c047aea75214ce1a4c4c73d849bd71b604264b7f5653f8a8/numpy-2.4.6-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7830bab239b79cda9c08c2da014761cafb48da6150e1da17ac06283f43b6089", size = 16633220, upload-time = "2026-05-18T23:34:45.075Z" }, + { url = "https://files.pythonhosted.org/packages/05/ae/7c67fba23bd98caec7c99261f3a16072ade14813486b0282cb29846de832/numpy-2.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ef4aea96ce4d3b074422cb4f2f64e216bf9e213004bb58ecfdf50ea02ea8eb9a", size = 17020800, upload-time = "2026-05-18T23:34:49.065Z" }, + { url = "https://files.pythonhosted.org/packages/d9/5d/3b6725cb31d983c5e66916f5d36f6d7e5521129e4c4404d64f918292a5b6/numpy-2.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dfa20cc6ca228e6b155b11da03825975ce66aea520985dbbddf0f2a5a495c605", size = 18357600, upload-time = "2026-05-18T23:34:52.709Z" }, + { url = "https://files.pythonhosted.org/packages/f7/da/2ccc6c2fe8898dee01d90c75c5f5f914a23daf99e3e0f59516a08760c8b5/numpy-2.4.6-cp313-cp313-win32.whl", hash = "sha256:56b39e5e0622a09a25bf5baf62f4bcf0cb8a41ae6e2819cf49bbc5a74c083f91", size = 5961134, upload-time = "2026-05-18T23:34:55.618Z" }, + { url = "https://files.pythonhosted.org/packages/b5/cd/9cc4dc876fb065d5c220aae4d5e14826b2715331bb7618ce1fb07a679d99/numpy-2.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:c4fc99836233ea196540b17ab0983aff60ed07941751930f5f4d05bc3b3b7359", size = 12318598, upload-time = "2026-05-18T23:34:58.928Z" }, + { url = "https://files.pythonhosted.org/packages/39/1e/c0bcba1f8694116485fe28fd1be698c278fcda4141c5b0e53a2aed8b12a8/numpy-2.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a7c711e21628b52034bb5ab8d1bce291f752fcc5e92accc615778acee1ff4778", size = 10222272, upload-time = "2026-05-18T23:35:02.167Z" }, + { url = "https://files.pythonhosted.org/packages/63/6d/cc5619247c8f4204e507f5883528372e4ac4bb189e579fb859a12e480b1f/numpy-2.4.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:112b06a867b235ef466ed3508ddf0238050df9c727cafb5301ac385b899189a1", size = 14821197, upload-time = "2026-05-18T23:35:05.468Z" }, + { url = "https://files.pythonhosted.org/packages/00/58/f1c39161c87d9e9bed660f1ed4bafc0e403d5ec9650b6dd77aead07d489b/numpy-2.4.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:eaf7fa2de5c0be8ae6ff8e9bea2ccd725e980541244521d8d4b5f3354a27babe", size = 5326287, upload-time = "2026-05-18T23:35:08.693Z" }, + { url = "https://files.pythonhosted.org/packages/af/57/3917ab0fd97f271a8694513581b8a36c655f111c446852c302f04ccdb6fc/numpy-2.4.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:7265a2f3d436e54ef9f2b52b5c937e6be778781bd97a590319d7348f1c1ca997", size = 6646763, upload-time = "2026-05-18T23:35:11.459Z" }, + { url = "https://files.pythonhosted.org/packages/eb/0f/037e64c494b67581ae18193d770adef354c41f3f2c8ebf865602d949bf8f/numpy-2.4.6-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f74a575920ab21fe304421a3fc28793d82e299cae9eccb37084e9fc7f3617c20", size = 15728070, upload-time = "2026-05-18T23:35:14.79Z" }, + { url = "https://files.pythonhosted.org/packages/21/a6/5d2bae9c9542eb4df16dc9c46dc79c186e9bad53805dfa5399a6023c6db0/numpy-2.4.6-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ede83e07a75dd06bc501566c1eca2afc0d61677c1472ac9ad93fdee6e638a48d", size = 16681752, upload-time = "2026-05-18T23:35:18.836Z" }, + { url = "https://files.pythonhosted.org/packages/92/14/23d1dfb410ae362cd59ce53e936b1513d545eb40db3949ced632e19a459e/numpy-2.4.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:68bb27509ac1b9a3443094260f6326150663b06abe40b73a2f81160623da5b67", size = 17086024, upload-time = "2026-05-18T23:35:22.52Z" }, + { url = "https://files.pythonhosted.org/packages/4b/6e/23595a2c642cdf3bc567877064bdd7f91c8b0038a4453cf2daf7248eafe9/numpy-2.4.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a0df0043bdb289bde1f62da130d20df23d58b45429f752bc7a8fc5325a225ecd", size = 18403398, upload-time = "2026-05-18T23:35:26.398Z" }, + { url = "https://files.pythonhosted.org/packages/8a/90/0ac3bc947217e66dec77e7cbc6a1979d1af70b6461b82f620d3bccd5e4c8/numpy-2.4.6-cp313-cp313t-win32.whl", hash = "sha256:29a287e0cf63ff528da061de6b9f64a4618da591ca1046aafc54062e40ca7eab", size = 6084971, upload-time = "2026-05-18T23:35:29.387Z" }, + { url = "https://files.pythonhosted.org/packages/77/71/5673e351671a1d2bd6063b91b44f70c0affea7d1516fa7a6572941ba4aa1/numpy-2.4.6-cp313-cp313t-win_amd64.whl", hash = "sha256:25c692919ac5a01f170a3bfcd62d745b24fd095c353d50812637d6fcab442e75", size = 12458532, upload-time = "2026-05-18T23:35:32.175Z" }, + { url = "https://files.pythonhosted.org/packages/3f/88/19d3503c5046e688f049274b27a3ef3d771152fa80d3ba3d01a3dff61abe/numpy-2.4.6-cp313-cp313t-win_arm64.whl", hash = "sha256:1e978ec1e8bd0e0e4de6bb75de9d30cbb74db6b6a2bb727618613703ca0167dd", size = 10291881, upload-time = "2026-05-18T23:35:35.465Z" }, + { url = "https://files.pythonhosted.org/packages/f8/91/3ab2044d05fd16d343c5ac2e69b127f1b2854040dd20b193257c78028bd3/numpy-2.4.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06ca2f61ec4385a07a6977c55ba998a4466c123642b4a32694d3128fce18c079", size = 16683458, upload-time = "2026-05-18T23:35:38.353Z" }, + { url = "https://files.pythonhosted.org/packages/8e/62/764ce66fa4147ae6d73071a3abf804ffe606f174618697c571acdf26a7c9/numpy-2.4.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:38efbc8de75c7a0fc1ac190162d892787f3f47b57cc291231aafee36b80982b7", size = 14704559, upload-time = "2026-05-18T23:35:42.14Z" }, + { url = "https://files.pythonhosted.org/packages/60/61/23f27c172f022e04025b7dc2367f4d63c1a398120607ec896228649a6f48/numpy-2.4.6-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:d581b735e177fdcdce6fed8e7e8880a3fb6ee4e3653a3ac6af01c6f4c03effc5", size = 5209716, upload-time = "2026-05-18T23:35:45.377Z" }, + { url = "https://files.pythonhosted.org/packages/03/71/21cf70dc6ea3e3acb95fc53a265b2fc248b981f0194ceb5b475271b8809d/numpy-2.4.6-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:0a041d3d761dc3c35cc56ce0351506a02bcbc25f7b169f652435141a17db9096", size = 6543947, upload-time = "2026-05-18T23:35:47.926Z" }, + { url = "https://files.pythonhosted.org/packages/d5/91/64288395ee1799bd2e0b04a305dce9666da90c961e1f3fe982a05ee1c036/numpy-2.4.6-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40fdc1ae7125e518ea98e53e69a4ebc27e1fd50510c47b7ea130cf21e5e1d42b", size = 15685197, upload-time = "2026-05-18T23:35:50.863Z" }, + { url = "https://files.pythonhosted.org/packages/f3/eb/ebffaa97dc55502df69584a8f0dcf07f69a3e0b3e2323670a2722db9aa39/numpy-2.4.6-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2c306dea656c12c68f51f4cea133cbe78ca7435eb28c735eac1d3ebe73be6e8", size = 16638245, upload-time = "2026-05-18T23:35:54.752Z" }, + { url = "https://files.pythonhosted.org/packages/b8/0b/54f9da33128d7e350fab89c7455902eeae70349ee52bddb448dc4a576f45/numpy-2.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:33111801a01c12a8a1e3721f0a9232f8cfc8ae2c6b7098167e6f623c6073f402", size = 17036587, upload-time = "2026-05-18T23:35:58.355Z" }, + { url = "https://files.pythonhosted.org/packages/b6/f0/fdebc1052db1cc37c64beb22072d67cd6d1c71adca1299f53dec2b5e20d3/numpy-2.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ae506e6902902557576a26ff33eda8695e7ecb3cb36c3b573a0765dee114ebdb", size = 18363226, upload-time = "2026-05-18T23:36:02.845Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b4/298628d98c72b57e57f7165ae6a481a1deaf6f3c28262a6e4c739c275930/numpy-2.4.6-cp314-cp314-win32.whl", hash = "sha256:aaf159caa35993cb1f56fb9b8e4610d35758e7ca005412eb1daa856a78c9c4b1", size = 6010196, upload-time = "2026-05-18T23:36:05.92Z" }, + { url = "https://files.pythonhosted.org/packages/df/ac/46de6dda46478f7942f839e094970be2d4a861e005c4b3bf07c92e291a09/numpy-2.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:b507f5c4c1d508876d1819b6bf9a49d365b96320b5d4993426b33a23ca4b8261", size = 12450334, upload-time = "2026-05-18T23:36:09.107Z" }, + { url = "https://files.pythonhosted.org/packages/78/92/b8b798ac784102c0da830d2257d59358e3d3d90d1e2b3f2575dad976c5cf/numpy-2.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:6f41ae150c4e32db4f3310cdaf64b1593a03dbabe29eec77fc9b50fe64061df6", size = 10495678, upload-time = "2026-05-18T23:36:12.766Z" }, + { url = "https://files.pythonhosted.org/packages/30/34/ec28d1aa8115971537c01469ab2011ee96827930f0a124de1000cc2a7ed7/numpy-2.4.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ece3d2cfe132e7d51f44a832b303895e6f2d499c5e74dfbdb06ee246147a304a", size = 14823672, upload-time = "2026-05-18T23:36:16.473Z" }, + { url = "https://files.pythonhosted.org/packages/16/bd/f6d1fede4e54e8042a7ff97bb495510f3c220f94bcd9e8b228e87c92cc0d/numpy-2.4.6-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:e3e5193ef5a3dc73bceee50f7fdc2c90dbb76c42df8d8fae3d1067a583df579e", size = 5328731, upload-time = "2026-05-18T23:36:19.767Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f0/e105b9e2fd728a9910103884decd6951d9dd73896b914a98d9a231de02ee/numpy-2.4.6-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:17f9ade344e7d9b464a084d69bcf18fc691cb1db67c62ed80820bf4926d78f0e", size = 6649805, upload-time = "2026-05-18T23:36:22.266Z" }, + { url = "https://files.pythonhosted.org/packages/82/dd/1206a7ca6ab15e3f02069707ca96222e202af681bb73756da7527f3cb837/numpy-2.4.6-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cd5ffd25db4e7ba6a375693b3fc0fc1791ec636c17db3720da19bde7180ec43", size = 15730496, upload-time = "2026-05-18T23:36:25.713Z" }, + { url = "https://files.pythonhosted.org/packages/51/e7/38d3ea825dcab85a591734decb2f6c67caa7c8367d374df1a1c3842f9b07/numpy-2.4.6-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d92c3819208a60205a12a245c91ad70cb0a85336659b19b834205573ac8456e", size = 16679616, upload-time = "2026-05-18T23:36:29.652Z" }, + { url = "https://files.pythonhosted.org/packages/93/b7/caabfdf53edf663e0b4eb74d7d405d83baef09eb5e83bcd32d601d72b93e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e85b752a1e912b70eaad4fafbd4d1238007ab221de2009b9a2f5ae7461239895", size = 17085145, upload-time = "2026-05-18T23:36:33.449Z" }, + { url = "https://files.pythonhosted.org/packages/f9/45/68d7c33a6bcf3e5aa3bdbd57a367e6f615286dfd6482f97e8ffeb734306e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:29cb7f67d10b479ff07c17d33e39f78c07f71c40ef30d63c153d340e96cd3fb4", size = 18403813, upload-time = "2026-05-18T23:36:37.369Z" }, + { url = "https://files.pythonhosted.org/packages/9c/50/0753655aa844c99cd9e018aacf76f130f1bd81d881bb74bc0aef5d73a8ba/numpy-2.4.6-cp314-cp314t-win32.whl", hash = "sha256:260a5d70215b61ab4fadf5c7baacd64821842975eea312125ed3c39a6391b063", size = 6156982, upload-time = "2026-05-18T23:36:40.817Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d4/7c67becf668f973cb490cec3e98dfd799d866f9c989a54d355672cfa0db6/numpy-2.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:81a1cca95ed5bb92aa8b10dd2cdc9a0d3853a50fad926c28b5d7e8ea54389627", size = 12638908, upload-time = "2026-05-18T23:36:43.996Z" }, + { url = "https://files.pythonhosted.org/packages/43/bb/e1c71a4295b1b1d1393d50dbb4f2a36283c6859d9d3892e84f00ec5a91d5/numpy-2.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:0c9136e14ed34a9e343a31c533d78a9813a69a3148332bce5e9821cb2f996e66", size = 10565867, upload-time = "2026-05-18T23:36:47.114Z" }, + { url = "https://files.pythonhosted.org/packages/de/12/b422cc84439adc0d00de605bf4a308890ae5c26f2c71fbd73e5d08fbb0dd/numpy-2.4.6-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:55cced7c52e981362f708ad635198e97a752dfba412cc03c23bbf3bd8d5cd662", size = 16847511, upload-time = "2026-05-18T23:36:50.673Z" }, + { url = "https://files.pythonhosted.org/packages/44/53/f481bef68011740f8849418d82db07230e825013f31f4eef5ba5b805316a/numpy-2.4.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d6da64deb6b8ed903e7560180a92f2d804ee1ba5eeb849ac2748b8c1aba1f6d7", size = 14889064, upload-time = "2026-05-18T23:36:53.879Z" }, + { url = "https://files.pythonhosted.org/packages/7f/57/42ed575c10ced8af951d426bc4e1f8aff16fd851db33f067036215a7f860/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:68a5124b13fa6cc2086764a20005d30bc0548146f7f5322f02fce212ca14317f", size = 5394157, upload-time = "2026-05-18T23:36:57.194Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ef/f66cc724fcc36c1e364c67f51ae9146090b8b584f27d58b97fdae3edd737/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:948424b06129ce883307e8cff868c31396d8dc7630a59c61d70d98dbe70f222c", size = 6708728, upload-time = "2026-05-18T23:36:59.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/9c/c531f2293b91265d8b48e9b329f54fdd7ffae73cb4134ea10cca4237e9cc/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbbdb29840ca3d91ee0fece42fc29278886d908280bfec0a5846c6f901a3eb0", size = 15798374, upload-time = "2026-05-18T23:37:02.674Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b0/413077f6b1153ed3cba361401c6783bbad6114804a000cc22eb71c13e190/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ad03c0965fb3c692200e74d458ca28c1dbb4ce96f9a479a8aa041ad5fabca02", size = 16747286, upload-time = "2026-05-18T23:37:06.327Z" }, + { url = "https://files.pythonhosted.org/packages/15/ce/e5ec180bc41812edcd8daeb8639d205622c0e8c02259d8ab25a0201b3c2a/numpy-2.4.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:2803abfebfc990042cd494d8ce2d5f82e9d847af6d35ec486923aa19dbad5e73", size = 12504263, upload-time = "2026-05-18T23:37:09.715Z" }, +] + +[[package]] +name = "numpy" +version = "2.5.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13'", + "python_full_version == '3.12.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/9a/80/db0b4559e57ec36362bedbb05530a87fafbcb6067708c946967a41d449e7/numpy-2.5.2.tar.gz", hash = "sha256:d482d171c406ae88c5b19cad3b6a1c4c5209f886ab74bc44c2c865c23f52d860", size = 20773161, upload-time = "2026-08-09T13:48:27.962Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/72/dccb0aaf40972777283303919f613964227266d0c13adebb79ac124f1c3e/numpy-2.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:14e373cfc6387177e8409dac3c7159be8eb05cd77096cd7c950268b86f62831c", size = 16891693, upload-time = "2026-08-09T13:44:51.702Z" }, + { url = "https://files.pythonhosted.org/packages/60/2e/b5aee50a1f74ac815cf8331812cb8251e29024025de462e0c047641c614c/numpy-2.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4bbd96c833ecc8cc069ce518078fc8c60cb9cbfb0fea5b7a803ad65035596d03", size = 11903109, upload-time = "2026-08-09T13:44:55.501Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f4/29e78102a80601cf034d4e9767022cffeca2c3b4c926e1754572ca95593d/numpy-2.5.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:6e8172ddfcf5cf74b811d372b570b83c60bd2de87a6fbfbebdadb4a9bd9c6cbb", size = 5350202, upload-time = "2026-08-09T13:44:58.401Z" }, + { url = "https://files.pythonhosted.org/packages/11/4b/dcd3b7eadaf4035d2c7a4289d232523a6964f602598ef7674e4bd7291f93/numpy-2.5.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:65f188481f1669e26f62b701e8205d19e460fa4a9b52a1414ba382330e4a3414", size = 6687736, upload-time = "2026-08-09T13:45:00.813Z" }, + { url = "https://files.pythonhosted.org/packages/e5/21/4947e0e9d6c9fc2e2ff15b8949049ee44f63adb9cacc729ab8793f97e712/numpy-2.5.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8ee9c4eeb8454b3660a8b53493563c3e121c2fc94fbd72b848ef814ed7b676a9", size = 15612696, upload-time = "2026-08-09T13:45:04.151Z" }, + { url = "https://files.pythonhosted.org/packages/3a/5f/62d28cf019460c7f1394105b4d49d9911a9c444cb77ab0bd95a204c5a6de/numpy-2.5.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3cdec01fa790a186d430433fdd4d4ffb70eed6f0eeb4bf05c8dbe2dce0a9bcb8", size = 16722264, upload-time = "2026-08-09T13:45:07.714Z" }, + { url = "https://files.pythonhosted.org/packages/14/25/3f0be4c1b9fdf5dd5e708a6806978564d7c46a055c000496309ff2a2f8af/numpy-2.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7999d4ddb0c4025018373fd787510d46e04c769467af22869707b3c1cfd459ab", size = 16974396, upload-time = "2026-08-09T13:45:11.316Z" }, + { url = "https://files.pythonhosted.org/packages/22/72/6262cbdeeb45da9d971e40715f579d791603ba8ec0b5e2db1ac55454421d/numpy-2.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c1f017dc0875c9209d219f97feceb7d54c2661bb243deb4114478e1295808af7", size = 18476044, upload-time = "2026-08-09T13:45:14.869Z" }, + { url = "https://files.pythonhosted.org/packages/36/33/29208b8b075bde62d26a81d14b358c42b0f69b6cabd98d4ff97f37f22b05/numpy-2.5.2-cp312-cp312-win32.whl", hash = "sha256:d6a48072864e3324e194a8fbb3c657bcc5b5c869dbc64c9537b1d5c862572c0a", size = 6072817, upload-time = "2026-08-09T13:45:17.867Z" }, + { url = "https://files.pythonhosted.org/packages/7f/b9/87fea2769fe1c47c1b5b01d8310772c9d1a85d485de7cf386ef7a3332b02/numpy-2.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:28ac63476ec7651484215ee7fa15a1f78b57c14621f01e392afe17b9a1390ce4", size = 12464674, upload-time = "2026-08-09T13:45:20.734Z" }, + { url = "https://files.pythonhosted.org/packages/14/52/032b97e00461ab0809bbe4c588b035620e5a14b8cdee47ecddefc7b17d33/numpy-2.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:27650bb0e7140fa3d37b9923b4803645e0b125d190f326eecfd3f4dad8e8ade1", size = 10397131, upload-time = "2026-08-09T13:45:23.73Z" }, + { url = "https://files.pythonhosted.org/packages/f5/d2/6b24738a0ef4557d189b150046cd07823c50e4273e8aebd651222e24306f/numpy-2.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8e4cb9a754c8a0c62eaa88273a5fba3391f4a610d1dee893c0755da31c083f15", size = 16886595, upload-time = "2026-08-09T13:45:27.323Z" }, + { url = "https://files.pythonhosted.org/packages/65/60/f2d208d366f263f39c6e69ed309290717aab41078b6d04c9be2a84fa2a07/numpy-2.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:52c808f96484f5571a5cc863775ce50247c17dfb3b0361f8ed6b4b0456f80080", size = 11896845, upload-time = "2026-08-09T13:45:31.638Z" }, + { url = "https://files.pythonhosted.org/packages/3c/79/81e0bf24f4d020a2b1d5cd297a9f60c3f24eeb116f9bba5870443f7b6a4a/numpy-2.5.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:29d81e97f668489cba8ebfd796b9bdd453525d35dd9e162e2daec94bf3fc7740", size = 5343880, upload-time = "2026-08-09T13:45:34.373Z" }, + { url = "https://files.pythonhosted.org/packages/ba/cc/e3141cf06d1a8a2c7e107543fe1269c1d1af760d4d683c0794a4ee1127c2/numpy-2.5.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:afb3f0632d6b2e3ba04dbce8d1e48d321b369138b73830b5ca371a0e8d479d56", size = 6682264, upload-time = "2026-08-09T13:45:36.7Z" }, + { url = "https://files.pythonhosted.org/packages/29/f1/2a64a307d92c5d98f5255a4014eb43bb6103ee477087b61ecae44a3aa9b9/numpy-2.5.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0aadf13b60048d501e05fa699efaf7734e2494f3498a4c2a5521d822640324f3", size = 15609566, upload-time = "2026-08-09T13:45:39.518Z" }, + { url = "https://files.pythonhosted.org/packages/7b/44/59a1eb68e773c4098d107ef34a0dbdeca501d72ffcfbff9a7707343921ce/numpy-2.5.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29b86ff8a6cc556b47ec6b64b194815cc80e6bf5eedcc6cddfd65318cb0b4eee", size = 16709995, upload-time = "2026-08-09T13:45:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/8a/4c/3e54d4ddbc359a1295f8b633e8106bcd4d7d4a206e82df051bdfb3058755/numpy-2.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6950c4b7dd562453090548ba7f5da7e59f57f85663f15d5dcc60e249192f7e59", size = 16972511, upload-time = "2026-08-09T13:45:47.094Z" }, + { url = "https://files.pythonhosted.org/packages/f2/9f/02e371638ebf19b66d46231e4be52999e87f32d1961b113bc45656608b22/numpy-2.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b9727f472d2f3888053b8a75ab0cb94745a9de224bb5846dbadc0092101bc71d", size = 18465609, upload-time = "2026-08-09T13:45:50.808Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ae/ad6645abc7a3510fe48e8ea1ab4598166f500057ef4ebf38bfad4f1577de/numpy-2.5.2-cp313-cp313-win32.whl", hash = "sha256:4f9744f9fbdcea0bc552e8f19e1f141f811a3f9bc2be2cc6e86d982cab23e3f4", size = 6070204, upload-time = "2026-08-09T13:45:54.111Z" }, + { url = "https://files.pythonhosted.org/packages/15/20/f3489f86d81ea460b2bcdceaed094142ca6579f6be0ec527b781d39afe68/numpy-2.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:85aaccb24182c25df891ad0ec333585967e115269d5f1b17f2c9ae005bc96657", size = 12460532, upload-time = "2026-08-09T13:45:57.167Z" }, + { url = "https://files.pythonhosted.org/packages/d5/21/35b31dde1b283b79de828b80f876afd8c94e28fe1e9c375f89e261cc4c0d/numpy-2.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:bd68ece1553d2023c09a4226d9e41c586ad2d20594d1a456186c33513d2cb3f2", size = 10396725, upload-time = "2026-08-09T13:46:00.478Z" }, + { url = "https://files.pythonhosted.org/packages/ac/f8/c3b222bf075b50afd8e949a07a15c4b312a4a84bd8102a332bcd953cbbb4/numpy-2.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d787cf769c3baeb5f6235e778edb52c08dfa923789b5958f28e6450f96107cb1", size = 16885180, upload-time = "2026-08-09T13:46:03.939Z" }, + { url = "https://files.pythonhosted.org/packages/17/e1/2c1d4b1987795a92b5bbf7c24fe249ab96aa2573ab0d7604802c189d7b86/numpy-2.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:24b9dc2e3d84aa58523798805194e23e736f3f6ce2d1a5b92583ae734e6dbda8", size = 11907878, upload-time = "2026-08-09T13:46:07.045Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ee/d08226fc858044355983a6e5b94f08ff6f3969e0a2b160a4a89f0ddb3445/numpy-2.5.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:9e9413326d726c2545bfa65d2c0876871e8d8386e77f992c1d426e180bbd4323", size = 5354922, upload-time = "2026-08-09T13:46:10.04Z" }, + { url = "https://files.pythonhosted.org/packages/94/f0/6d3d933056440ebbc5e6bad92065fc6c26a48a84a36b1208580e94eea76c/numpy-2.5.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:60e902ac295855348a5ca2ea4c89108989a9f5fddfad3dfc0a8f36b10358567e", size = 6679168, upload-time = "2026-08-09T13:46:12.275Z" }, + { url = "https://files.pythonhosted.org/packages/c4/3b/ecd49dd90033cceb2704d88ca905d4d7d89b0e8c739608754ffd325fa820/numpy-2.5.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50e500dc868e9313530ce12ba470fe50ff3afe3d62993ed6eff652dacd555b65", size = 15624501, upload-time = "2026-08-09T13:46:15.322Z" }, + { url = "https://files.pythonhosted.org/packages/c7/99/461bd36dbdfac6c1c53efa370bd55a83227542d0d118f1677dbf1a3dacd5/numpy-2.5.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318b9a4c845dbea06708a29c84ee429cc3065048db34cdb799047643492050ee", size = 16713701, upload-time = "2026-08-09T13:46:18.949Z" }, + { url = "https://files.pythonhosted.org/packages/f9/9c/2b251df9e8a5d647b62b0cbc1b90a91850c1cf4859ecb532fd0b4eacff6c/numpy-2.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:34c319e2963be042673fb46570501b2f06c41924e17e3563d58646b4380dfb68", size = 16986065, upload-time = "2026-08-09T13:46:23.006Z" }, + { url = "https://files.pythonhosted.org/packages/8f/25/20de43f53ff1390534a124475055a19f01fe10c920a0fd11b8e18d6d6052/numpy-2.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f06571a052127dc1b4e8b83029b4d1b20daa2b64a31cdd181fc6bc774e9000eb", size = 18470031, upload-time = "2026-08-09T13:46:27.102Z" }, + { url = "https://files.pythonhosted.org/packages/56/5e/0c577ca308d6da5eb79b546ba10bbe5b60148192194e2da060913b1de4f1/numpy-2.5.2-cp314-cp314-win32.whl", hash = "sha256:2cc779226e476d1e1f08c74068c419e60f41a9e0e069c92f6671d31d5c985e98", size = 6121028, upload-time = "2026-08-09T13:46:30.046Z" }, + { url = "https://files.pythonhosted.org/packages/15/5c/7bcbd5b11f94199073320410cddcbb80cee62415bfeb540874b265c2d922/numpy-2.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:7587f53dfbd5edc0f7b87c6217b4c6d2d1f2ef9c3da70bc1315e7db5f8d7ec9d", size = 12597627, upload-time = "2026-08-09T13:46:32.886Z" }, + { url = "https://files.pythonhosted.org/packages/87/bc/4d0b06fba0da90ccc75af62823cb9dcedb6c9ea0cffa058cb2c9ee773a77/numpy-2.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:3e4c367352d3747784248a227fbec218e193b56f7e6692e3b64fc805478ecfdf", size = 10680414, upload-time = "2026-08-09T13:46:36.036Z" }, + { url = "https://files.pythonhosted.org/packages/cd/17/f429aac9dc08833a0d0f188eba38c532a751b1a1f2ca6018a37b455cb321/numpy-2.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b879fb674276e331513fb136b78dbc6bd3c848309e0d841cfd63be3896c4cfc1", size = 12026967, upload-time = "2026-08-09T13:46:39.084Z" }, + { url = "https://files.pythonhosted.org/packages/ca/9f/d0849de96a2a4ceaa16662f18ee13eaa9c0aa418269fdc8c4857c56b11da/numpy-2.5.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:fd0d703772bba096843785bd38371e31bb4a0c1151497ad5739d182114a73f7f", size = 5473874, upload-time = "2026-08-09T13:46:42.075Z" }, + { url = "https://files.pythonhosted.org/packages/89/3c/8df216d4a4a5422a3de045301cf7df8ea47286d76f5cb7160b0128ac26b7/numpy-2.5.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:3a2f061cebd9e3d23bdcfaaded5e2293a4c6a5b60fa42df85d410a725ce621bf", size = 6789276, upload-time = "2026-08-09T13:46:44.387Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3a/20d7e9891c4ddfadd6ff8d95bf4b29f353d8e1770553de2099880551dfb9/numpy-2.5.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6df895598c0edcb41030126c89e0f353b07d93238116143b7405e937359736c4", size = 15659154, upload-time = "2026-08-09T13:46:47.538Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d6/f3aa3d2688bf501b858835c6bd087ae9b51a56ae6fca8e2b0990abd177af/numpy-2.5.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1ab3d4a901f844ea836c3e80bf463c6a27d7f3c14e8e292fcf28d348b25b9bce", size = 16748909, upload-time = "2026-08-09T13:46:51.442Z" }, + { url = "https://files.pythonhosted.org/packages/7d/8f/1c5cae8d2baf86ab802ae97a00be55bc7e21ebc11b12bbc33376c5f05342/numpy-2.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:cebc2d6dbb605a7703d59751dea4bd6b0ab127a5a4338a6f432df1936fef8b26", size = 17027685, upload-time = "2026-08-09T13:46:55.095Z" }, + { url = "https://files.pythonhosted.org/packages/5c/27/71d3467404aedc1c24ce79610f91b52b0b0f466c43a701aa56fc75c145ab/numpy-2.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eaca7ff36f0f52e2111ec71f169d8fd3e889e7ddc0d2592e0d703fd8d3ce8fac", size = 18501181, upload-time = "2026-08-09T13:46:59.09Z" }, + { url = "https://files.pythonhosted.org/packages/14/2f/42921d27c40aea7e077f4a423ae509fd9220b028cd787bafefd8ab2b3a5f/numpy-2.5.2-cp314-cp314t-win32.whl", hash = "sha256:ddf47472af2e4280d79bac82304f5e80150211f1b9e614b760061d5fdfbb6eba", size = 6271085, upload-time = "2026-08-09T13:47:01.903Z" }, + { url = "https://files.pythonhosted.org/packages/75/e6/bad5f5d56de9b1971bac959963dda276d35c40f1854475005434bbe08692/numpy-2.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:44ef9675d908e65f9953063837c3277730f3f4437615a4cdab67b366cabaf884", size = 12787971, upload-time = "2026-08-09T13:47:04.963Z" }, + { url = "https://files.pythonhosted.org/packages/df/05/f608795cb34391acd67e38d94a3c36abd8d8576293a3a80727d7595c372c/numpy-2.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:eaa088384c46f519dacb93b7ec483a6d6b19a4a2085ae4f25ab9b1c43d387d1e", size = 10750306, upload-time = "2026-08-09T13:47:07.976Z" }, + { url = "https://files.pythonhosted.org/packages/33/c6/28de0191c5f82b7d42a0a51390ba98587048aa93a39fafb05bdbe6e8d00c/numpy-2.5.2-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:078f9b027b478c9379b9677babbf0f8b8f1ecfada27636d7b9a93990c638739f", size = 16885274, upload-time = "2026-08-09T13:47:11.439Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d1/973ca116000d244897e468ea1aff30b589e5022e3c8744b71706fe33bd57/numpy-2.5.2-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:50a68f4bacd8a2b33d8da3d2269d0d78500f86ea582e4786dc10f5ef2c2c6842", size = 11907846, upload-time = "2026-08-09T13:47:15.128Z" }, + { url = "https://files.pythonhosted.org/packages/78/d9/8c4b3937ef204cb2fd88d389ccd0f265a2ffb11f35a01d2064cf46714bd6/numpy-2.5.2-cp315-cp315-macosx_14_0_arm64.whl", hash = "sha256:e79aba74ffaf5f78a050d777c184cddf8fdffabab38acf5f3ef1fecbc17895d6", size = 5354892, upload-time = "2026-08-09T13:47:18.07Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/b6ee65ea2999fdb7023935e108e6fb776ee4082aa15f159acfa857e578c8/numpy-2.5.2-cp315-cp315-macosx_14_0_x86_64.whl", hash = "sha256:9a0731745a72a184490a582fb4af2533512bd071ace67785b5fdffc0ae58dce8", size = 6679309, upload-time = "2026-08-09T13:47:20.456Z" }, + { url = "https://files.pythonhosted.org/packages/43/f3/acb18d8b137a393c8e7803a8c994c9e64bde3930692a69d826993113a159/numpy-2.5.2-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4ec954036759bcee3aa484f8603bd9c14f3e776293b85578b8734c2d72777c69", size = 15625850, upload-time = "2026-08-09T13:47:24.365Z" }, + { url = "https://files.pythonhosted.org/packages/a9/bf/a8e9bb0db815a0e265b5744ebedd3af0bd5faad8604e5b50a1cd012f3c91/numpy-2.5.2-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc649493697006bc90614a5f0bbc8cb3cb1866715c474e473694968d7e6b99ab", size = 16713664, upload-time = "2026-08-09T13:47:27.965Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c3/6e913736b3dd6582344af32418b5fb9dab34282e8a8174ae1d54ceb0fc13/numpy-2.5.2-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:cf7de32f486e4ac9e2d93b810f9e9ac72a728dd46a32a0bb403222f27f653514", size = 16986749, upload-time = "2026-08-09T13:47:31.541Z" }, + { url = "https://files.pythonhosted.org/packages/80/09/7d3b23eff5c7428ef6c01e6f7052bb60d504c4d33e317b36b8959c24ad97/numpy-2.5.2-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:2ffa7bacab3e2ee1b19ed31766bb60bb380b68c23f051e199c5cc598afd68710", size = 18470495, upload-time = "2026-08-09T13:47:35.364Z" }, + { url = "https://files.pythonhosted.org/packages/a5/a4/68a321d825374f6eb677ffe8ef8c6b9a328304e6fd2e39d9530822776607/numpy-2.5.2-cp315-cp315-win32.whl", hash = "sha256:6b588cc8f902d6bff201c19fd00c43ab8545671e3554d014e12e14139e5e8617", size = 6120696, upload-time = "2026-08-09T13:47:38.561Z" }, + { url = "https://files.pythonhosted.org/packages/c8/23/deafbb1700f79fae9cd1e91220f133d124cc267de1b584da3fbf6db2f6cd/numpy-2.5.2-cp315-cp315-win_amd64.whl", hash = "sha256:07d4e89f3a9ab0a9ba24264ccdb642b3dd951b2281e8883a5481a4aa79cc31a7", size = 12597324, upload-time = "2026-08-09T13:47:41.401Z" }, + { url = "https://files.pythonhosted.org/packages/33/cd/3272ba105e3bbbdaeb11357eda31e7a6825ffe159e8171665660299a948f/numpy-2.5.2-cp315-cp315-win_arm64.whl", hash = "sha256:a610dc7e3c52edd39c2bc2375ff9c3fd59cb3ad00e4472d36f83bc1457145788", size = 10680466, upload-time = "2026-08-09T13:47:44.873Z" }, + { url = "https://files.pythonhosted.org/packages/0e/0e/58370637b1bb70a5c9ce2b43f4b521ccb224e36ccb76a6596b17ae4b447c/numpy-2.5.2-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:40f4d451aed46a8046a1aae41c4e55fb3612273df9c502480135e1501576a34b", size = 16993947, upload-time = "2026-08-09T13:47:48.97Z" }, + { url = "https://files.pythonhosted.org/packages/10/93/2abcb807712b289d6d60fe4cf30532f98974a8396d885650f3ba5a13026e/numpy-2.5.2-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:c081cbe16ba1ab53078e5ff29013621e33c509eedab055775d956427712c236e", size = 12025331, upload-time = "2026-08-09T13:47:52.646Z" }, + { url = "https://files.pythonhosted.org/packages/8b/3a/2898e003a5fbaf87e76c039b4ee1f5eb390471b4ffe74887c1f34c4e791e/numpy-2.5.2-cp315-cp315t-macosx_14_0_arm64.whl", hash = "sha256:0090ccdd57ec2703e9b49d0bf554767370581c1dd0a6b2bb2b2d9def317d042a", size = 5472336, upload-time = "2026-08-09T13:47:55.403Z" }, + { url = "https://files.pythonhosted.org/packages/61/a5/23f69d07c544597b29758b31b55c27dc9d541012a2c1496189fef702aec2/numpy-2.5.2-cp315-cp315t-macosx_14_0_x86_64.whl", hash = "sha256:6a9bb119fb8dd21ba30b3f0e555b7e2b081bd9883af21ec9c1c633d161cda3a8", size = 6788387, upload-time = "2026-08-09T13:47:58.192Z" }, + { url = "https://files.pythonhosted.org/packages/15/ea/c0dbdbcf22f43782510a3e492dd3da73c6112b69cac8929d16d127536fc4/numpy-2.5.2-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a839318485284a6fb31be4f8f2c91c8f2cb22f4543c4a8903f12b0671ffe07cc", size = 15667096, upload-time = "2026-08-09T13:48:01.562Z" }, + { url = "https://files.pythonhosted.org/packages/fc/5e/29c73c31748cdb0f7566642125ba17fd5b56780cddf891b085dab27e4466/numpy-2.5.2-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba0a474801b8dc67b66bf465548abc90e82b44d2611b5770f33008dcabffe8ec", size = 16751730, upload-time = "2026-08-09T13:48:05.706Z" }, + { url = "https://files.pythonhosted.org/packages/47/95/02501e8454796bb58dadf7a99d3181e0b464bf264e1003039572f9779fac/numpy-2.5.2-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:0a4035ae1129ff8777f08bfbd44f1e5d8e9c049ce0c2dd78fc0d92c13e7251c0", size = 17038686, upload-time = "2026-08-09T13:48:09.627Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b5/53a681d91b5c82687067d8ea5035e02d917b5509d6f334cb06484a954714/numpy-2.5.2-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:77843ca236b777e67f8d6b3660ea116e499612703a0ecd7093f316201eb9d8e2", size = 18507727, upload-time = "2026-08-09T13:48:13.744Z" }, + { url = "https://files.pythonhosted.org/packages/42/06/6e11443f7b64ee376c860506091103bf68f92d2cab9e8d96d4501babf07c/numpy-2.5.2-cp315-cp315t-win32.whl", hash = "sha256:7354826bc6f8f69402e9b7fe28d15fcd34feebd74f856f111585c5b0c9fb0251", size = 6269775, upload-time = "2026-08-09T13:48:17.543Z" }, + { url = "https://files.pythonhosted.org/packages/f1/18/195d6b86cd72dbbc501edfa778005fa6b87afd34c153e46028cd3a0938f4/numpy-2.5.2-cp315-cp315t-win_amd64.whl", hash = "sha256:e5651f3f87add730ee6608d915009e19c911fba0cb000c7e3ea994b7d768eb12", size = 12782559, upload-time = "2026-08-09T13:48:21.023Z" }, + { url = "https://files.pythonhosted.org/packages/b4/07/458c344f0f0c178f4481dad5cca790626ffe4c34eabf9467069d06ee4999/numpy-2.5.2-cp315-cp315t-win_arm64.whl", hash = "sha256:5f8e00be2ec6f45f4e8a41a527f68d44a7d96fee92a650e4d8b1326f77f61e6e", size = 10748103, upload-time = "2026-08-09T13:48:24.21Z" }, +] + +[[package]] +name = "packaging" +version = "26.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, +] + +[[package]] +name = "pillow" +version = "12.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/3d/bb7fca845737cf9d7dbde16ed1843984665ff2e0a518f5db43e77ec540b9/pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce", size = 47025035, upload-time = "2026-07-01T11:56:38.965Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/c2/669d88644cddb1485bd9534e63e8cf476c8e51cb3c3a1297677023505c0e/pillow-12.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:6c0016e7b354317c4e9e525b937ac8596c38d2d232b419529b9cd7a1cd46e39a", size = 5392418, upload-time = "2026-07-01T11:53:27.808Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ba/3762f376a2948e3036488d773a146e0ae6ecc2ca03ac20e2615bd0b2ba02/pillow-12.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bcc33feacfaefce60c12fd500a277533bdc02b10a19f7f6d348763d8140bbba7", size = 4785287, upload-time = "2026-07-01T11:53:29.761Z" }, + { url = "https://files.pythonhosted.org/packages/07/50/b5d688cc9c52d4482f3d5bcab6ce20bc2a74a85d2343841c907444a3be2c/pillow-12.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5594fc43d548a7ed94949d139aa1341b270f1863f11cfd37f5a6c8b778a6b67f", size = 6253754, upload-time = "2026-07-01T11:53:32.298Z" }, + { url = "https://files.pythonhosted.org/packages/4e/89/36f4cd76cf4baf05c50ababb976249153f18c959171c7f6ba09a6f217260/pillow-12.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0606c8bf2cdefea14a43530f7657cbbb7ecf1c4222512492ef4a4434a9501ec", size = 6925605, upload-time = "2026-07-01T11:53:34.487Z" }, + { url = "https://files.pythonhosted.org/packages/eb/c0/4de58cf6633b9e3a6061ef4be6fb91fc3c90b812ece886f531e3c523d777/pillow-12.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:85f998ea1848bc6757289e739cfbdda3a04adfd58b02fc018ce54d754a5ce468", size = 6327788, upload-time = "2026-07-01T11:53:36.433Z" }, + { url = "https://files.pythonhosted.org/packages/87/3c/14d53682a19550dbbaf3b598f807d5457646c510805a44c7d7891cd1cd1a/pillow-12.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:25b9b82bb22e6e2b3cd07b39c68b7b862001226cb3dff7130d1cb914121b39ed", size = 7036288, upload-time = "2026-07-01T11:53:38.712Z" }, + { url = "https://files.pythonhosted.org/packages/38/1d/36279e3c77efe034e4cc2b0393ee74ffdb5a62391dacbf9b916154f5f0b8/pillow-12.3.0-cp310-cp310-win32.whl", hash = "sha256:37dc8f7bbb66efe481bb60defacef820c950c24713fb44962ed6aa2a50966de1", size = 6472396, upload-time = "2026-07-01T11:53:40.781Z" }, + { url = "https://files.pythonhosted.org/packages/48/7c/8fa0039574c476d7c6fa57dd7c32a130436877c6ec1e5ce1cc8ec44878c1/pillow-12.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:300557495eb45ebb8aec96c2da9c4be642fbf7cd937278b4013ba894ea8eb0eb", size = 7226887, upload-time = "2026-07-01T11:53:42.764Z" }, + { url = "https://files.pythonhosted.org/packages/fa/17/e324be141d173c1c919428066c3259f21c1b8982e564e01a4a81e96dbdcf/pillow-12.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:514435a37670e3e5e08f3945b68718b6ed329bb84367777e16f9f4dfe1e61a0f", size = 2568039, upload-time = "2026-07-01T11:53:45.372Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c8/0a78b0e02d7ac54bc03e5321c9220da52f0c2ea83b21f7c40e7f3169c502/pillow-12.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:00808c5e14ef63ac5161091d242999076604ff74b883423a11e5d7bbb38bf756", size = 5392415, upload-time = "2026-07-01T11:53:47.162Z" }, + { url = "https://files.pythonhosted.org/packages/b2/5b/a02d30018abd97ced9f5a6c63d28597694a00d066516b9c1c6de45859fc9/pillow-12.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:37d6d0a00072fd2948eb22bce7e1475f34569d90c87c59f7a2ec59541b77f7a6", size = 4785266, upload-time = "2026-07-01T11:53:49.079Z" }, + { url = "https://files.pythonhosted.org/packages/c8/98/766667a4be768150a202836acd9fad19c06824ca86c4286d3cf6b274964e/pillow-12.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bcb46e2f9feff8d06323983bd83ed00c201fdcab3d74973e7072a889b3979fcd", size = 6263814, upload-time = "2026-07-01T11:53:51.32Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2d/ede717bc1144f63886c21fd349bb95860b0d1a21149ff16f2bb362b612b6/pillow-12.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23d27a3e0307ec2244cc51e7287b919aa68d097504ebe19df4e76a98a3eea5bd", size = 6934408, upload-time = "2026-07-01T11:53:53.487Z" }, + { url = "https://files.pythonhosted.org/packages/a3/48/9c58b685e69d49c31af6c8eb9012055fab7e665785165c84796e2c73ce72/pillow-12.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4f883547d4b7f0495ebe7056b0cc2aea76094e7a4abc8e933540f3271df27d9c", size = 6337160, upload-time = "2026-07-01T11:53:55.457Z" }, + { url = "https://files.pythonhosted.org/packages/ff/fa/dc2a5c0ba6df93f67c31d34b808b7ce440b40cdbf96f0b81cde1d1e6fa93/pillow-12.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:236ff70b9312fb68943c703aa842ca6a758abfa45ac187a5e7c1452e96ef72b5", size = 7045172, upload-time = "2026-07-01T11:53:57.736Z" }, + { url = "https://files.pythonhosted.org/packages/86/a5/444817a4d4c4c2417df00513086ca196f388d8f9ef40c2e4ccd1ad1af54b/pillow-12.3.0-cp311-cp311-win32.whl", hash = "sha256:10e41f0fbf1eec8cfd234b8fe17a4caac7c9d0db4c204d3c173a8f9f6ef3232b", size = 6472232, upload-time = "2026-07-01T11:53:59.767Z" }, + { url = "https://files.pythonhosted.org/packages/63/c6/4bad1b18d132a50b27e1365e1ab163616f7a5bb56d330f66f9d1d9d4f9d4/pillow-12.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:8e95e1385e4998ae9694eeaa4730ba5457ff61185b3a55e2e7bea0880aef452a", size = 7233653, upload-time = "2026-07-01T11:54:02.066Z" }, + { url = "https://files.pythonhosted.org/packages/fd/16/00f91ab7760dc842f5aad55217e80fc4a7067a0604535249bc8a2d6d9870/pillow-12.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:ebaea975e03d3141d9d3a507df75c9b3ec90fa9d2ffd07567b3a978d9d790b26", size = 2568195, upload-time = "2026-07-01T11:54:04.622Z" }, + { url = "https://files.pythonhosted.org/packages/37/bf/fb3ebff8ddcb76aac5a01389251bbbb9519922a9b520d8247c1ca864a25d/pillow-12.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ba09209fbe443b4acccebe845d8a138b89a8f4fbaeedd44953490b5315d5e965", size = 5345969, upload-time = "2026-07-01T11:54:06.397Z" }, + { url = "https://files.pythonhosted.org/packages/d8/66/9a386a92561f402389a4fc70c18838bf6d35eb5eb5c6850b4b2dc64f5048/pillow-12.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffd0c5368496f41b0944be820fcb7a838aa6e623d250b01acf2643939c3f99d7", size = 4780323, upload-time = "2026-07-01T11:54:09.351Z" }, + { url = "https://files.pythonhosted.org/packages/25/27/ac8f99618ffd3dde21db0f4d4b1d2ab00c0880595bfd17df103f7f39fd0c/pillow-12.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9c7f76c0673154f044e9d78c8655fb4213f6ca31a836df48b40fe5d187717b9", size = 6266838, upload-time = "2026-07-01T11:54:11.71Z" }, + { url = "https://files.pythonhosted.org/packages/84/21/a35af28dcc61f37ed850a2d64c65c701321dfbf25085e469d5559360cbbf/pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91", size = 6940830, upload-time = "2026-07-01T11:54:13.732Z" }, + { url = "https://files.pythonhosted.org/packages/eb/51/8b08617af3ad95e33ce6d7dd2c99ed6c8298f7fb131636303956be022e25/pillow-12.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e491916b378fba47242221bb9ead245211b70d504f495d105d17b14a24b4907c", size = 6344383, upload-time = "2026-07-01T11:54:15.756Z" }, + { url = "https://files.pythonhosted.org/packages/1d/72/cf78ac9780bb93c28328f408973845a309d4d145041665f734572ced1b52/pillow-12.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0dd2064cbc55aaec028ef5fbb60fa47bb6c3e7918e07ff17935284b227a9d2df", size = 7052934, upload-time = "2026-07-01T11:54:17.721Z" }, + { url = "https://files.pythonhosted.org/packages/20/20/25e0f4dc178a6bc0696793720055519a0de89e7661dae886992decbd2f81/pillow-12.3.0-cp312-cp312-win32.whl", hash = "sha256:dbce0b29841537a2fa4a214c2bbf14de3587c9680caa9b4e217568472490b28f", size = 6472684, upload-time = "2026-07-01T11:54:19.839Z" }, + { url = "https://files.pythonhosted.org/packages/45/89/da2f7971a317f83d807fdd4065c0af40208e59e692cc43d315a71a0e96d1/pillow-12.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a2b55dd6b2a4c4b7d87ffa56bdb33fdc5fdb9a462173861a7bc097f17d91cb09", size = 7227137, upload-time = "2026-07-01T11:54:22.025Z" }, + { url = "https://files.pythonhosted.org/packages/de/47/4845a0a6c0dbf1db8456bd9fc791f13c5ced7ced20606d08a0aacfd25b49/pillow-12.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:331b624368d4f1d069149002f25f44bc61c8919ce8ddb3c45bdad8f6e2d89510", size = 2568267, upload-time = "2026-07-01T11:54:24.051Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ac/31fb64e1e7efb5a4b50cd3d92049ba89ac6e4d8d3bb6a74e15048ca3353e/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:21900ce7ba264168cd50defae43cd75d25c833ad4ad6e73ffc5596d12e25ac89", size = 4161684, upload-time = "2026-07-01T11:54:25.934Z" }, + { url = "https://files.pythonhosted.org/packages/87/b4/9805e23d2b4d77842b468513841fda254ee42f0289d25088340e4ff46e2d/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4e8c2a84d977f50b9daed6eeaf3baef67d00d5d74d932288f02cb94518ee3ace", size = 4255487, upload-time = "2026-07-01T11:54:27.935Z" }, + { url = "https://files.pythonhosted.org/packages/df/39/ecf519435a200c693fe053a6ee4d835b41cf963a4dfc2551c4e637cb2a71/pillow-12.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:ae26d61dfa7a47befdc7572b521024e8745f3d809bd95ca9505a7bba9ef849ec", size = 3696433, upload-time = "2026-07-01T11:54:29.813Z" }, + { url = "https://files.pythonhosted.org/packages/42/92/2fc3ffad878ae8dd5469ec1bc8eb83b71f48e13efdf68f02709003982a32/pillow-12.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7a743ff716f746fc19a9557f60dab1600d4613255f8a7aeb3cdde4db7eb15a66", size = 5345889, upload-time = "2026-07-01T11:54:31.97Z" }, + { url = "https://files.pythonhosted.org/packages/10/76/8803c13605b763d33d156c4678fc77f8443389c0c51c8aef707bb02015f4/pillow-12.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d69141514cc30b774ceea5e3ed3a6635c8d8a96edf664689b890f4089111fb35", size = 4780109, upload-time = "2026-07-01T11:54:34.026Z" }, + { url = "https://files.pythonhosted.org/packages/1f/01/e18aff37cb0b4aac47ac90f016d347a49aca667ef97f190b06ac2aabc928/pillow-12.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7401aebd7f581d7f83a439d87d474999317ee099218e5ad25d125290990ba65", size = 6263736, upload-time = "2026-07-01T11:54:36.131Z" }, + { url = "https://files.pythonhosted.org/packages/f7/62/de5bdd77d935331f4f802edc11e4d82950f642caad6cb2f949837b8560e2/pillow-12.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0847a763afefb695bc912d7c131e7e0632d4edc1d8698f58ddabec8e46b8b6d3", size = 6937129, upload-time = "2026-07-01T11:54:38.216Z" }, + { url = "https://files.pythonhosted.org/packages/70/4d/105627a13300c5e0df1d174230b32fd1273062c96f7745fd552b945d1e1d/pillow-12.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:571b9fcb07b97ef3a492028fb3d2dc0993ca23a06138b0315286566d29ef718a", size = 6339562, upload-time = "2026-07-01T11:54:40.354Z" }, + { url = "https://files.pythonhosted.org/packages/6b/1d/f13de01a553988ab895ba1c722e06cf3144d4f57656fd5b81b6d881f1179/pillow-12.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:756c768d0c9c2955feb7a56c37ea24aea2e369f8d36a88da270b6a9f19e62b5e", size = 7049439, upload-time = "2026-07-01T11:54:42.489Z" }, + { url = "https://files.pythonhosted.org/packages/c9/f9/066794cca041b969964f779ee5fa66a9498bbf34248ac39c5d7954e4198f/pillow-12.3.0-cp313-cp313-win32.whl", hash = "sha256:a876864214e136f0eb367788dbd7df045f4806801518e2cfe9e13229cfe06d8f", size = 6473287, upload-time = "2026-07-01T11:54:44.9Z" }, + { url = "https://files.pythonhosted.org/packages/a6/9b/7a58e61d62be561da3a356fe2384d4059a6345fc130e23ef1c36a5b81d24/pillow-12.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:1cca606cd25738df4ed873d5ad46bbdb3d83b5cbca291f6b4ff13a4df6b0bbe8", size = 7239691, upload-time = "2026-07-01T11:54:47.141Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b0/c4ed4f0ef8f8fa5ee8351537db6650bb8189f7e118842978dd6589065692/pillow-12.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:b629de27fda84b42cde7edef0d85f13b958b47f6e9bbcbba9b673c562a89bd8b", size = 2568185, upload-time = "2026-07-01T11:54:49.137Z" }, + { url = "https://files.pythonhosted.org/packages/dc/01/001f65b68192f0228cc1dbbc8d2530ab5d58b61037ba0587f946fea607cd/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9cf95fe4d0f84c82d282745d9bb08ad9f926efa00be4697e767b814ce40d4330", size = 4161736, upload-time = "2026-07-01T11:54:51.156Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d2/0219746d0fd16fc8a84498e79452375be3797d3ce4044596ce565164b84f/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:8728f216dcdb6e6d555cf971cb34076139ad74b31fc2c14da4fafc741c5f6217", size = 4255435, upload-time = "2026-07-01T11:54:53.414Z" }, + { url = "https://files.pythonhosted.org/packages/c8/02/8d0bc62ef0302318c46ff2a512822d2610e81c7aa46c9b3abe6cbaca5ad0/pillow-12.3.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a45650e8ce7fafffd731db8550230db6b0d306d181a90b67d3e6bca2f1990930", size = 3696262, upload-time = "2026-07-01T11:54:55.739Z" }, + { url = "https://files.pythonhosted.org/packages/85/e2/73c77d218410b14f5f2d565e8a998d5317b7b9c75368d29985139f7a46f0/pillow-12.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ba54cfebe86920a559a7c4d6b9050791c20513650a1952ebe3368c7dc70306f8", size = 5350344, upload-time = "2026-07-01T11:54:57.657Z" }, + { url = "https://files.pythonhosted.org/packages/c7/da/32c752228ae345f489e3a42499d817b6c3996da7e8a3bc7a04fc806b243b/pillow-12.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e158cb00350dc278f3b91551101aa7d12415a66ebf2c91d8d5ac14e56ddd3ad0", size = 4780131, upload-time = "2026-07-01T11:54:59.713Z" }, + { url = "https://files.pythonhosted.org/packages/b1/9d/8b2c807dbef61a5197c047afe99823787eb66f63daf9fb2432f91d6f0462/pillow-12.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9aeb04d6aef139de265b29683e119b638208f88cf73cdd1658aa07221165321", size = 6263757, upload-time = "2026-07-01T11:55:01.778Z" }, + { url = "https://files.pythonhosted.org/packages/5c/44/c85361f65dbe00eea8576ee467c768d25129989efb76e94f205e9ca9bb46/pillow-12.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:251bf95b67017e27b13d82f5b326234ca62d70f9cf4c2b9032de2358a3b12c7b", size = 6936962, upload-time = "2026-07-01T11:55:03.93Z" }, + { url = "https://files.pythonhosted.org/packages/18/7e/e483414b35800b86b6f08dbbc7803fb5cd52c4d6f897f47d53ea2c7e6f65/pillow-12.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fe3cca2e4e8a592be0f269a1ca4835c25199d9f3ce815c8491048f785b0a0198", size = 6339171, upload-time = "2026-07-01T11:55:05.989Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f4/68c491844841ede6bed70189546b3ee9731cf9f2cbad396faff5e1ccba45/pillow-12.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:23aceaa007d6172b02c277f0cd359c79492bbb14f7072b4ede9fbcaf20648130", size = 7048116, upload-time = "2026-07-01T11:55:08.131Z" }, + { url = "https://files.pythonhosted.org/packages/a3/34/77f3f793fed8efc7d243f21b33c5a3f0d1c97ee70346d3db855587e155ff/pillow-12.3.0-cp314-cp314-win32.whl", hash = "sha256:af8d94b0db561cf68b88a267c5c44b49e134f525d0dc2cb7ed413a66bc23559a", size = 6467209, upload-time = "2026-07-01T11:55:10.408Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e0/492879f69d94f91f60fc8cd05ba03650e9520afebb2fb7aa12777d7c7f38/pillow-12.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:fdafc9cce40277e0f7a0feabce0ee50dd2fa1800f3b38015e51296b5e814048d", size = 7237707, upload-time = "2026-07-01T11:55:12.745Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ac/6b11f2875f1c2ac040d84e1bbf9cf22a88038f901ca1037898b280b38365/pillow-12.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:e91206ee562682b51b98ef4b26a6ef48fd84e15fd4c4bc5ec768eb641d206838", size = 2565995, upload-time = "2026-07-01T11:55:14.736Z" }, + { url = "https://files.pythonhosted.org/packages/52/69/c2208e56af9bfc1913afb24020297a691eb1d4ef688474c8a04913f65e04/pillow-12.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:164b31cd1a0490ab6efae01aa5df49da7061be0af1b30e035b6e9a1bfe34ee6e", size = 5352503, upload-time = "2026-07-01T11:55:17.076Z" }, + { url = "https://files.pythonhosted.org/packages/07/70/e5686d753e898a45d778ff1718dba8516ead6ab6b95d85fc8c4b70650cf2/pillow-12.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5afb51d599ea772b8365ae807ae557f18bccfe46ab261fd1c2a9ed700fc6eb17", size = 4782956, upload-time = "2026-07-01T11:55:19.448Z" }, + { url = "https://files.pythonhosted.org/packages/d5/37/25c6692f06927ee973ff18c8d9ee98ad0b4d84ee67a09610c2dd1447958e/pillow-12.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3edce1d53195db527e0191f84b71d02022de0540bf43a16ed734ed7537b07385", size = 6322855, upload-time = "2026-07-01T11:55:21.613Z" }, + { url = "https://files.pythonhosted.org/packages/cc/91/420637fcb8f1bc11029e403b4538e6694744428d8246118e45719f944556/pillow-12.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf16ba1b4d0b6b7c8e534936632270cf70eb00dbe09005bc345b2677b726855c", size = 6989642, upload-time = "2026-07-01T11:55:24.006Z" }, + { url = "https://files.pythonhosted.org/packages/10/08/b94d7811281ccf0d143a1cf768d1c49e1e54af63e7b708ab2ee3eb87face/pillow-12.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:24870b09b224f7ae3c39ed07d10e819d06f8720bc551847b1d623832b5b0e28d", size = 6391281, upload-time = "2026-07-01T11:55:26.252Z" }, + { url = "https://files.pythonhosted.org/packages/d2/87/24233f785f55474dc02ce3e739c5528a77e3a862e9333d1dd7a25cc31f70/pillow-12.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:30f2aa603c41533cc25c05acd0da21636e84a315768feb631c937177db558931", size = 7096716, upload-time = "2026-07-01T11:55:28.318Z" }, + { url = "https://files.pythonhosted.org/packages/23/26/fcb2f6e37175b04f53570b59937867e2b80ee1685e744023153028fc14f9/pillow-12.3.0-cp314-cp314t-win32.whl", hash = "sha256:4b0a7fe987b14c31ebda6083f74f22b561fd3739bc0ac51e019622e3d72668c7", size = 6474125, upload-time = "2026-07-01T11:55:30.956Z" }, + { url = "https://files.pythonhosted.org/packages/90/de/3634abee5f1c9e13c56787b7d5517b0ba8d6de51700b95578cf338349c9f/pillow-12.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:962864dc93511324d51ddbb5b9f8731bf71675b93ca612a07441896f4688fb8c", size = 7242939, upload-time = "2026-07-01T11:55:34.044Z" }, + { url = "https://files.pythonhosted.org/packages/ce/2a/fd13f8eb24de5714a6eb444a3d67e2842c6c576e159a43793adf23051351/pillow-12.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0740a512dc522224c77d9aa5a8d70d8b7d73fb91f2c21125d8d025d3b8990e45", size = 2567506, upload-time = "2026-07-01T11:55:35.988Z" }, + { url = "https://files.pythonhosted.org/packages/5d/dc/8fdce34ec725a33c81c6ba122b904d6b9024e50ea9ac7bede62fab54506c/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0feb2e9d6ad6c9e3c06effe9d00f3f1e618a6643273576b016f591e9315a7139", size = 4162063, upload-time = "2026-07-01T11:55:37.941Z" }, + { url = "https://files.pythonhosted.org/packages/76/66/2044b9a63d3b84ff048228dfcb7cd9bf0df983e8470971bf7d4c57b693de/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:9e881fca225083806662a5c43d627d215f258ff43c890f831966c7d7ba9c7402", size = 4255549, upload-time = "2026-07-01T11:55:40.022Z" }, + { url = "https://files.pythonhosted.org/packages/52/7e/1f67e6f4ece6b582ee4b539decbcc9f848dc245a93ed8cd7338bafef72f1/pillow-12.3.0-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:4998562bf62a445225f22e07c896bb04b35b1b1f2eb6d760584c9c51d7a5f78c", size = 3696331, upload-time = "2026-07-01T11:55:41.98Z" }, + { url = "https://files.pythonhosted.org/packages/12/40/d306fc2c8e4d45d7f175c77edca7063be7b86fe7fe6e68f4353bf71d808c/pillow-12.3.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:dc624f6bc473dacdf7ef7eb8678d0d08edf15cd94fad6ae5c7d6cc67a4e4902f", size = 5350370, upload-time = "2026-07-01T11:55:44.028Z" }, + { url = "https://files.pythonhosted.org/packages/dd/44/668fb1437e8ce420f62d6106eb66e44a5971602a4d794615bdf79315d82d/pillow-12.3.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:71d6097b330eea8fd15097780c8e89cb1a8ce7838669f48c5bacd6f663dd4701", size = 4780147, upload-time = "2026-07-01T11:55:46.073Z" }, + { url = "https://files.pythonhosted.org/packages/0c/08/93fa2e70e30a2d81547e481b6ee2bb9522117221fb1e0ce4b5df70967677/pillow-12.3.0-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:28ce87c5ab450a9dd970b52e5aca5fe63ed432d18a2eaddd1979a00a1ba24ace", size = 6273659, upload-time = "2026-07-01T11:55:48.264Z" }, + { url = "https://files.pythonhosted.org/packages/f8/6d/043e96ff814fc31a33077e4cba86082167db520c93632afdf2042febbb0c/pillow-12.3.0-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b02afb9b97f65fbca5f31db6a2a3ba21aa93030225f150fa3f249717e938fb4", size = 6947439, upload-time = "2026-07-01T11:55:50.503Z" }, + { url = "https://files.pythonhosted.org/packages/af/92/ba71d2ee2ac0edf3fa33bd9d5ee9ee080da70b1766f3ca3934f9938ddac9/pillow-12.3.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:1182d52bc2d5e5d7d0949503aa7e36d12f42205dc287e4883f407b1988820d39", size = 6353577, upload-time = "2026-07-01T11:55:52.697Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ce/e63064e2122923ff687c8ad792d0d736a7b3920a56a46982e81a7fdd25d6/pillow-12.3.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e795b7eb908249c4e43c7c99fac7c2c75dab0c43566e37db472a355f63693d71", size = 7060394, upload-time = "2026-07-01T11:55:55.149Z" }, + { url = "https://files.pythonhosted.org/packages/54/76/a09cc3ccc8d773a7283d34c38bec1708f9e3cc932093cbc4c5e71ac4060b/pillow-12.3.0-cp315-cp315-win32.whl", hash = "sha256:57b3d78c95ba9059768b10e28b813002261d3f3dfc55cc48b0c988f625175827", size = 6467375, upload-time = "2026-07-01T11:55:57.769Z" }, + { url = "https://files.pythonhosted.org/packages/3e/03/1846c49ba3b1d5550392a4bbd06d6fb4578e1cd91a803198b5c90f5f7d53/pillow-12.3.0-cp315-cp315-win_amd64.whl", hash = "sha256:fa4ecea169a355be7a3ade2c783e2ed12f0e40d2c5621cda8b3297faf7fbb9f5", size = 7237048, upload-time = "2026-07-01T11:55:59.975Z" }, + { url = "https://files.pythonhosted.org/packages/fb/bb/89f35dcc79610423f9f195504d7def7f0d1416a711541b42867e25fe3412/pillow-12.3.0-cp315-cp315-win_arm64.whl", hash = "sha256:877c3f311ff35410f690861c4409e7ccbf0cd2f878e50628a28e5a0bb689e658", size = 2566006, upload-time = "2026-07-01T11:56:02.143Z" }, + { url = "https://files.pythonhosted.org/packages/30/88/707027ba09942dfa2c28759b5c222d769290a41c6d20ea60ec250801941f/pillow-12.3.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:e9871b1ffbfa9656b60aeee92ed5136a5742696006fa322b29ea3d8da0ecc9cf", size = 5352509, upload-time = "2026-07-01T11:56:04.2Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6d/00352fa25332c2569cd387851f568cc5a4b75a9adbfb37ac4fbce4c02eec/pillow-12.3.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:53aa02d20d10c3d814d536aa4e5ac9b84ca0ff5a88377963b085ad6822f93e64", size = 4783167, upload-time = "2026-07-01T11:56:06.631Z" }, + { url = "https://files.pythonhosted.org/packages/13/4f/9e049dfa21af7c22427275720e2490267ba8138120add5c4c574deb69782/pillow-12.3.0-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:446c34dcc4324b084a53b705127dc15717b22c5e140ae0a3c38349d4efec071e", size = 6329237, upload-time = "2026-07-01T11:56:08.868Z" }, + { url = "https://files.pythonhosted.org/packages/36/16/cf6eeaae8d0fce8dd390a33437cf68c5d5bd73834a2bc6e2f14efda0ab45/pillow-12.3.0-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf1845d02ad822a369a49f2bb9345b1614744267682e7a03527dc3bf6eea1777", size = 6997047, upload-time = "2026-07-01T11:56:11.379Z" }, + { url = "https://files.pythonhosted.org/packages/1e/69/dbf769bdd55f48bf5733cac28edc6364ffaa072ec9ba336266e4fe66be55/pillow-12.3.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:186941b6aef820ad110fb01fb06eb925374dc3a21b17e37ec9a53b250c6fe2d1", size = 6400440, upload-time = "2026-07-01T11:56:13.908Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e1/ffc9cfc2eea0d178da8018e18e959301ad9d6bc9f3edb7181e748a474b97/pillow-12.3.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:f13c32a3abd6079a66d9526e18dad9b6d280384d49d7c54040cd57b6424041d9", size = 7105895, upload-time = "2026-07-01T11:56:16.575Z" }, + { url = "https://files.pythonhosted.org/packages/18/f0/a5595c1e8c3ae44b9828cb2f0fa8155e5095ef04d6327b8f61cf44a3df85/pillow-12.3.0-cp315-cp315t-win32.whl", hash = "sha256:1657923d2d45afb66526e5b933e5b3052e6bdea196c90d3abb2424e18c77dae8", size = 6474384, upload-time = "2026-07-01T11:56:18.855Z" }, + { url = "https://files.pythonhosted.org/packages/e4/04/62bcd9f844984c5938d3b05264a61d797a29d3e0812341a8204af70bbdee/pillow-12.3.0-cp315-cp315t-win_amd64.whl", hash = "sha256:8cd2f7bdda092d99c9fc2fb7391354f306d01443d22785d0cbfafa2e2c8bb418", size = 7243537, upload-time = "2026-07-01T11:56:21.214Z" }, + { url = "https://files.pythonhosted.org/packages/3d/68/1f3066acedf37673694a7141381d8f811ae97f30d34413d236abe7d489f1/pillow-12.3.0-cp315-cp315t-win_arm64.whl", hash = "sha256:06ff022112bc9cbf83b60f8e028d94ad87b60621706487e65f673de61610ab59", size = 2567491, upload-time = "2026-07-01T11:56:23.506Z" }, + { url = "https://files.pythonhosted.org/packages/75/18/2e8b40223153ccbc60df07f9e8928dc0c76202aa4e55ae9f53962b6510d6/pillow-12.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b3c777e849237620b022f7f297dd67705f9f5cf1685f09f02e46f93e92725468", size = 5302510, upload-time = "2026-07-01T11:56:25.736Z" }, + { url = "https://files.pythonhosted.org/packages/46/3e/51fabf59d5ab801ceab709453d3ab6b180083496579549de4c45ced6528a/pillow-12.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:b343699e8308bdc51978310e1c959c584e7869cc8c40780058c87da7781a1e94", size = 4736058, upload-time = "2026-07-01T11:56:28.041Z" }, + { url = "https://files.pythonhosted.org/packages/bf/20/22fe9384b7949e25fb1293bcfc84fb82590ff4ea6b37c95b24d26d793d86/pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbd139c8447d25dd750ab79ee274cc5e1fe80fc56340ab10b18a195e1b6eca3e", size = 5237776, upload-time = "2026-07-01T11:56:30.263Z" }, + { url = "https://files.pythonhosted.org/packages/08/14/f6ba68107680ffa74b39985f3f30884e41318fbc4250caa423c79b4788bb/pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7e480451b9fa137494bccd3a7d69adbe8ac65a87d97be61e11f1b1050a5bac3", size = 5860358, upload-time = "2026-07-01T11:56:32.68Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0169bc772ec491108b62f644f8ecf1fe5d8ae5ebafde2ee2142210166903/pillow-12.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:04f01d28a6aaff387bf842a13be313df23ba0597a44f1a976c9feb3c6ff4711a", size = 7231786, upload-time = "2026-07-01T11:56:35.046Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.11.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/69/b7/802a56eca9f2fac455b8bab5375a2647b0f0e14a2cd63ef077de3c4a7658/platformdirs-4.11.7.tar.gz", hash = "sha256:4f41487eeeeeb07f3a6625e61d9bc0ae6809f92d3386dbd74392fbb76108104d", size = 35127, upload-time = "2026-09-01T13:35:10.502Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/6e/80993e10a0482f630cef528635789233224f36b1ffd11592aa15d13ff9ce/platformdirs-4.11.7-py3-none-any.whl", hash = "sha256:8a02cb259042c79d1cd0450facc2fe6dc9d303ae7901afbe33bf8ea0b188cef6", size = 23938, upload-time = "2026-09-01T13:35:09.02Z" }, +] + +[[package]] +name = "pooch" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "platformdirs" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/43/85ef45e8b36c6a48546af7b266592dc32d7f67837a6514d111bced6d7d75/pooch-1.9.0.tar.gz", hash = "sha256:de46729579b9857ffd3e741987a2f6d5e0e03219892c167c6578c0091fb511ed", size = 61788, upload-time = "2026-01-30T19:15:09.649Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/2d/d4bf65e47cea8ff2c794a600c4fd1273a7902f268757c531e0ee9f18aa58/pooch-1.9.0-py3-none-any.whl", hash = "sha256:f265597baa9f760d25ceb29d0beb8186c243d6607b0f60b83ecf14078dbc703b", size = 67175, upload-time = "2026-01-30T19:15:08.36Z" }, +] + +[[package]] +name = "pycairo" +version = "1.29.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/7c/ab4637b028d9ee12280ab9342f28310e613d5ca30c4d72d2aebeaa6a4969/pycairo-1.29.1.tar.gz", hash = "sha256:4fbd26b4af24c9787d84cf5448e34eb8dca064b732479aaecd03109520eebd5f", size = 666272, upload-time = "2026-08-07T08:45:50.095Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/70/358f2a617c024d491a8db8cc40f090c2fdb771c13ae5afb2c82471166663/pycairo-1.29.1-cp310-cp310-win32.whl", hash = "sha256:46abf30e61d4c27022296e858af900d3e5dae764c1e6acfb9b9e4082af860131", size = 751304, upload-time = "2026-08-07T08:46:02.516Z" }, + { url = "https://files.pythonhosted.org/packages/b7/29/359b2bacc7e2d85e02e18fc4edd6e6f9c3807c518e04e95c5222c5980d05/pycairo-1.29.1-cp310-cp310-win_amd64.whl", hash = "sha256:507fefbddc8eeb27862a815c4a4ec648f06d6e22a671e00dcbe766a899e4bcef", size = 845458, upload-time = "2026-08-07T08:46:03.838Z" }, + { url = "https://files.pythonhosted.org/packages/b3/1c/a8f3649b867fd7e176ac530935fd00fb0e22dd9863c84f304ccb80b1878b/pycairo-1.29.1-cp310-cp310-win_arm64.whl", hash = "sha256:eae57db7719c8b717f0d69fe81a1586621dcbfd7f92a1277a181dd89e754be25", size = 694624, upload-time = "2026-08-07T08:46:05.458Z" }, + { url = "https://files.pythonhosted.org/packages/4d/83/d2dfb322cc3acc583aa6e8e601e7c320108804f3f3c677383f23a0de8624/pycairo-1.29.1-cp311-cp311-win32.whl", hash = "sha256:50da6e8eb1525f216e564b3e634b19ede5465080d4fa813416ed45a69b3f6605", size = 751309, upload-time = "2026-08-07T08:46:07.317Z" }, + { url = "https://files.pythonhosted.org/packages/34/e6/f09d1c0ca4a887aea020774c59adabc04b7e1d2f00287ef48473540001e5/pycairo-1.29.1-cp311-cp311-win_amd64.whl", hash = "sha256:73a364341ce6b382200d13cfdf814f8ea50a8a2e0f8280c2d2e7dfb3ba52edcf", size = 845463, upload-time = "2026-08-07T08:46:09.287Z" }, + { url = "https://files.pythonhosted.org/packages/69/a6/84415dce82c4865169847acc2de7895a1463956569383f17cb39e89f1287/pycairo-1.29.1-cp311-cp311-win_arm64.whl", hash = "sha256:59eb4a6d1101834a7ba2c7a8096fccd54eb8514d8189f896b4a8e24a19cfa1f0", size = 694560, upload-time = "2026-08-07T08:46:10.657Z" }, + { url = "https://files.pythonhosted.org/packages/72/f6/b6a2e016cf0df97b2cd094c228c9303af21d626993607d8ea750ea8b704b/pycairo-1.29.1-cp312-cp312-win32.whl", hash = "sha256:cf25a97a15bedd0cdabee5b18652de45eff70b4a7999416ed18dcee2144d17f5", size = 751363, upload-time = "2026-08-07T08:46:12.545Z" }, + { url = "https://files.pythonhosted.org/packages/69/7e/7be1346df5ab8cd7f748f2d17d25d61f34550cfd141f60e8319e9c70df87/pycairo-1.29.1-cp312-cp312-win_amd64.whl", hash = "sha256:839cd8c98e1933426d8959168b466901b398bf4867454451023b93d91030cca9", size = 846004, upload-time = "2026-08-07T08:46:14.067Z" }, + { url = "https://files.pythonhosted.org/packages/04/97/40966895bc12319ff3173cbb5d7876149a8e90e99bbf3b70e4af492c73a9/pycairo-1.29.1-cp312-cp312-win_arm64.whl", hash = "sha256:7cb6279cf0a7951ae9aaa21c6a0adff0fce62c0a68797b6a26a54f9a0859ad4d", size = 694560, upload-time = "2026-08-07T08:46:15.839Z" }, + { url = "https://files.pythonhosted.org/packages/95/7d/26191cac03e8c0d957856839736ee9fb45c6d4ae68c30393895558f1f7ad/pycairo-1.29.1-cp313-cp313-win32.whl", hash = "sha256:4506a15f0dcd9d65b2706f417f8e5633e1456867d5cf272dc69c0b48e2302842", size = 750753, upload-time = "2026-08-07T08:46:17.523Z" }, + { url = "https://files.pythonhosted.org/packages/81/ba/3499ae0f6f8be6d55ef5db620a60693b4ef925b710890bf66c17aeff036e/pycairo-1.29.1-cp313-cp313-win_amd64.whl", hash = "sha256:859f5f29d7c30fc28dbc5ce91eb30a45e0b7e447a7bd738c231dd7f9be368d1a", size = 844528, upload-time = "2026-08-07T08:46:19.1Z" }, + { url = "https://files.pythonhosted.org/packages/91/c1/f83f25aab64418d4de6527cbf951988fc9449020e416113e3a7a4ad7e7b8/pycairo-1.29.1-cp313-cp313-win_arm64.whl", hash = "sha256:0c6159117680798da1457404b648e69b0a0eb23597b22f8a97315eaeea19f917", size = 693588, upload-time = "2026-08-07T08:46:21.087Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ef/87408894ebef901c2accde14dcacfbdbd9bc9abe617039ee6ff382847b75/pycairo-1.29.1-cp314-cp314-win32.whl", hash = "sha256:7222587a4ae1674287c17d10d1c42c4219914f7ef64cac4b2fb995df4f7918cb", size = 767089, upload-time = "2026-08-07T08:46:25.92Z" }, + { url = "https://files.pythonhosted.org/packages/1d/13/aef0ea29661a2fa0cadb3cbb470b514d439efe763cafc956c8b740ad1480/pycairo-1.29.1-cp314-cp314-win_amd64.whl", hash = "sha256:f635a60b7525661b562061e4b1635670b804d1efc9c7f40796c2fa14b24c3ddf", size = 872072, upload-time = "2026-08-07T08:46:27.605Z" }, + { url = "https://files.pythonhosted.org/packages/68/95/fc823a255ed1967ec5bda7243247a2fb484faf868c0cd1ffd1425ae515e8/pycairo-1.29.1-cp314-cp314-win_arm64.whl", hash = "sha256:b5db545cc8e59fe9bb68039db86c5ebcf9d1e6417695d0d2cb5530e67d925af9", size = 719612, upload-time = "2026-08-07T08:46:29.127Z" }, + { url = "https://files.pythonhosted.org/packages/ef/25/4cd521dd9e0f280930717bb93499c50008a6f14b312649d34a54dbd1df71/pycairo-1.29.1-cp314-cp314t-win_amd64.whl", hash = "sha256:3836feabfaf35ef9c502234c5924f3b2db935b6c55d8850ed190b3a58ab3b4ca", size = 874492, upload-time = "2026-08-07T08:46:22.695Z" }, + { url = "https://files.pythonhosted.org/packages/c2/ee/006df2caa60f2df242b9baf13ab95d4575b47acabc54f3ab6d757d3a9691/pycairo-1.29.1-cp314-cp314t-win_arm64.whl", hash = "sha256:85127fb834a8a37a550fa5c684a4c49fc546b465a64ac8df94d7728914a56279", size = 721461, upload-time = "2026-08-07T08:46:24.104Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ec/4466cce6363c07759d04f9481f81d84a6dc506b735e795c193eb63e4f9ef/pycairo-1.29.1-cp315-cp315-win_amd64.whl", hash = "sha256:5c51bd98d2190841389845deae8243479393c1f2d9423de85e9fb4c95afebc10", size = 872007, upload-time = "2026-08-07T08:46:34.392Z" }, + { url = "https://files.pythonhosted.org/packages/92/03/214ee759a86bd821e306c2071aef0de9c201020e7690f9cf165fa2d61d42/pycairo-1.29.1-cp315-cp315-win_arm64.whl", hash = "sha256:e923875c96cfe935d632de50aeebf49db7dffdd23f9042587c3f34af7596868e", size = 719602, upload-time = "2026-08-07T08:46:35.802Z" }, + { url = "https://files.pythonhosted.org/packages/14/df/b163de08eb5307eacda6ee98001a4b920df8432d49f1da9b8d1c8d0342f5/pycairo-1.29.1-cp315-cp315t-win_amd64.whl", hash = "sha256:12515e71e2f17db8c40911ff089c23c465f92ddbabb89cd01304f275030c80ec", size = 874517, upload-time = "2026-08-07T08:46:30.779Z" }, + { url = "https://files.pythonhosted.org/packages/ab/04/81a13cde3c6685186fd79770d619c9b861d51de7450975618a85e8676772/pycairo-1.29.1-cp315-cp315t-win_arm64.whl", hash = "sha256:e5dde8dc08d92ad6935d1a45ff6ea71ea6d5739ed08dbf8677313935f94d5c6d", size = 721459, upload-time = "2026-08-07T08:46:32.19Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pydub" +version = "0.25.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/9a/e6bca0eed82db26562c73b5076539a4a08d3cffd19c3cc5913a3e61145fd/pydub-0.25.1.tar.gz", hash = "sha256:980a33ce9949cab2a569606b65674d748ecbca4f0796887fd6f46173a7b0d30f", size = 38326, upload-time = "2021-03-10T02:09:54.659Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/53/d78dc063216e62fc55f6b2eebb447f6a4b0a59f55c8406376f76bf959b08/pydub-0.25.1-py2.py3-none-any.whl", hash = "sha256:65617e33033874b59d87db603aa1ed450633288aefead953b30bded59cb599a6", size = 32327, upload-time = "2021-03-10T02:09:53.503Z" }, +] + +[[package]] +name = "pyglet" +version = "2.1.16" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8d/af/72a0c4fc95339ec590f441a591143e04acb27b2c94845735809c1630ade6/pyglet-2.1.16.tar.gz", hash = "sha256:ca90ee05532ced8330b8a8087ac2a7b69589b7fa7fb67f85aabe24aaf4ae42c0", size = 6598246, upload-time = "2026-08-01T01:32:35.895Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/93/b81e304f8954876fed84c5afffe83d2c1c0ceed064d5003b87fbfe9f9076/pyglet-2.1.16-py3-none-any.whl", hash = "sha256:27f9cabf97a64b15d335cf70d9e8979bbab5f84a9900a058206bcd3a2de341ae", size = 1037857, upload-time = "2026-08-01T01:32:30.566Z" }, +] + +[[package]] +name = "pyglm" +version = "2.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/8b/bdaf7b9cacecd28f7b4c6fc2d7d136824c506ad38cfdb37a05ea7ec88694/pyglm-2.8.3.tar.gz", hash = "sha256:161781ea4d1267f796b645f85ebff53aeb8ee4f13b4e993c04d64c96d286e534", size = 584038, upload-time = "2025-11-26T12:12:59.47Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/4c/181816f46073c1875a7f9ac5c6edcebc37244cccf437af78e044c0b9d983/pyglm-2.8.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:159547399d3e700cdf2e6b6480c467f0a2e1abf140c01d395b7228793bda46ad", size = 1624313, upload-time = "2025-11-26T12:10:53.078Z" }, + { url = "https://files.pythonhosted.org/packages/d8/fa/e7d73ea259cc7a3ebcbf80f82e13d0e619afdfa4e2b23cc36c7a2d3c5fdd/pyglm-2.8.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3986e7c3b9d0b112972cc2dc2f02b6720cea16aca6ec38eee5bb9effa17af035", size = 1359863, upload-time = "2025-11-26T12:10:55.096Z" }, + { url = "https://files.pythonhosted.org/packages/96/61/77498ca6fdfaecbf7016b89d303e687a94402762c332b538b1ff2a84a405/pyglm-2.8.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c804ac7a678db3441f15f09cb600ed23c504ceab0d8129fc8818699074f816ba", size = 11511439, upload-time = "2025-11-26T12:10:56.654Z" }, + { url = "https://files.pythonhosted.org/packages/b2/be/8a52a6c0ece5caeccd1e752257ca426e7395a2afe2281443fc6b79a03806/pyglm-2.8.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b24bd5f9ba4b6966a4bf7d38931cdf8db3a94cafac40e85fc64ec245f2289efa", size = 12170669, upload-time = "2025-11-26T12:10:59.656Z" }, + { url = "https://files.pythonhosted.org/packages/21/b8/3c7ce31c14042fa1ca00e42196001d1561d4b6f508da3d6b0d86c4ee7ea6/pyglm-2.8.3-cp310-cp310-manylinux_2_34_aarch64.whl", hash = "sha256:463113f24a6b41eff41721c4c65013092368cf56ec32e0523b71a3df43d34cbf", size = 10449600, upload-time = "2025-11-26T12:11:02.18Z" }, + { url = "https://files.pythonhosted.org/packages/7c/6f/34bd16721b269446b997c09c015b5accecfecd4b233b3837983c2665bfe6/pyglm-2.8.3-cp310-cp310-manylinux_2_34_x86_64.whl", hash = "sha256:a5d9e0fe8778cc16a18fdf594e4f672f3e18cac4b38c74d226e29badf0a3844d", size = 11279856, upload-time = "2025-11-26T12:11:04.339Z" }, + { url = "https://files.pythonhosted.org/packages/c5/1d/ab166ec5885d2bd48ab14c628292445b5b4137d06f0643c715286593560c/pyglm-2.8.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:19b8d942bf9ae5349e78c851094407af3c0ee967684dff11fd13954380fe48ca", size = 11506946, upload-time = "2025-11-26T12:11:06.48Z" }, + { url = "https://files.pythonhosted.org/packages/1d/40/efeb069603adedff32993befea76d796021a5614864da242a798c3fb0b3c/pyglm-2.8.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a9f9e0ab479a51d5482a3b1b0eec8b28cfd3436d9b213ecb71c706c0eb9da45f", size = 12346110, upload-time = "2025-11-26T12:11:09.002Z" }, + { url = "https://files.pythonhosted.org/packages/85/f0/91244701fe9f1ecb383d823b24eceee7465666da38ba4d530c971d58e94c/pyglm-2.8.3-cp310-cp310-win_amd64.whl", hash = "sha256:f6f906e9e135b8a7f8548e31500a26bf2de94c507feace6f1a999d57774fa2b2", size = 1644743, upload-time = "2025-11-26T12:11:10.848Z" }, + { url = "https://files.pythonhosted.org/packages/75/df/7ac777b69f6e9c52ae22a6a1fbf85837cae6ec4c3cc6596a6d638bc13eb8/pyglm-2.8.3-cp310-cp310-win_arm64.whl", hash = "sha256:638e7cd772d2a3cc0f1cc64904085b6eaf202d2790605a04e7c1bd559f06e99b", size = 1225324, upload-time = "2025-11-26T12:11:12.108Z" }, + { url = "https://files.pythonhosted.org/packages/db/08/3a3e227515a7e4511699bb467379e8184fe883ba17a96adfa8b246e4a7b1/pyglm-2.8.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:94c7eb9c967423a123306f3a1bf66691cd66ed5ef17165c29424c096a2f96537", size = 1624376, upload-time = "2025-11-26T12:11:13.161Z" }, + { url = "https://files.pythonhosted.org/packages/f7/23/beab60070ef7dcfbac7de7e6512e42eea8ffad0a11ff59ab6b1c82849ef5/pyglm-2.8.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:43ac57b7ec33f0ec1e7224a4a8833c0f1405c446177e5abcd6d87867d286f8b3", size = 1359776, upload-time = "2025-11-26T12:11:14.27Z" }, + { url = "https://files.pythonhosted.org/packages/5d/39/8449cc2901a6693e89ed2f5d6913d99cc16816e8b539662700c11e341487/pyglm-2.8.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4ccdc19eb1b432297aebc60e8f72f7870186f2defdaf2e0d0a5bc449057dad01", size = 12081814, upload-time = "2025-11-26T12:11:15.601Z" }, + { url = "https://files.pythonhosted.org/packages/a5/52/a3e9b3e91b312e0b21e21ba587bca9601e032e33e2693be7a9c864660e72/pyglm-2.8.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2f1424cd0a5d49bc0d8c43a9028a471dcb90a22ba1954baad9158659726f256c", size = 12703254, upload-time = "2025-11-26T12:11:17.402Z" }, + { url = "https://files.pythonhosted.org/packages/c6/c7/f76f2dd862a00e1a34e9ef76a6d5ccf7454224f734b9ff5d5780dd6ddf16/pyglm-2.8.3-cp311-cp311-manylinux_2_34_aarch64.whl", hash = "sha256:32e7a2815f39a49f5434d66d2d109c020d3e7125044905d3afebcb597d6a583e", size = 10827408, upload-time = "2025-11-26T12:11:19.066Z" }, + { url = "https://files.pythonhosted.org/packages/7c/5d/73cfdde12032bf741cb2e3de5467814b57611d32f5e42c4196847a75e777/pyglm-2.8.3-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:96da922be7d7d754f2ea269687c9215906ecc651b5f197ff92dcb497230d5f57", size = 11646495, upload-time = "2025-11-26T12:11:20.895Z" }, + { url = "https://files.pythonhosted.org/packages/4b/42/ef3cddd57fb8e0ab5b44bd0146704efa1e651015bf85296b16e0413890e7/pyglm-2.8.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:71132521c65b43badb3338edc12102eafacc4bf8597378ea7bb57a700975ce77", size = 11876783, upload-time = "2025-11-26T12:11:22.723Z" }, + { url = "https://files.pythonhosted.org/packages/73/70/55c2c56c9b29e9862ddafabfc90b97615c0470424fdddfaff7c56092e79c/pyglm-2.8.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:957816050c3bded151be3183e3160e7fc6b31f708b17d282f6c401ba1536f2a4", size = 12711844, upload-time = "2025-11-26T12:11:24.948Z" }, + { url = "https://files.pythonhosted.org/packages/ee/1e/891cede4878d7b0644b96af3999c5417c50bfccce18b2653ffc02e7483fe/pyglm-2.8.3-cp311-cp311-win_amd64.whl", hash = "sha256:ae0dd576e89638654079f554d3f41b5d463ff6fee68961e4cd8b23069bff7e55", size = 1659367, upload-time = "2025-11-26T12:11:27.071Z" }, + { url = "https://files.pythonhosted.org/packages/e4/7b/2042502444d7d823e1e5f8c8776f8fe029b90da57ae76ec245c4f2263e39/pyglm-2.8.3-cp311-cp311-win_arm64.whl", hash = "sha256:28dba29d5b80cd9e81cf9875290bcedb451c83ca532de90c451d5b3a79727fcc", size = 1240884, upload-time = "2025-11-26T12:11:28.177Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a0/8759ed290b8d9830a6beda947c48cfb4e7ac9a14c04c8b612e1954dd0cf8/pyglm-2.8.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:72c75bd76ade851bf5c2b12cbe384193ea901a73474d57e4d7044bbbcd7d08fd", size = 1608534, upload-time = "2025-11-26T12:11:29.277Z" }, + { url = "https://files.pythonhosted.org/packages/77/36/5feaf47a5f105cf478505cde5e32b4ce19649a11f802afaee1063ad3adaf/pyglm-2.8.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:570da7032ff1185c842c5864d080ee5eab091fa53ec217b8d4d4034c22ba744f", size = 1362644, upload-time = "2025-11-26T12:11:30.774Z" }, + { url = "https://files.pythonhosted.org/packages/f9/6d/840566e3cbadb4d66d60c112f2c507260b46aa674e6fd22913493cfb7cd4/pyglm-2.8.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a14f1ffdc30a6f53334d16651888f60a2f8a63e05613357234c5592747b906bf", size = 12155701, upload-time = "2025-11-26T12:11:32.54Z" }, + { url = "https://files.pythonhosted.org/packages/f1/97/3e6648727f597885f4e6d92b77b49dc2fa2b65a2b9ea3ab35caaff06e072/pyglm-2.8.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1d2305b258bebc358e00b8c85c3bc2d6f78d64f9b3f8802e1699c3e8dae54361", size = 12785652, upload-time = "2025-11-26T12:11:34.449Z" }, + { url = "https://files.pythonhosted.org/packages/e4/9c/04c525d78356e6493d0abc3ea945802befd5b813b1820e5b78dd5ca2f072/pyglm-2.8.3-cp312-cp312-manylinux_2_34_aarch64.whl", hash = "sha256:f80d208e558a024a3dc83300bd661a71aa4cda884a256c5b87179059a1c55373", size = 10886479, upload-time = "2025-11-26T12:11:36.324Z" }, + { url = "https://files.pythonhosted.org/packages/eb/20/d4003e53a590b8dd8fa9ab2e6848a1acaeb8d239c95161dc2e05f5972c9c/pyglm-2.8.3-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:8abf221111db95e34620e446a853d43c887ced10d5c217e2df18471c9ac683ef", size = 11711522, upload-time = "2025-11-26T12:11:38.639Z" }, + { url = "https://files.pythonhosted.org/packages/f0/15/2b1a7761b33c309f4c14ce505f671fb75cc2c4b285af8fdcfb6d580cc1be/pyglm-2.8.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:71ace7b15dd6d2ea9dbeadd43738ce69e7134547fc4674617084df5096e9b866", size = 11938446, upload-time = "2025-11-26T12:11:40.36Z" }, + { url = "https://files.pythonhosted.org/packages/9b/e7/9cbee60fbd66592a0cc46ad4e26c4bd26d55f45411323a5b03e3bd9a33d5/pyglm-2.8.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:04be7de24547130b8d413bc96ab9998d95db89cbaf224f401624ac1fa62bc431", size = 12773288, upload-time = "2025-11-26T12:11:42.44Z" }, + { url = "https://files.pythonhosted.org/packages/70/11/c80ffa11495b5788cfa5d021ae3bdbef20c941ead87911b2d50d534b2054/pyglm-2.8.3-cp312-cp312-win_amd64.whl", hash = "sha256:5f7ff6f4ac14b092d897931b9ef76bb8108b5faa6f5118963c9de86c6fa18efc", size = 1663004, upload-time = "2025-11-26T12:11:44.1Z" }, + { url = "https://files.pythonhosted.org/packages/f5/d6/a24ac1280fc1cd03fe1ff0fa2632fbafdee1a4abe9cd319cb0ae6aed99bf/pyglm-2.8.3-cp312-cp312-win_arm64.whl", hash = "sha256:ba2de300dbaf39cb6cd82ce7db05a8dcf85bb7dacf1ec65db18b1ebc34438a0d", size = 1239976, upload-time = "2025-11-26T12:11:45.196Z" }, + { url = "https://files.pythonhosted.org/packages/2d/22/ee11dff20adfc6aac3e0482ed275843e0b9854dbc2e812ab56cdacbed8d6/pyglm-2.8.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7a6d07b6b73e55a36e1b6daa63acb53f4912f2ddf88f434af4cc70441435aef8", size = 1608547, upload-time = "2025-11-26T12:11:46.426Z" }, + { url = "https://files.pythonhosted.org/packages/1e/5f/10c4cd636c3e6c63328a476f84bb03501d8016f12053c3a82ba588fc61ce/pyglm-2.8.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eaf6241f7c3ff169575e11da78f4439422517e42558961332db4cd09e9599267", size = 1362651, upload-time = "2025-11-26T12:11:47.555Z" }, + { url = "https://files.pythonhosted.org/packages/ff/70/2c7fe768900ee9d0f87e7a89375fa7d83b5b0a8f0eee8d0ad06b22a96e37/pyglm-2.8.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e1ec0dfb8f2c848c4ee6330c70a1ca9333004776c8e5ec76096e0b67c739f688", size = 12157160, upload-time = "2025-11-26T12:11:49.042Z" }, + { url = "https://files.pythonhosted.org/packages/b0/a6/befefccf1c8a0a66f09a7a1a1d324a3f988cb5e6b634026280518e5a83cd/pyglm-2.8.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3eea9093210afb946769c84fe31f17a9f73696a1161ee84fb42e9255f8c5cc8a", size = 12787899, upload-time = "2025-11-26T12:11:50.946Z" }, + { url = "https://files.pythonhosted.org/packages/7c/d2/5475d0791b585ae26dc0d690862a0e1e6fda243552a1718628dd937e5543/pyglm-2.8.3-cp313-cp313-manylinux_2_34_aarch64.whl", hash = "sha256:0c652650912dbd88994fa02ec9dba2f3b35dc3995427b6ae8056ae542d5a060a", size = 10886936, upload-time = "2025-11-26T12:11:53.628Z" }, + { url = "https://files.pythonhosted.org/packages/e1/a1/035068410f60ed53007e0488af96f7e4634b679e9156d1090e70732b2159/pyglm-2.8.3-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:e3bdda68f75ad270e66b0fc7a1883749489cca14c44cbe22bac038fa170a8a1c", size = 11711601, upload-time = "2025-11-26T12:11:55.367Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e6/286d1879eae87199f086136b08c70a3bff27880417f98dadc0cbcdb65e50/pyglm-2.8.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:08f8b1bd0d80ce396ee9cd5d3d4c7aeb4bbfa2a54dc73413924e3c9982412528", size = 11939755, upload-time = "2025-11-26T12:11:57.23Z" }, + { url = "https://files.pythonhosted.org/packages/72/58/f40f109ac025bb18412db78e6e73ffc20a17b6b495de90352f5aabfb9982/pyglm-2.8.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:12fa61feefa5a255097d0887415bce9dd72c1dd5e5a8c6577121ee5348336a16", size = 12774242, upload-time = "2025-11-26T12:11:59.051Z" }, + { url = "https://files.pythonhosted.org/packages/17/fd/71b44ee5ac341e9979731a7b868e237f3775e140a9481cea79bae7abb83c/pyglm-2.8.3-cp313-cp313-win_amd64.whl", hash = "sha256:33118ef1d678ab573546757dee7f0a1ca2fba8e8d7760c9fe6320fe0cfa3deb7", size = 1663010, upload-time = "2025-11-26T12:12:00.914Z" }, + { url = "https://files.pythonhosted.org/packages/fa/1e/9b8ba9d4627585797d8bda952412c93a4c09b70cb25e64756a52accddc1e/pyglm-2.8.3-cp313-cp313-win_arm64.whl", hash = "sha256:73ff3785dfc4ce017626d7ab56d6711a7119c29e2e71294efed73810c1d307f9", size = 1239893, upload-time = "2025-11-26T12:12:02.032Z" }, + { url = "https://files.pythonhosted.org/packages/65/fe/494d7dce3fcaf0123e787320b4a558d8fe733d6d0e23b7fe51e687f88dc2/pyglm-2.8.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:b312012458d0537b3d84f24f4ba51fd3930df7a0773fc643e36f8df27b807c7a", size = 1610593, upload-time = "2025-11-26T12:12:03.136Z" }, + { url = "https://files.pythonhosted.org/packages/27/c0/37cced4a1b29957a29baebc1f0034be5d5419adf12da4b99140c56152cdb/pyglm-2.8.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3249bfe5352e18cc777fac591665679a99f270e1ff27cd11dc349af07684f007", size = 1366295, upload-time = "2025-11-26T12:12:04.176Z" }, + { url = "https://files.pythonhosted.org/packages/a7/ba/186176d1c3e26196ae4bec4b228bdbb1304576f2038a7f1635070923ee48/pyglm-2.8.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb8ea0d6721c763a26eacde59b2c9165719050dcf49c99a2857f6e1e5a5f30bb", size = 12140715, upload-time = "2025-11-26T12:12:05.429Z" }, + { url = "https://files.pythonhosted.org/packages/eb/3e/8d9f307649e9b79b34ae51303e46e7012cc2550d60e6dd00ea8d3d8c9cf2/pyglm-2.8.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:83effb89e2cf6dd79cf9ebecf2f9fbda3d25a92b61af264de3cadad408911de6", size = 12764551, upload-time = "2025-11-26T12:12:07.403Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d6/2481433abe537d7019d9ce75eba86b57e1731e3e0352fd1ac7f31f4d1883/pyglm-2.8.3-cp314-cp314-manylinux_2_34_aarch64.whl", hash = "sha256:e958d65ed55f2716fd8a3a2ef872cc52893ea7300d7feec62dccb27ec25fbc2f", size = 10869966, upload-time = "2025-11-26T12:12:09.198Z" }, + { url = "https://files.pythonhosted.org/packages/fe/bf/3c78a9718e1d26c5b6ec468a584eb30cd797c46b6edd08e79cb1d67e8bf0/pyglm-2.8.3-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:ec7cc14d2eb9f46a18012ee7c1a164e0395b058ceb6e341bf6d986316b698574", size = 11693025, upload-time = "2025-11-26T12:12:10.948Z" }, + { url = "https://files.pythonhosted.org/packages/d3/0d/00ef293153b6ca7e56d1be6facbd9d0a8e331d0c6d3114f47f2cf2913eda/pyglm-2.8.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4beb0ada21e7641a577f274496451befcde79965467ef4027bd933334b3de39b", size = 11920727, upload-time = "2025-11-26T12:12:12.845Z" }, + { url = "https://files.pythonhosted.org/packages/4e/10/d8ecf9b5ac3b6fc1b179b33dfbd087b8f4d4b12bab99b5794f569a15a99a/pyglm-2.8.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:77878c1b8c713b9e39fe32f870d82e20c864da3b11a3b875eca06c270b04cdbe", size = 12759345, upload-time = "2025-11-26T12:12:14.705Z" }, + { url = "https://files.pythonhosted.org/packages/a1/d1/cc1f75ee77fd2f9bcb122bc7f3a628710fe3f0bc517333fa9f38268a7065/pyglm-2.8.3-cp314-cp314-win_amd64.whl", hash = "sha256:15c77bc46ff69d945565309e13ca99c4a001d6a941a80c45f26fbdec80fa16c4", size = 1710524, upload-time = "2025-11-26T12:12:16.486Z" }, + { url = "https://files.pythonhosted.org/packages/35/51/74c0a3107567dc769c06b6c04c7bc828f33792e089036c637334b7e4c573/pyglm-2.8.3-cp314-cp314-win_arm64.whl", hash = "sha256:2b16ec33bd43c514502bae8de2b319d168259090e101e1cde79cd0a7d33e1185", size = 1271521, upload-time = "2025-11-26T12:12:17.611Z" }, + { url = "https://files.pythonhosted.org/packages/32/8e/867045b54da8257b21c71c0255ba7a231c251aae4b5f26b12eb50b651cc7/pyglm-2.8.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:6a1f1ab8debc06e0fdedb3f4285ded4bec38bf075652c393039838504767e6cf", size = 1633871, upload-time = "2025-11-26T12:12:18.791Z" }, + { url = "https://files.pythonhosted.org/packages/93/f5/81bb8b52e132dc1ecc87b7ffb50c714b4fd2f71f98b801dd376e036f942b/pyglm-2.8.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a7835d18747ab9f8e736e343cc35bee0a514f18add282f1fc8035945fcf9d9bd", size = 1387757, upload-time = "2025-11-26T12:12:20.497Z" }, + { url = "https://files.pythonhosted.org/packages/32/40/8581283c00e2a18a6bb20a9192e25a792b605c53dee1c3abb8871eb0d3ca/pyglm-2.8.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:af3ddee3d150bbef68ee7338ccd3e0710b75b08121b8efd52d786b0d2b6731be", size = 12724921, upload-time = "2025-11-26T12:12:21.802Z" }, + { url = "https://files.pythonhosted.org/packages/a0/0e/bc3c03038da822d1a66c38e166d4c89b6bdab846e576ac226442813af7f4/pyglm-2.8.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a76c8eaba0c58f5738e87be5efcd16c4e75540fe6ebfdf15c236a799a61358e", size = 13317946, upload-time = "2025-11-26T12:12:23.947Z" }, + { url = "https://files.pythonhosted.org/packages/3b/21/b1f52dc73d610e36aa3ccf3ef61634530e6f97c2ca809a057839bf578667/pyglm-2.8.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:879ee9ab3c8ab47b1de59fe7e593eda854b8349f274ca60f057b83f3a405b84d", size = 11371582, upload-time = "2025-11-26T12:12:25.768Z" }, + { url = "https://files.pythonhosted.org/packages/16/aa/e03cc7a2daceb1bbdf98780a991a323e4078254584cf6664984a626ecfe1/pyglm-2.8.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:3e37c15b6c3e08f960b34ff9ec42e73469dfd868aec214e8a347da6c9d0245d6", size = 12192848, upload-time = "2025-11-26T12:12:28.606Z" }, + { url = "https://files.pythonhosted.org/packages/69/07/be893aee24a9a8b5400feda4d032fad9d281047d4379aa9f5ef4bea1f6a4/pyglm-2.8.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd34a8670debef4a55bc756b14cbe8b0a4daa49f8f6850c86c5e11d20554927f", size = 12414214, upload-time = "2025-11-26T12:12:30.438Z" }, + { url = "https://files.pythonhosted.org/packages/0c/06/6bb4e8a09f7dd2bf8dc1f4cc5edc938fc1247a1b2770833515fb418fc695/pyglm-2.8.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:699f852e0335b79d0b664ba1c2d02cb4689256cda786e7780e821f60b0824c46", size = 13237516, upload-time = "2025-11-26T12:12:32.412Z" }, + { url = "https://files.pythonhosted.org/packages/2b/5c/7f15edd05020540748dad8f5eae0a07ed7f14f699bff530172b8850bd998/pyglm-2.8.3-cp314-cp314t-win_amd64.whl", hash = "sha256:78caadaf9cc2ddea1c55b0d44fa8032f35c9f821a6f152b72422e5657d38f01d", size = 1764614, upload-time = "2025-11-26T12:12:34.498Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f9/8bc8d010503a250319c55d2ceec5f90b0e807c7f85d581638c5ecd0a81de/pyglm-2.8.3-cp314-cp314t-win_arm64.whl", hash = "sha256:69400ad1852ca0972e4d9cbef9d9510941d4b81dc0fffebc5ac796a85440a119", size = 1292992, upload-time = "2025-11-26T12:12:35.681Z" }, +] + +[[package]] +name = "pygments" +version = "2.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" }, +] + +[[package]] +name = "pyobjc-core" +version = "12.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/78/abc4ce5920305780aeb36b4067a86253378b36e29ba96673a3deb02eb03a/pyobjc_core-12.2.2.tar.gz", hash = "sha256:3906452339cd06a3bb07df103c2511d4cb0f7a22d8771c0b802eba15d9a642b6", size = 1067701, upload-time = "2026-08-11T19:43:39.059Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/1d/baf7197cee12f32a8eb9f8633093da1ec1ea702b0e1346bc1c7bfe022673/pyobjc_core-12.2.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:56c6c39f1de059fcbb174ebca5525505fc8feaa89be2a28c329bf09b6b25ee75", size = 6483051, upload-time = "2026-08-11T13:55:45.262Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8e/18284fec7913ef78b25a1c97f9689ebef98bc14038386191491516abeb25/pyobjc_core-12.2.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b9cdd686e32db8e451feb19f8a85bc4cd52c2893103881d04aca51e1f35371d1", size = 6481574, upload-time = "2026-08-11T14:13:27.153Z" }, + { url = "https://files.pythonhosted.org/packages/86/b2/bbf7f049880ab40d110e66f25122342a1f6c98d6fe3c59bb98985503c660/pyobjc_core-12.2.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:122e6ad302a2abf5d4d4adb0156db751600ddf2768441696cba17b31323085e7", size = 6427637, upload-time = "2026-08-11T14:51:36.038Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ed/a8bf040caf3704023d74086b7fb96cf4ed2e844e24bd94e5248ba214b700/pyobjc_core-12.2.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:950bd2d9c74634398c4e3d24ef2f213d4e23d705083697464fa67afedc53c1ad", size = 6427113, upload-time = "2026-08-11T15:04:39.424Z" }, + { url = "https://files.pythonhosted.org/packages/e7/5a/760f8b9e116edd43c57e33844dc17619158fbdd311250d4209910192d72d/pyobjc_core-12.2.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:3772b406edb3ff78171530a17cda1c4a7817f87b87ded0d8715b3fa664df16db", size = 6666817, upload-time = "2026-08-11T19:30:17.01Z" }, + { url = "https://files.pythonhosted.org/packages/13/37/486d38a173b0b8dce973a3e13c74cf402ed1b8621586b5963bc9efd49a48/pyobjc_core-12.2.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:2062e8ad30a310441cd022544a897553408bebeaa7820d5edba3c96fd7fd693b", size = 6421184, upload-time = "2026-08-11T19:30:21.081Z" }, + { url = "https://files.pythonhosted.org/packages/04/f1/d138fd9b9a66ea8db56a8138b77d3413b85da3defe13363a19f364f85529/pyobjc_core-12.2.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:2c7ef3d2f865b4b3ebb14ec3556f7a3e8abb6d130c67275cd9daa08dbd6e4e4e", size = 6671824, upload-time = "2026-08-11T19:30:25.005Z" }, + { url = "https://files.pythonhosted.org/packages/d5/85/577e2265cccf59daf48c460f0a8deeaf7dbe2991227a8859ab1eeab4945e/pyobjc_core-12.2.2-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:89acc6bc13aaa6e3f52b0ce652ede7e201edb6bf062741b246b0c5a44582f25f", size = 6477694, upload-time = "2026-08-11T19:30:28.821Z" }, + { url = "https://files.pythonhosted.org/packages/77/0a/bd9f830c64c6f334530831e75c01bfe0a770a3fbb00fddc70329223118b3/pyobjc_core-12.2.2-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:59a77038ebe0ab1240f61c341e7fb67b8674f2b4cd41bc71a6472511a12b50f7", size = 6712233, upload-time = "2026-08-11T19:30:33.032Z" }, +] + +[[package]] +name = "pyobjc-framework-cocoa" +version = "12.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/75/76/49c6da2c6a831020b4854ba20079d5a1030474bffc776b7b73c2eeff8c15/pyobjc_framework_cocoa-12.2.2.tar.gz", hash = "sha256:c96c0ef69a71afbbb0e6a7d594b455c5fe47d62e0db376ee7a2b4b828c16ace9", size = 3132831, upload-time = "2026-08-11T19:44:02.288Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/dd/aba439652cae293a736680ef5ed5cc29419adbbac1c6d4555b910741d516/pyobjc_framework_cocoa-12.2.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5a751c8033a3b51f7996f0327e0675eb44dcfdfe7920fae01e3d78b662723fff", size = 387296, upload-time = "2026-08-11T19:32:40.684Z" }, + { url = "https://files.pythonhosted.org/packages/f6/a7/370f12143661dff66f2c68a735938afab6530aa3b153f6a7a6f12b5eabab/pyobjc_framework_cocoa-12.2.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:851dca4c16e70b405e5cd5a8c166cf7c445ae54a4cdd95ce9a523803172f32d1", size = 387300, upload-time = "2026-08-11T19:32:42.043Z" }, + { url = "https://files.pythonhosted.org/packages/fd/2f/b67e73d8bc367e03fe7861cd9c49fff9dcfa6db83bc0630c0adcfb25b7fa/pyobjc_framework_cocoa-12.2.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e106f395531e67694376b0f1184612cbeea3ec8b9bf56b55ef41d026171d2a2d", size = 388117, upload-time = "2026-08-11T19:32:43.161Z" }, + { url = "https://files.pythonhosted.org/packages/db/e1/5d9b04ebb60042b9cb49adc2d33115e2f2c2e4ff7d548017bfaff8b7f536/pyobjc_framework_cocoa-12.2.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:600b1723184ca094931330e79355274949965460e23de38628d601b5a967baf9", size = 388181, upload-time = "2026-08-11T19:32:44.537Z" }, + { url = "https://files.pythonhosted.org/packages/b4/25/2a343357d5fe09bbe9c0e294dc03450866a0d6c1792fad36b6bcc00174c0/pyobjc_framework_cocoa-12.2.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:875f2aad73963faa81a6b36ae674fd494a4658d6d999e1075e0e2aca3d2391df", size = 392275, upload-time = "2026-08-11T19:32:45.631Z" }, + { url = "https://files.pythonhosted.org/packages/1f/1a/b99521999b9f54b89aad928ddff0faad507abfe33bc46599454bfa48a4b2/pyobjc_framework_cocoa-12.2.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:889d7bbd4ba2d4941078bfbbfb882138e51dbead27df006abfe0f2e0d49b5b2e", size = 388368, upload-time = "2026-08-11T19:32:46.781Z" }, + { url = "https://files.pythonhosted.org/packages/6d/26/0c697dbc73dcc76bc0f68ea5aeed25bf7b05217df5102659e878501b2d5f/pyobjc_framework_cocoa-12.2.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:de69c5933750f3a4599ed962eccd92b6a71914c7e4318dacc7895738a8ae60d7", size = 392404, upload-time = "2026-08-11T19:32:47.918Z" }, + { url = "https://files.pythonhosted.org/packages/df/82/502f740fd8f4e9ef741c9d40ba67467ab2c8196f2c09dcba12936d28a4fd/pyobjc_framework_cocoa-12.2.2-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:0e8ace0d44a00d281281a723d17fcd05eea7544a38a6a512e1fd018ddb7aece2", size = 388585, upload-time = "2026-08-11T19:32:49.171Z" }, + { url = "https://files.pythonhosted.org/packages/7d/3b/07ce3c0ab8d1e9e1bed74fea1bf1cce73527a365a7a23c755051d3be9865/pyobjc_framework_cocoa-12.2.2-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:8fe5b2e79c9530f667b4e58a87a3a15ea62f86a5d19eec405517ecbd4f454868", size = 392693, upload-time = "2026-08-11T19:32:50.283Z" }, +] + +[[package]] +name = "pyparsing" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "requests" +version = "2.34.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, +] + +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + +[[package]] +name = "scikit-learn" +version = "1.7.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +dependencies = [ + { name = "joblib" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" } }, + { name = "threadpoolctl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/c2/a7855e41c9d285dfe86dc50b250978105dce513d6e459ea66a6aeb0e1e0c/scikit_learn-1.7.2.tar.gz", hash = "sha256:20e9e49ecd130598f1ca38a1d85090e1a600147b9c02fa6f15d69cb53d968fda", size = 7193136, upload-time = "2025-09-09T08:21:29.075Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/3e/daed796fd69cce768b8788401cc464ea90b306fb196ae1ffed0b98182859/scikit_learn-1.7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b33579c10a3081d076ab403df4a4190da4f4432d443521674637677dc91e61f", size = 9336221, upload-time = "2025-09-09T08:20:19.328Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ce/af9d99533b24c55ff4e18d9b7b4d9919bbc6cd8f22fe7a7be01519a347d5/scikit_learn-1.7.2-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:36749fb62b3d961b1ce4fedf08fa57a1986cd409eff2d783bca5d4b9b5fce51c", size = 8653834, upload-time = "2025-09-09T08:20:22.073Z" }, + { url = "https://files.pythonhosted.org/packages/58/0e/8c2a03d518fb6bd0b6b0d4b114c63d5f1db01ff0f9925d8eb10960d01c01/scikit_learn-1.7.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7a58814265dfc52b3295b1900cfb5701589d30a8bb026c7540f1e9d3499d5ec8", size = 9660938, upload-time = "2025-09-09T08:20:24.327Z" }, + { url = "https://files.pythonhosted.org/packages/2b/75/4311605069b5d220e7cf5adabb38535bd96f0079313cdbb04b291479b22a/scikit_learn-1.7.2-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a847fea807e278f821a0406ca01e387f97653e284ecbd9750e3ee7c90347f18", size = 9477818, upload-time = "2025-09-09T08:20:26.845Z" }, + { url = "https://files.pythonhosted.org/packages/7f/9b/87961813c34adbca21a6b3f6b2bea344c43b30217a6d24cc437c6147f3e8/scikit_learn-1.7.2-cp310-cp310-win_amd64.whl", hash = "sha256:ca250e6836d10e6f402436d6463d6c0e4d8e0234cfb6a9a47835bd392b852ce5", size = 8886969, upload-time = "2025-09-09T08:20:29.329Z" }, + { url = "https://files.pythonhosted.org/packages/43/83/564e141eef908a5863a54da8ca342a137f45a0bfb71d1d79704c9894c9d1/scikit_learn-1.7.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7509693451651cd7361d30ce4e86a1347493554f172b1c72a39300fa2aea79e", size = 9331967, upload-time = "2025-09-09T08:20:32.421Z" }, + { url = "https://files.pythonhosted.org/packages/18/d6/ba863a4171ac9d7314c4d3fc251f015704a2caeee41ced89f321c049ed83/scikit_learn-1.7.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:0486c8f827c2e7b64837c731c8feff72c0bd2b998067a8a9cbc10643c31f0fe1", size = 8648645, upload-time = "2025-09-09T08:20:34.436Z" }, + { url = "https://files.pythonhosted.org/packages/ef/0e/97dbca66347b8cf0ea8b529e6bb9367e337ba2e8be0ef5c1a545232abfde/scikit_learn-1.7.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:89877e19a80c7b11a2891a27c21c4894fb18e2c2e077815bcade10d34287b20d", size = 9715424, upload-time = "2025-09-09T08:20:36.776Z" }, + { url = "https://files.pythonhosted.org/packages/f7/32/1f3b22e3207e1d2c883a7e09abb956362e7d1bd2f14458c7de258a26ac15/scikit_learn-1.7.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8da8bf89d4d79aaec192d2bda62f9b56ae4e5b4ef93b6a56b5de4977e375c1f1", size = 9509234, upload-time = "2025-09-09T08:20:38.957Z" }, + { url = "https://files.pythonhosted.org/packages/9f/71/34ddbd21f1da67c7a768146968b4d0220ee6831e4bcbad3e03dd3eae88b6/scikit_learn-1.7.2-cp311-cp311-win_amd64.whl", hash = "sha256:9b7ed8d58725030568523e937c43e56bc01cadb478fc43c042a9aca1dacb3ba1", size = 8894244, upload-time = "2025-09-09T08:20:41.166Z" }, + { url = "https://files.pythonhosted.org/packages/a7/aa/3996e2196075689afb9fce0410ebdb4a09099d7964d061d7213700204409/scikit_learn-1.7.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8d91a97fa2b706943822398ab943cde71858a50245e31bc71dba62aab1d60a96", size = 9259818, upload-time = "2025-09-09T08:20:43.19Z" }, + { url = "https://files.pythonhosted.org/packages/43/5d/779320063e88af9c4a7c2cf463ff11c21ac9c8bd730c4a294b0000b666c9/scikit_learn-1.7.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:acbc0f5fd2edd3432a22c69bed78e837c70cf896cd7993d71d51ba6708507476", size = 8636997, upload-time = "2025-09-09T08:20:45.468Z" }, + { url = "https://files.pythonhosted.org/packages/5c/d0/0c577d9325b05594fdd33aa970bf53fb673f051a45496842caee13cfd7fe/scikit_learn-1.7.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e5bf3d930aee75a65478df91ac1225ff89cd28e9ac7bd1196853a9229b6adb0b", size = 9478381, upload-time = "2025-09-09T08:20:47.982Z" }, + { url = "https://files.pythonhosted.org/packages/82/70/8bf44b933837ba8494ca0fc9a9ab60f1c13b062ad0197f60a56e2fc4c43e/scikit_learn-1.7.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4d6e9deed1a47aca9fe2f267ab8e8fe82ee20b4526b2c0cd9e135cea10feb44", size = 9300296, upload-time = "2025-09-09T08:20:50.366Z" }, + { url = "https://files.pythonhosted.org/packages/c6/99/ed35197a158f1fdc2fe7c3680e9c70d0128f662e1fee4ed495f4b5e13db0/scikit_learn-1.7.2-cp312-cp312-win_amd64.whl", hash = "sha256:6088aa475f0785e01bcf8529f55280a3d7d298679f50c0bb70a2364a82d0b290", size = 8731256, upload-time = "2025-09-09T08:20:52.627Z" }, + { url = "https://files.pythonhosted.org/packages/ae/93/a3038cb0293037fd335f77f31fe053b89c72f17b1c8908c576c29d953e84/scikit_learn-1.7.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0b7dacaa05e5d76759fb071558a8b5130f4845166d88654a0f9bdf3eb57851b7", size = 9212382, upload-time = "2025-09-09T08:20:54.731Z" }, + { url = "https://files.pythonhosted.org/packages/40/dd/9a88879b0c1104259136146e4742026b52df8540c39fec21a6383f8292c7/scikit_learn-1.7.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:abebbd61ad9e1deed54cca45caea8ad5f79e1b93173dece40bb8e0c658dbe6fe", size = 8592042, upload-time = "2025-09-09T08:20:57.313Z" }, + { url = "https://files.pythonhosted.org/packages/46/af/c5e286471b7d10871b811b72ae794ac5fe2989c0a2df07f0ec723030f5f5/scikit_learn-1.7.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:502c18e39849c0ea1a5d681af1dbcf15f6cce601aebb657aabbfe84133c1907f", size = 9434180, upload-time = "2025-09-09T08:20:59.671Z" }, + { url = "https://files.pythonhosted.org/packages/f1/fd/df59faa53312d585023b2da27e866524ffb8faf87a68516c23896c718320/scikit_learn-1.7.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a4c328a71785382fe3fe676a9ecf2c86189249beff90bf85e22bdb7efaf9ae0", size = 9283660, upload-time = "2025-09-09T08:21:01.71Z" }, + { url = "https://files.pythonhosted.org/packages/a7/c7/03000262759d7b6f38c836ff9d512f438a70d8a8ddae68ee80de72dcfb63/scikit_learn-1.7.2-cp313-cp313-win_amd64.whl", hash = "sha256:63a9afd6f7b229aad94618c01c252ce9e6fa97918c5ca19c9a17a087d819440c", size = 8702057, upload-time = "2025-09-09T08:21:04.234Z" }, + { url = "https://files.pythonhosted.org/packages/55/87/ef5eb1f267084532c8e4aef98a28b6ffe7425acbfd64b5e2f2e066bc29b3/scikit_learn-1.7.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:9acb6c5e867447b4e1390930e3944a005e2cb115922e693c08a323421a6966e8", size = 9558731, upload-time = "2025-09-09T08:21:06.381Z" }, + { url = "https://files.pythonhosted.org/packages/93/f8/6c1e3fc14b10118068d7938878a9f3f4e6d7b74a8ddb1e5bed65159ccda8/scikit_learn-1.7.2-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:2a41e2a0ef45063e654152ec9d8bcfc39f7afce35b08902bfe290c2498a67a6a", size = 9038852, upload-time = "2025-09-09T08:21:08.628Z" }, + { url = "https://files.pythonhosted.org/packages/83/87/066cafc896ee540c34becf95d30375fe5cbe93c3b75a0ee9aa852cd60021/scikit_learn-1.7.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98335fb98509b73385b3ab2bd0639b1f610541d3988ee675c670371d6a87aa7c", size = 9527094, upload-time = "2025-09-09T08:21:11.486Z" }, + { url = "https://files.pythonhosted.org/packages/9c/2b/4903e1ccafa1f6453b1ab78413938c8800633988c838aa0be386cbb33072/scikit_learn-1.7.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:191e5550980d45449126e23ed1d5e9e24b2c68329ee1f691a3987476e115e09c", size = 9367436, upload-time = "2025-09-09T08:21:13.602Z" }, + { url = "https://files.pythonhosted.org/packages/b5/aa/8444be3cfb10451617ff9d177b3c190288f4563e6c50ff02728be67ad094/scikit_learn-1.7.2-cp313-cp313t-win_amd64.whl", hash = "sha256:57dc4deb1d3762c75d685507fbd0bc17160144b2f2ba4ccea5dc285ab0d0e973", size = 9275749, upload-time = "2025-09-09T08:21:15.96Z" }, + { url = "https://files.pythonhosted.org/packages/d9/82/dee5acf66837852e8e68df6d8d3a6cb22d3df997b733b032f513d95205b7/scikit_learn-1.7.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fa8f63940e29c82d1e67a45d5297bdebbcb585f5a5a50c4914cc2e852ab77f33", size = 9208906, upload-time = "2025-09-09T08:21:18.557Z" }, + { url = "https://files.pythonhosted.org/packages/3c/30/9029e54e17b87cb7d50d51a5926429c683d5b4c1732f0507a6c3bed9bf65/scikit_learn-1.7.2-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:f95dc55b7902b91331fa4e5845dd5bde0580c9cd9612b1b2791b7e80c3d32615", size = 8627836, upload-time = "2025-09-09T08:21:20.695Z" }, + { url = "https://files.pythonhosted.org/packages/60/18/4a52c635c71b536879f4b971c2cedf32c35ee78f48367885ed8025d1f7ee/scikit_learn-1.7.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9656e4a53e54578ad10a434dc1f993330568cfee176dff07112b8785fb413106", size = 9426236, upload-time = "2025-09-09T08:21:22.645Z" }, + { url = "https://files.pythonhosted.org/packages/99/7e/290362f6ab582128c53445458a5befd471ed1ea37953d5bcf80604619250/scikit_learn-1.7.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96dc05a854add0e50d3f47a1ef21a10a595016da5b007c7d9cd9d0bffd1fcc61", size = 9312593, upload-time = "2025-09-09T08:21:24.65Z" }, + { url = "https://files.pythonhosted.org/packages/8e/87/24f541b6d62b1794939ae6422f8023703bbf6900378b2b34e0b4384dfefd/scikit_learn-1.7.2-cp314-cp314-win_amd64.whl", hash = "sha256:bb24510ed3f9f61476181e4db51ce801e2ba37541def12dc9333b946fc7a9cf8", size = 8820007, upload-time = "2025-09-09T08:21:26.713Z" }, +] + +[[package]] +name = "scikit-learn" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13'", + "python_full_version == '3.12.*'", + "python_full_version == '3.11.*'", +] +dependencies = [ + { name = "joblib" }, + { name = "narwhals" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "2.5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "scipy", version = "1.18.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "threadpoolctl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fa/6f/37092bdb25f712817231799fc5674d8e704066a8a70c1d2d40517e18b4ab/scikit_learn-1.9.0.tar.gz", hash = "sha256:8833266989d3a5110178a9fae30783675460724d0e1efb13b14901d2c660c557", size = 7750767, upload-time = "2026-06-02T11:54:32.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/be/e844fd9586e66540a15b71924d17a6cbc1bb749e81ddd0a796bcdba4c055/scikit_learn-1.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9db6f4d34e68c8899e4cab27fdf8eafe6ed21f2ba52ceb25ea250cd237f8e47b", size = 8789686, upload-time = "2026-06-02T11:53:05.439Z" }, + { url = "https://files.pythonhosted.org/packages/42/e2/ff880f62677a17d035817d543cb0fc8727d01eccbee81c5f7fc733a9d856/scikit_learn-1.9.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:f401448645a3e7bc115aa3c094097865155b34bff1cba8101857d9104e99074c", size = 8256782, upload-time = "2026-06-02T11:53:08.904Z" }, + { url = "https://files.pythonhosted.org/packages/25/64/eb40435e1a508ab1b4e284ce43ae80f6a162e5be5e38ed5a6fab467a9ea4/scikit_learn-1.9.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fd3a8ef0c758555a3b23c03adaa858af32f7736785ded50ad5991f59c4ed03fa", size = 8992419, upload-time = "2026-06-02T11:53:11.551Z" }, + { url = "https://files.pythonhosted.org/packages/8d/da/4810a28e473185429e45a57eebcc91fc991b33d889cc0676063e671db03d/scikit_learn-1.9.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7e254636164090da847715a27f8e5478feb98c40a9e0ee90cbd277de9e5ceb8", size = 9281411, upload-time = "2026-06-02T11:53:15.063Z" }, + { url = "https://files.pythonhosted.org/packages/3b/67/be3d369f40d8178ba3bd86635d132e08cb5329b023e4669d9426d84bc007/scikit_learn-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:5dc1818c77575d149e25fce9ef82dd7b7263ae372f03494158668ad632a69759", size = 8272736, upload-time = "2026-06-02T11:53:18.108Z" }, + { url = "https://files.pythonhosted.org/packages/37/79/a733f02dc2118da7e77a134b34f39f40201a353311b011d20859d2db3556/scikit_learn-1.9.0-cp311-cp311-win_arm64.whl", hash = "sha256:366652351f092b219c248f1e72821e841960a63d8f358f1dcfd54dc1cbdbbc28", size = 7919564, upload-time = "2026-06-02T11:53:21.2Z" }, + { url = "https://files.pythonhosted.org/packages/ac/20/75f915ff375d6249e6550ac740fdbbd66159a068fd3af1400ff62036b07a/scikit_learn-1.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2bd41b0d201bc81575531b96b713d3eb5e5f50fb0b82101ff0f92294fdc236ac", size = 8741122, upload-time = "2026-06-02T11:53:24.08Z" }, + { url = "https://files.pythonhosted.org/packages/cc/d5/2b5148f2279196775e1db2aeb85d14b70ac80e7e32b3b28e7ebeafb0901d/scikit_learn-1.9.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:5be45aa4a42a68a533913a6ed736cf309de2226411c79ef8d609a5456f1939b1", size = 8261512, upload-time = "2026-06-02T11:53:27.183Z" }, + { url = "https://files.pythonhosted.org/packages/a0/ee/5adbc77656b71f9456a2f5a7a9fdb4bcf9207a6b962889f1c2f9323afa4e/scikit_learn-1.9.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e50ed4da51974e86e940690e9a3d82e729b62b5a49f7c9bac534d515d39d86f", size = 8837603, upload-time = "2026-06-02T11:53:30.328Z" }, + { url = "https://files.pythonhosted.org/packages/6c/c2/63fdda36c56437eeb44aaf9493c8bcd62ce230ab1598924fc626ffbfa943/scikit_learn-1.9.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:056c92bb67ad4c28463c2f2653d9701449201e7e7a9e94e321be0f71c4fef2b8", size = 9132097, upload-time = "2026-06-02T11:53:33.456Z" }, + { url = "https://files.pythonhosted.org/packages/83/a4/c8e67227c680e2259c8864ae72ff48b06e16a6f51253a22167aa02a8aa4e/scikit_learn-1.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:4306775fad04cc4b472a1b15af1ae9cede1540fbfcc17fbce3767cd8dc7ae283", size = 8211173, upload-time = "2026-06-02T11:53:36.602Z" }, + { url = "https://files.pythonhosted.org/packages/cf/fd/3c0863792e98e67e9184aa4029288a175935eb65443afcd30d4f143450cf/scikit_learn-1.9.0-cp312-cp312-win_arm64.whl", hash = "sha256:26e22435f63bcdcf396b574273f29f13dd531f5ea035801f5be10ba1540a4e60", size = 7867451, upload-time = "2026-06-02T11:53:39.075Z" }, + { url = "https://files.pythonhosted.org/packages/3c/01/cf3310626b6d48d3e9be69a1223f9180360b5e6edb045f50fade723ce494/scikit_learn-1.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:80746d63bd4b6eaca54d36fe5feaf4d28bb38dc6f9470f81c7cad7c40155f119", size = 8705188, upload-time = "2026-06-02T11:53:41.964Z" }, + { url = "https://files.pythonhosted.org/packages/3e/04/5acd7ae280c5f93b6ac5ef6cdec14eef4c8d1cd91d85b3292989c94d96b1/scikit_learn-1.9.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:5b934c45c252844a91d69fda3a34cff5e7307e1db10d77cb10a3980312c74713", size = 8228299, upload-time = "2026-06-02T11:53:44.817Z" }, + { url = "https://files.pythonhosted.org/packages/0c/39/ffe829a5b8ecb40a518724a997794657fdc354ada5e8fe8e64d998c0bac9/scikit_learn-1.9.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:38c3dcb9a1ffb85505ec53d54c7b4aea0cff70050425a7760c2af661ac85df05", size = 8789690, upload-time = "2026-06-02T11:53:47.461Z" }, + { url = "https://files.pythonhosted.org/packages/1f/88/8dab5de10c638c083772a6be83a3d8106ced492f74a928c8693638e5bb50/scikit_learn-1.9.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da76d09304a4706db7cc1e3ebaa3b6b98a67365cc11d2996c4f1e58ba47df714", size = 9087723, upload-time = "2026-06-02T11:53:50.702Z" }, + { url = "https://files.pythonhosted.org/packages/20/3f/7917ca72464038f6240ec70c29f94862d08a34a74291ae4d4ec5eb8186a0/scikit_learn-1.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:5808d98f15c6bf6d9d96d2348c1997392a5888ce7097e664105f930c4bca1277", size = 8184330, upload-time = "2026-06-02T11:53:53.396Z" }, + { url = "https://files.pythonhosted.org/packages/78/c7/15739eb2f61fda3c54639e9942414e5a19ad8a8d1f5a3266afad7cb7df80/scikit_learn-1.9.0-cp313-cp313-win_arm64.whl", hash = "sha256:d77f54c017633791bc0225a43e2f8d03745fdcfe4880268fcc4df15f505dec2e", size = 7840653, upload-time = "2026-06-02T11:53:56.035Z" }, + { url = "https://files.pythonhosted.org/packages/f4/7d/c9a35cf59b20a86fec24d306f1547b78dec194b08d367ce2a3e4854169d9/scikit_learn-1.9.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9656acd4e93f74e0b66c8a36c88830a99252dfa900044d36bc2212ae89a47162", size = 8713289, upload-time = "2026-06-02T11:53:58.788Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a7/552a7821597c632b907f7bfe8f36f9f572777af8ef8a48353041cf8e091a/scikit_learn-1.9.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:24360002ae845e7866522b0a5bbf690802e7bc388cac8663502e78aa98598aa2", size = 8245141, upload-time = "2026-06-02T11:54:01.694Z" }, + { url = "https://files.pythonhosted.org/packages/7d/79/f4a0c4fe9711154cddabf913471153af79056382ddc612cfe5ee0ff4b72e/scikit_learn-1.9.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5162ad10a418c8a282dde04c9aa06965de3e9a65f33c1440c0ae69bb1a09d913", size = 8847671, upload-time = "2026-06-02T11:54:04.448Z" }, + { url = "https://files.pythonhosted.org/packages/f0/af/4d72d9e475ac83719160c662619e4bf7b95c19507cd582e7d0167a3c3dae/scikit_learn-1.9.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fea2cc5677ab49d6f5bade978c866da44957b712d92e9635e8b4f723013c3cb", size = 9118104, upload-time = "2026-06-02T11:54:07.205Z" }, + { url = "https://files.pythonhosted.org/packages/a2/d5/6a58eea2cb9abbb9b3f2bb8b2cfb3243d1152d69f442d256c7af71304769/scikit_learn-1.9.0-cp314-cp314-win_amd64.whl", hash = "sha256:64fa347efc1c839c487433e40c5144d38c336e8a2b59c81aa8660373945c2673", size = 8290674, upload-time = "2026-06-02T11:54:10.087Z" }, + { url = "https://files.pythonhosted.org/packages/65/5b/d4c879cf358f1187141cf90ced473f087183489090244f50c124a2ee478b/scikit_learn-1.9.0-cp314-cp314-win_arm64.whl", hash = "sha256:1b944b6db288f6b926e3650026ddafb988929de95d11fc2cc5fa117773c9ba42", size = 7978807, upload-time = "2026-06-02T11:54:12.769Z" }, + { url = "https://files.pythonhosted.org/packages/8a/43/bfae3121ec67ae09150d453c442c7c1cc166e9aefe056e6ab3b7728a5cfc/scikit_learn-1.9.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:4ccacf04ca5f4b492158a5f28afe0ace43f81b2571e4b9a66d34848b46128949", size = 9031941, upload-time = "2026-06-02T11:54:15.436Z" }, + { url = "https://files.pythonhosted.org/packages/75/b0/20a4546eb17f3b25d3c66df15810411c14ed5065bcfab50b53c96fb627b2/scikit_learn-1.9.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:ee1a8db2c18c08e34c7412d4b10be1cac214cd4ea7dc9715a6a327eb49a37c96", size = 8613528, upload-time = "2026-06-02T11:54:18.842Z" }, + { url = "https://files.pythonhosted.org/packages/18/3c/e440e039bb82cd19004edaaad00acbde0fb9b461083c3ecf37941c557312/scikit_learn-1.9.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:147e9329ef0e39f75d4cffa02b2aa48d827832684926cd5210d9a2cb5c57246b", size = 8855050, upload-time = "2026-06-02T11:54:21.699Z" }, + { url = "https://files.pythonhosted.org/packages/43/26/b341b8dab5998da6270a3a42c2152c578501354d36f944b5856757035ef8/scikit_learn-1.9.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bad8f8b9950321b54c965fdcbac6c6c55e79e16646b49977bcf3668d3870a1a", size = 9097190, upload-time = "2026-06-02T11:54:24.454Z" }, + { url = "https://files.pythonhosted.org/packages/fb/de/b650b4d69b84468cfa2e28a3ff7b8103743029e6446ce1a97fe060ef688c/scikit_learn-1.9.0-cp314-cp314t-win_amd64.whl", hash = "sha256:78fc56eafd4edb9575d2d8950d1dd152061abb573341a1cb7e099fc40f6c6666", size = 8963204, upload-time = "2026-06-02T11:54:27.428Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f3/ff83d76d7418112e5a61326443cdda87be3545dd8d6599c95b2481a4419e/scikit_learn-1.9.0-cp314-cp314t-win_arm64.whl", hash = "sha256:051075bda8b7aab87b1906ab3d4740a1e1224a19d7b3781a576736edc94e76aa", size = 8222661, upload-time = "2026-06-02T11:54:30.192Z" }, +] + +[[package]] +name = "scipy" +version = "1.15.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/2f/4966032c5f8cc7e6a60f1b2e0ad686293b9474b65246b0c642e3ef3badd0/scipy-1.15.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:a345928c86d535060c9c2b25e71e87c39ab2f22fc96e9636bd74d1dbf9de448c", size = 38702770, upload-time = "2025-05-08T16:04:20.849Z" }, + { url = "https://files.pythonhosted.org/packages/a0/6e/0c3bf90fae0e910c274db43304ebe25a6b391327f3f10b5dcc638c090795/scipy-1.15.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:ad3432cb0f9ed87477a8d97f03b763fd1d57709f1bbde3c9369b1dff5503b253", size = 30094511, upload-time = "2025-05-08T16:04:27.103Z" }, + { url = "https://files.pythonhosted.org/packages/ea/b1/4deb37252311c1acff7f101f6453f0440794f51b6eacb1aad4459a134081/scipy-1.15.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:aef683a9ae6eb00728a542b796f52a5477b78252edede72b8327a886ab63293f", size = 22368151, upload-time = "2025-05-08T16:04:31.731Z" }, + { url = "https://files.pythonhosted.org/packages/38/7d/f457626e3cd3c29b3a49ca115a304cebb8cc6f31b04678f03b216899d3c6/scipy-1.15.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:1c832e1bd78dea67d5c16f786681b28dd695a8cb1fb90af2e27580d3d0967e92", size = 25121732, upload-time = "2025-05-08T16:04:36.596Z" }, + { url = "https://files.pythonhosted.org/packages/db/0a/92b1de4a7adc7a15dcf5bddc6e191f6f29ee663b30511ce20467ef9b82e4/scipy-1.15.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:263961f658ce2165bbd7b99fa5135195c3a12d9bef045345016b8b50c315cb82", size = 35547617, upload-time = "2025-05-08T16:04:43.546Z" }, + { url = "https://files.pythonhosted.org/packages/8e/6d/41991e503e51fc1134502694c5fa7a1671501a17ffa12716a4a9151af3df/scipy-1.15.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2abc762b0811e09a0d3258abee2d98e0c703eee49464ce0069590846f31d40", size = 37662964, upload-time = "2025-05-08T16:04:49.431Z" }, + { url = "https://files.pythonhosted.org/packages/25/e1/3df8f83cb15f3500478c889be8fb18700813b95e9e087328230b98d547ff/scipy-1.15.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ed7284b21a7a0c8f1b6e5977ac05396c0d008b89e05498c8b7e8f4a1423bba0e", size = 37238749, upload-time = "2025-05-08T16:04:55.215Z" }, + { url = "https://files.pythonhosted.org/packages/93/3e/b3257cf446f2a3533ed7809757039016b74cd6f38271de91682aa844cfc5/scipy-1.15.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5380741e53df2c566f4d234b100a484b420af85deb39ea35a1cc1be84ff53a5c", size = 40022383, upload-time = "2025-05-08T16:05:01.914Z" }, + { url = "https://files.pythonhosted.org/packages/d1/84/55bc4881973d3f79b479a5a2e2df61c8c9a04fcb986a213ac9c02cfb659b/scipy-1.15.3-cp310-cp310-win_amd64.whl", hash = "sha256:9d61e97b186a57350f6d6fd72640f9e99d5a4a2b8fbf4b9ee9a841eab327dc13", size = 41259201, upload-time = "2025-05-08T16:05:08.166Z" }, + { url = "https://files.pythonhosted.org/packages/96/ab/5cc9f80f28f6a7dff646c5756e559823614a42b1939d86dd0ed550470210/scipy-1.15.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:993439ce220d25e3696d1b23b233dd010169b62f6456488567e830654ee37a6b", size = 38714255, upload-time = "2025-05-08T16:05:14.596Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4a/66ba30abe5ad1a3ad15bfb0b59d22174012e8056ff448cb1644deccbfed2/scipy-1.15.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:34716e281f181a02341ddeaad584205bd2fd3c242063bd3423d61ac259ca7eba", size = 30111035, upload-time = "2025-05-08T16:05:20.152Z" }, + { url = "https://files.pythonhosted.org/packages/4b/fa/a7e5b95afd80d24313307f03624acc65801846fa75599034f8ceb9e2cbf6/scipy-1.15.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3b0334816afb8b91dab859281b1b9786934392aa3d527cd847e41bb6f45bee65", size = 22384499, upload-time = "2025-05-08T16:05:24.494Z" }, + { url = "https://files.pythonhosted.org/packages/17/99/f3aaddccf3588bb4aea70ba35328c204cadd89517a1612ecfda5b2dd9d7a/scipy-1.15.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:6db907c7368e3092e24919b5e31c76998b0ce1684d51a90943cb0ed1b4ffd6c1", size = 25152602, upload-time = "2025-05-08T16:05:29.313Z" }, + { url = "https://files.pythonhosted.org/packages/56/c5/1032cdb565f146109212153339f9cb8b993701e9fe56b1c97699eee12586/scipy-1.15.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:721d6b4ef5dc82ca8968c25b111e307083d7ca9091bc38163fb89243e85e3889", size = 35503415, upload-time = "2025-05-08T16:05:34.699Z" }, + { url = "https://files.pythonhosted.org/packages/bd/37/89f19c8c05505d0601ed5650156e50eb881ae3918786c8fd7262b4ee66d3/scipy-1.15.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39cb9c62e471b1bb3750066ecc3a3f3052b37751c7c3dfd0fd7e48900ed52982", size = 37652622, upload-time = "2025-05-08T16:05:40.762Z" }, + { url = "https://files.pythonhosted.org/packages/7e/31/be59513aa9695519b18e1851bb9e487de66f2d31f835201f1b42f5d4d475/scipy-1.15.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:795c46999bae845966368a3c013e0e00947932d68e235702b5c3f6ea799aa8c9", size = 37244796, upload-time = "2025-05-08T16:05:48.119Z" }, + { url = "https://files.pythonhosted.org/packages/10/c0/4f5f3eeccc235632aab79b27a74a9130c6c35df358129f7ac8b29f562ac7/scipy-1.15.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:18aaacb735ab38b38db42cb01f6b92a2d0d4b6aabefeb07f02849e47f8fb3594", size = 40047684, upload-time = "2025-05-08T16:05:54.22Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a7/0ddaf514ce8a8714f6ed243a2b391b41dbb65251affe21ee3077ec45ea9a/scipy-1.15.3-cp311-cp311-win_amd64.whl", hash = "sha256:ae48a786a28412d744c62fd7816a4118ef97e5be0bee968ce8f0a2fba7acf3bb", size = 41246504, upload-time = "2025-05-08T16:06:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/37/4b/683aa044c4162e10ed7a7ea30527f2cbd92e6999c10a8ed8edb253836e9c/scipy-1.15.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6ac6310fdbfb7aa6612408bd2f07295bcbd3fda00d2d702178434751fe48e019", size = 38766735, upload-time = "2025-05-08T16:06:06.471Z" }, + { url = "https://files.pythonhosted.org/packages/7b/7e/f30be3d03de07f25dc0ec926d1681fed5c732d759ac8f51079708c79e680/scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:185cd3d6d05ca4b44a8f1595af87f9c372bb6acf9c808e99aa3e9aa03bd98cf6", size = 30173284, upload-time = "2025-05-08T16:06:11.686Z" }, + { url = "https://files.pythonhosted.org/packages/07/9c/0ddb0d0abdabe0d181c1793db51f02cd59e4901da6f9f7848e1f96759f0d/scipy-1.15.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:05dc6abcd105e1a29f95eada46d4a3f251743cfd7d3ae8ddb4088047f24ea477", size = 22446958, upload-time = "2025-05-08T16:06:15.97Z" }, + { url = "https://files.pythonhosted.org/packages/af/43/0bce905a965f36c58ff80d8bea33f1f9351b05fad4beaad4eae34699b7a1/scipy-1.15.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:06efcba926324df1696931a57a176c80848ccd67ce6ad020c810736bfd58eb1c", size = 25242454, upload-time = "2025-05-08T16:06:20.394Z" }, + { url = "https://files.pythonhosted.org/packages/56/30/a6f08f84ee5b7b28b4c597aca4cbe545535c39fe911845a96414700b64ba/scipy-1.15.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05045d8b9bfd807ee1b9f38761993297b10b245f012b11b13b91ba8945f7e45", size = 35210199, upload-time = "2025-05-08T16:06:26.159Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1f/03f52c282437a168ee2c7c14a1a0d0781a9a4a8962d84ac05c06b4c5b555/scipy-1.15.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:271e3713e645149ea5ea3e97b57fdab61ce61333f97cfae392c28ba786f9bb49", size = 37309455, upload-time = "2025-05-08T16:06:32.778Z" }, + { url = "https://files.pythonhosted.org/packages/89/b1/fbb53137f42c4bf630b1ffdfc2151a62d1d1b903b249f030d2b1c0280af8/scipy-1.15.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6cfd56fc1a8e53f6e89ba3a7a7251f7396412d655bca2aa5611c8ec9a6784a1e", size = 36885140, upload-time = "2025-05-08T16:06:39.249Z" }, + { url = "https://files.pythonhosted.org/packages/2e/2e/025e39e339f5090df1ff266d021892694dbb7e63568edcfe43f892fa381d/scipy-1.15.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ff17c0bb1cb32952c09217d8d1eed9b53d1463e5f1dd6052c7857f83127d539", size = 39710549, upload-time = "2025-05-08T16:06:45.729Z" }, + { url = "https://files.pythonhosted.org/packages/e6/eb/3bf6ea8ab7f1503dca3a10df2e4b9c3f6b3316df07f6c0ded94b281c7101/scipy-1.15.3-cp312-cp312-win_amd64.whl", hash = "sha256:52092bc0472cfd17df49ff17e70624345efece4e1a12b23783a1ac59a1b728ed", size = 40966184, upload-time = "2025-05-08T16:06:52.623Z" }, + { url = "https://files.pythonhosted.org/packages/73/18/ec27848c9baae6e0d6573eda6e01a602e5649ee72c27c3a8aad673ebecfd/scipy-1.15.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c620736bcc334782e24d173c0fdbb7590a0a436d2fdf39310a8902505008759", size = 38728256, upload-time = "2025-05-08T16:06:58.696Z" }, + { url = "https://files.pythonhosted.org/packages/74/cd/1aef2184948728b4b6e21267d53b3339762c285a46a274ebb7863c9e4742/scipy-1.15.3-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:7e11270a000969409d37ed399585ee530b9ef6aa99d50c019de4cb01e8e54e62", size = 30109540, upload-time = "2025-05-08T16:07:04.209Z" }, + { url = "https://files.pythonhosted.org/packages/5b/d8/59e452c0a255ec352bd0a833537a3bc1bfb679944c4938ab375b0a6b3a3e/scipy-1.15.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8c9ed3ba2c8a2ce098163a9bdb26f891746d02136995df25227a20e71c396ebb", size = 22383115, upload-time = "2025-05-08T16:07:08.998Z" }, + { url = "https://files.pythonhosted.org/packages/08/f5/456f56bbbfccf696263b47095291040655e3cbaf05d063bdc7c7517f32ac/scipy-1.15.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:0bdd905264c0c9cfa74a4772cdb2070171790381a5c4d312c973382fc6eaf730", size = 25163884, upload-time = "2025-05-08T16:07:14.091Z" }, + { url = "https://files.pythonhosted.org/packages/a2/66/a9618b6a435a0f0c0b8a6d0a2efb32d4ec5a85f023c2b79d39512040355b/scipy-1.15.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79167bba085c31f38603e11a267d862957cbb3ce018d8b38f79ac043bc92d825", size = 35174018, upload-time = "2025-05-08T16:07:19.427Z" }, + { url = "https://files.pythonhosted.org/packages/b5/09/c5b6734a50ad4882432b6bb7c02baf757f5b2f256041da5df242e2d7e6b6/scipy-1.15.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9deabd6d547aee2c9a81dee6cc96c6d7e9a9b1953f74850c179f91fdc729cb7", size = 37269716, upload-time = "2025-05-08T16:07:25.712Z" }, + { url = "https://files.pythonhosted.org/packages/77/0a/eac00ff741f23bcabd352731ed9b8995a0a60ef57f5fd788d611d43d69a1/scipy-1.15.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dde4fc32993071ac0c7dd2d82569e544f0bdaff66269cb475e0f369adad13f11", size = 36872342, upload-time = "2025-05-08T16:07:31.468Z" }, + { url = "https://files.pythonhosted.org/packages/fe/54/4379be86dd74b6ad81551689107360d9a3e18f24d20767a2d5b9253a3f0a/scipy-1.15.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f77f853d584e72e874d87357ad70f44b437331507d1c311457bed8ed2b956126", size = 39670869, upload-time = "2025-05-08T16:07:38.002Z" }, + { url = "https://files.pythonhosted.org/packages/87/2e/892ad2862ba54f084ffe8cc4a22667eaf9c2bcec6d2bff1d15713c6c0703/scipy-1.15.3-cp313-cp313-win_amd64.whl", hash = "sha256:b90ab29d0c37ec9bf55424c064312930ca5f4bde15ee8619ee44e69319aab163", size = 40988851, upload-time = "2025-05-08T16:08:33.671Z" }, + { url = "https://files.pythonhosted.org/packages/1b/e9/7a879c137f7e55b30d75d90ce3eb468197646bc7b443ac036ae3fe109055/scipy-1.15.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3ac07623267feb3ae308487c260ac684b32ea35fd81e12845039952f558047b8", size = 38863011, upload-time = "2025-05-08T16:07:44.039Z" }, + { url = "https://files.pythonhosted.org/packages/51/d1/226a806bbd69f62ce5ef5f3ffadc35286e9fbc802f606a07eb83bf2359de/scipy-1.15.3-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:6487aa99c2a3d509a5227d9a5e889ff05830a06b2ce08ec30df6d79db5fcd5c5", size = 30266407, upload-time = "2025-05-08T16:07:49.891Z" }, + { url = "https://files.pythonhosted.org/packages/e5/9b/f32d1d6093ab9eeabbd839b0f7619c62e46cc4b7b6dbf05b6e615bbd4400/scipy-1.15.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:50f9e62461c95d933d5c5ef4a1f2ebf9a2b4e83b0db374cb3f1de104d935922e", size = 22540030, upload-time = "2025-05-08T16:07:54.121Z" }, + { url = "https://files.pythonhosted.org/packages/e7/29/c278f699b095c1a884f29fda126340fcc201461ee8bfea5c8bdb1c7c958b/scipy-1.15.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:14ed70039d182f411ffc74789a16df3835e05dc469b898233a245cdfd7f162cb", size = 25218709, upload-time = "2025-05-08T16:07:58.506Z" }, + { url = "https://files.pythonhosted.org/packages/24/18/9e5374b617aba742a990581373cd6b68a2945d65cc588482749ef2e64467/scipy-1.15.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a769105537aa07a69468a0eefcd121be52006db61cdd8cac8a0e68980bbb723", size = 34809045, upload-time = "2025-05-08T16:08:03.929Z" }, + { url = "https://files.pythonhosted.org/packages/e1/fe/9c4361e7ba2927074360856db6135ef4904d505e9b3afbbcb073c4008328/scipy-1.15.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db984639887e3dffb3928d118145ffe40eff2fa40cb241a306ec57c219ebbbb", size = 36703062, upload-time = "2025-05-08T16:08:09.558Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8e/038ccfe29d272b30086b25a4960f757f97122cb2ec42e62b460d02fe98e9/scipy-1.15.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:40e54d5c7e7ebf1aa596c374c49fa3135f04648a0caabcb66c52884b943f02b4", size = 36393132, upload-time = "2025-05-08T16:08:15.34Z" }, + { url = "https://files.pythonhosted.org/packages/10/7e/5c12285452970be5bdbe8352c619250b97ebf7917d7a9a9e96b8a8140f17/scipy-1.15.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5e721fed53187e71d0ccf382b6bf977644c533e506c4d33c3fb24de89f5c3ed5", size = 38979503, upload-time = "2025-05-08T16:08:21.513Z" }, + { url = "https://files.pythonhosted.org/packages/81/06/0a5e5349474e1cbc5757975b21bd4fad0e72ebf138c5592f191646154e06/scipy-1.15.3-cp313-cp313t-win_amd64.whl", hash = "sha256:76ad1fb5f8752eabf0fa02e4cc0336b4e8f021e2d5f061ed37d6d264db35e3ca", size = 40308097, upload-time = "2025-05-08T16:08:27.627Z" }, +] + +[[package]] +name = "scipy" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.11.*'", +] +dependencies = [ + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" } }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/75/b4ce781849931fef6fd529afa6b63711d5a733065722d0c3e2724af9e40a/scipy-1.17.1-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:1f95b894f13729334fb990162e911c9e5dc1ab390c58aa6cbecb389c5b5e28ec", size = 31613675, upload-time = "2026-02-23T00:16:00.13Z" }, + { url = "https://files.pythonhosted.org/packages/f7/58/bccc2861b305abdd1b8663d6130c0b3d7cc22e8d86663edbc8401bfd40d4/scipy-1.17.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:e18f12c6b0bc5a592ed23d3f7b891f68fd7f8241d69b7883769eb5d5dfb52696", size = 28162057, upload-time = "2026-02-23T00:16:09.456Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ee/18146b7757ed4976276b9c9819108adbc73c5aad636e5353e20746b73069/scipy-1.17.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a3472cfbca0a54177d0faa68f697d8ba4c80bbdc19908c3465556d9f7efce9ee", size = 20334032, upload-time = "2026-02-23T00:16:17.358Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e6/cef1cf3557f0c54954198554a10016b6a03b2ec9e22a4e1df734936bd99c/scipy-1.17.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:766e0dc5a616d026a3a1cffa379af959671729083882f50307e18175797b3dfd", size = 22709533, upload-time = "2026-02-23T00:16:25.791Z" }, + { url = "https://files.pythonhosted.org/packages/4d/60/8804678875fc59362b0fb759ab3ecce1f09c10a735680318ac30da8cd76b/scipy-1.17.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:744b2bf3640d907b79f3fd7874efe432d1cf171ee721243e350f55234b4cec4c", size = 33062057, upload-time = "2026-02-23T00:16:36.931Z" }, + { url = "https://files.pythonhosted.org/packages/09/7d/af933f0f6e0767995b4e2d705a0665e454d1c19402aa7e895de3951ebb04/scipy-1.17.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43af8d1f3bea642559019edfe64e9b11192a8978efbd1539d7bc2aaa23d92de4", size = 35349300, upload-time = "2026-02-23T00:16:49.108Z" }, + { url = "https://files.pythonhosted.org/packages/b4/3d/7ccbbdcbb54c8fdc20d3b6930137c782a163fa626f0aef920349873421ba/scipy-1.17.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd96a1898c0a47be4520327e01f874acfd61fb48a9420f8aa9f6483412ffa444", size = 35127333, upload-time = "2026-02-23T00:17:01.293Z" }, + { url = "https://files.pythonhosted.org/packages/e8/19/f926cb11c42b15ba08e3a71e376d816ac08614f769b4f47e06c3580c836a/scipy-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4eb6c25dd62ee8d5edf68a8e1c171dd71c292fdae95d8aeb3dd7d7de4c364082", size = 37741314, upload-time = "2026-02-23T00:17:12.576Z" }, + { url = "https://files.pythonhosted.org/packages/95/da/0d1df507cf574b3f224ccc3d45244c9a1d732c81dcb26b1e8a766ae271a8/scipy-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:d30e57c72013c2a4fe441c2fcb8e77b14e152ad48b5464858e07e2ad9fbfceff", size = 36607512, upload-time = "2026-02-23T00:17:23.424Z" }, + { url = "https://files.pythonhosted.org/packages/68/7f/bdd79ceaad24b671543ffe0ef61ed8e659440eb683b66f033454dcee90eb/scipy-1.17.1-cp311-cp311-win_arm64.whl", hash = "sha256:9ecb4efb1cd6e8c4afea0daa91a87fbddbce1b99d2895d151596716c0b2e859d", size = 24599248, upload-time = "2026-02-23T00:17:34.561Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/b992b488d6f299dbe3f11a20b24d3dda3d46f1a635ede1c46b5b17a7b163/scipy-1.17.1-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:35c3a56d2ef83efc372eaec584314bd0ef2e2f0d2adb21c55e6ad5b344c0dcb8", size = 31610954, upload-time = "2026-02-23T00:17:49.855Z" }, + { url = "https://files.pythonhosted.org/packages/b2/02/cf107b01494c19dc100f1d0b7ac3cc08666e96ba2d64db7626066cee895e/scipy-1.17.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:fcb310ddb270a06114bb64bbe53c94926b943f5b7f0842194d585c65eb4edd76", size = 28172662, upload-time = "2026-02-23T00:18:01.64Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a9/599c28631bad314d219cf9ffd40e985b24d603fc8a2f4ccc5ae8419a535b/scipy-1.17.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:cc90d2e9c7e5c7f1a482c9875007c095c3194b1cfedca3c2f3291cdc2bc7c086", size = 20344366, upload-time = "2026-02-23T00:18:12.015Z" }, + { url = "https://files.pythonhosted.org/packages/35/f5/906eda513271c8deb5af284e5ef0206d17a96239af79f9fa0aebfe0e36b4/scipy-1.17.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:c80be5ede8f3f8eded4eff73cc99a25c388ce98e555b17d31da05287015ffa5b", size = 22704017, upload-time = "2026-02-23T00:18:21.502Z" }, + { url = "https://files.pythonhosted.org/packages/da/34/16f10e3042d2f1d6b66e0428308ab52224b6a23049cb2f5c1756f713815f/scipy-1.17.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e19ebea31758fac5893a2ac360fedd00116cbb7628e650842a6691ba7ca28a21", size = 32927842, upload-time = "2026-02-23T00:18:35.367Z" }, + { url = "https://files.pythonhosted.org/packages/01/8e/1e35281b8ab6d5d72ebe9911edcdffa3f36b04ed9d51dec6dd140396e220/scipy-1.17.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02ae3b274fde71c5e92ac4d54bc06c42d80e399fec704383dcd99b301df37458", size = 35235890, upload-time = "2026-02-23T00:18:49.188Z" }, + { url = "https://files.pythonhosted.org/packages/c5/5c/9d7f4c88bea6e0d5a4f1bc0506a53a00e9fcb198de372bfe4d3652cef482/scipy-1.17.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8a604bae87c6195d8b1045eddece0514d041604b14f2727bbc2b3020172045eb", size = 35003557, upload-time = "2026-02-23T00:18:54.74Z" }, + { url = "https://files.pythonhosted.org/packages/65/94/7698add8f276dbab7a9de9fb6b0e02fc13ee61d51c7c3f85ac28b65e1239/scipy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f590cd684941912d10becc07325a3eeb77886fe981415660d9265c4c418d0bea", size = 37625856, upload-time = "2026-02-23T00:19:00.307Z" }, + { url = "https://files.pythonhosted.org/packages/a2/84/dc08d77fbf3d87d3ee27f6a0c6dcce1de5829a64f2eae85a0ecc1f0daa73/scipy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:41b71f4a3a4cab9d366cd9065b288efc4d4f3c0b37a91a8e0947fb5bd7f31d87", size = 36549682, upload-time = "2026-02-23T00:19:07.67Z" }, + { url = "https://files.pythonhosted.org/packages/bc/98/fe9ae9ffb3b54b62559f52dedaebe204b408db8109a8c66fdd04869e6424/scipy-1.17.1-cp312-cp312-win_arm64.whl", hash = "sha256:f4115102802df98b2b0db3cce5cb9b92572633a1197c77b7553e5203f284a5b3", size = 24547340, upload-time = "2026-02-23T00:19:12.024Z" }, + { url = "https://files.pythonhosted.org/packages/76/27/07ee1b57b65e92645f219b37148a7e7928b82e2b5dbeccecb4dff7c64f0b/scipy-1.17.1-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:5e3c5c011904115f88a39308379c17f91546f77c1667cea98739fe0fccea804c", size = 31590199, upload-time = "2026-02-23T00:19:17.192Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ae/db19f8ab842e9b724bf5dbb7db29302a91f1e55bc4d04b1025d6d605a2c5/scipy-1.17.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6fac755ca3d2c3edcb22f479fceaa241704111414831ddd3bc6056e18516892f", size = 28154001, upload-time = "2026-02-23T00:19:22.241Z" }, + { url = "https://files.pythonhosted.org/packages/5b/58/3ce96251560107b381cbd6e8413c483bbb1228a6b919fa8652b0d4090e7f/scipy-1.17.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:7ff200bf9d24f2e4d5dc6ee8c3ac64d739d3a89e2326ba68aaf6c4a2b838fd7d", size = 20325719, upload-time = "2026-02-23T00:19:26.329Z" }, + { url = "https://files.pythonhosted.org/packages/b2/83/15087d945e0e4d48ce2377498abf5ad171ae013232ae31d06f336e64c999/scipy-1.17.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4b400bdc6f79fa02a4d86640310dde87a21fba0c979efff5248908c6f15fad1b", size = 22683595, upload-time = "2026-02-23T00:19:30.304Z" }, + { url = "https://files.pythonhosted.org/packages/b4/e0/e58fbde4a1a594c8be8114eb4aac1a55bcd6587047efc18a61eb1f5c0d30/scipy-1.17.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b64ca7d4aee0102a97f3ba22124052b4bd2152522355073580bf4845e2550b6", size = 32896429, upload-time = "2026-02-23T00:19:35.536Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5f/f17563f28ff03c7b6799c50d01d5d856a1d55f2676f537ca8d28c7f627cd/scipy-1.17.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:581b2264fc0aa555f3f435a5944da7504ea3a065d7029ad60e7c3d1ae09c5464", size = 35203952, upload-time = "2026-02-23T00:19:42.259Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a5/9afd17de24f657fdfe4df9a3f1ea049b39aef7c06000c13db1530d81ccca/scipy-1.17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:beeda3d4ae615106d7094f7e7cef6218392e4465cc95d25f900bebabfded0950", size = 34979063, upload-time = "2026-02-23T00:19:47.547Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/88b1d2384b424bf7c924f2038c1c409f8d88bb2a8d49d097861dd64a57b2/scipy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6609bc224e9568f65064cfa72edc0f24ee6655b47575954ec6339534b2798369", size = 37598449, upload-time = "2026-02-23T00:19:53.238Z" }, + { url = "https://files.pythonhosted.org/packages/35/e5/d6d0e51fc888f692a35134336866341c08655d92614f492c6860dc45bb2c/scipy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:37425bc9175607b0268f493d79a292c39f9d001a357bebb6b88fdfaff13f6448", size = 36510943, upload-time = "2026-02-23T00:20:50.89Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fd/3be73c564e2a01e690e19cc618811540ba5354c67c8680dce3281123fb79/scipy-1.17.1-cp313-cp313-win_arm64.whl", hash = "sha256:5cf36e801231b6a2059bf354720274b7558746f3b1a4efb43fcf557ccd484a87", size = 24545621, upload-time = "2026-02-23T00:20:55.871Z" }, + { url = "https://files.pythonhosted.org/packages/6f/6b/17787db8b8114933a66f9dcc479a8272e4b4da75fe03b0c282f7b0ade8cd/scipy-1.17.1-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:d59c30000a16d8edc7e64152e30220bfbd724c9bbb08368c054e24c651314f0a", size = 31936708, upload-time = "2026-02-23T00:19:58.694Z" }, + { url = "https://files.pythonhosted.org/packages/38/2e/524405c2b6392765ab1e2b722a41d5da33dc5c7b7278184a8ad29b6cb206/scipy-1.17.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:010f4333c96c9bb1a4516269e33cb5917b08ef2166d5556ca2fd9f082a9e6ea0", size = 28570135, upload-time = "2026-02-23T00:20:03.934Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c3/5bd7199f4ea8556c0c8e39f04ccb014ac37d1468e6cfa6a95c6b3562b76e/scipy-1.17.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:2ceb2d3e01c5f1d83c4189737a42d9cb2fc38a6eeed225e7515eef71ad301dce", size = 20741977, upload-time = "2026-02-23T00:20:07.935Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b8/8ccd9b766ad14c78386599708eb745f6b44f08400a5fd0ade7cf89b6fc93/scipy-1.17.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:844e165636711ef41f80b4103ed234181646b98a53c8f05da12ca5ca289134f6", size = 23029601, upload-time = "2026-02-23T00:20:12.161Z" }, + { url = "https://files.pythonhosted.org/packages/6d/a0/3cb6f4d2fb3e17428ad2880333cac878909ad1a89f678527b5328b93c1d4/scipy-1.17.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:158dd96d2207e21c966063e1635b1063cd7787b627b6f07305315dd73d9c679e", size = 33019667, upload-time = "2026-02-23T00:20:17.208Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c3/2d834a5ac7bf3a0c806ad1508efc02dda3c8c61472a56132d7894c312dea/scipy-1.17.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74cbb80d93260fe2ffa334efa24cb8f2f0f622a9b9febf8b483c0b865bfb3475", size = 35264159, upload-time = "2026-02-23T00:20:23.087Z" }, + { url = "https://files.pythonhosted.org/packages/4d/77/d3ed4becfdbd217c52062fafe35a72388d1bd82c2d0ba5ca19d6fcc93e11/scipy-1.17.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dbc12c9f3d185f5c737d801da555fb74b3dcfa1a50b66a1a93e09190f41fab50", size = 35102771, upload-time = "2026-02-23T00:20:28.636Z" }, + { url = "https://files.pythonhosted.org/packages/bd/12/d19da97efde68ca1ee5538bb261d5d2c062f0c055575128f11a2730e3ac1/scipy-1.17.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94055a11dfebe37c656e70317e1996dc197e1a15bbcc351bcdd4610e128fe1ca", size = 37665910, upload-time = "2026-02-23T00:20:34.743Z" }, + { url = "https://files.pythonhosted.org/packages/06/1c/1172a88d507a4baaf72c5a09bb6c018fe2ae0ab622e5830b703a46cc9e44/scipy-1.17.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e30bdeaa5deed6bc27b4cc490823cd0347d7dae09119b8803ae576ea0ce52e4c", size = 36562980, upload-time = "2026-02-23T00:20:40.575Z" }, + { url = "https://files.pythonhosted.org/packages/70/b0/eb757336e5a76dfa7911f63252e3b7d1de00935d7705cf772db5b45ec238/scipy-1.17.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a720477885a9d2411f94a93d16f9d89bad0f28ca23c3f8daa521e2dcc3f44d49", size = 24856543, upload-time = "2026-02-23T00:20:45.313Z" }, + { url = "https://files.pythonhosted.org/packages/cf/83/333afb452af6f0fd70414dc04f898647ee1423979ce02efa75c3b0f2c28e/scipy-1.17.1-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:a48a72c77a310327f6a3a920092fa2b8fd03d7deaa60f093038f22d98e096717", size = 31584510, upload-time = "2026-02-23T00:21:01.015Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a6/d05a85fd51daeb2e4ea71d102f15b34fedca8e931af02594193ae4fd25f7/scipy-1.17.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:45abad819184f07240d8a696117a7aacd39787af9e0b719d00285549ed19a1e9", size = 28170131, upload-time = "2026-02-23T00:21:05.888Z" }, + { url = "https://files.pythonhosted.org/packages/db/7b/8624a203326675d7746a254083a187398090a179335b2e4a20e2ddc46e83/scipy-1.17.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3fd1fcdab3ea951b610dc4cef356d416d5802991e7e32b5254828d342f7b7e0b", size = 20342032, upload-time = "2026-02-23T00:21:09.904Z" }, + { url = "https://files.pythonhosted.org/packages/c9/35/2c342897c00775d688d8ff3987aced3426858fd89d5a0e26e020b660b301/scipy-1.17.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7bdf2da170b67fdf10bca777614b1c7d96ae3ca5794fd9587dce41eb2966e866", size = 22678766, upload-time = "2026-02-23T00:21:14.313Z" }, + { url = "https://files.pythonhosted.org/packages/ef/f2/7cdb8eb308a1a6ae1e19f945913c82c23c0c442a462a46480ce487fdc0ac/scipy-1.17.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:adb2642e060a6549c343603a3851ba76ef0b74cc8c079a9a58121c7ec9fe2350", size = 32957007, upload-time = "2026-02-23T00:21:19.663Z" }, + { url = "https://files.pythonhosted.org/packages/0b/2e/7eea398450457ecb54e18e9d10110993fa65561c4f3add5e8eccd2b9cd41/scipy-1.17.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eee2cfda04c00a857206a4330f0c5e3e56535494e30ca445eb19ec624ae75118", size = 35221333, upload-time = "2026-02-23T00:21:25.278Z" }, + { url = "https://files.pythonhosted.org/packages/d9/77/5b8509d03b77f093a0d52e606d3c4f79e8b06d1d38c441dacb1e26cacf46/scipy-1.17.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d2650c1fb97e184d12d8ba010493ee7b322864f7d3d00d3f9bb97d9c21de4068", size = 35042066, upload-time = "2026-02-23T00:21:31.358Z" }, + { url = "https://files.pythonhosted.org/packages/f9/df/18f80fb99df40b4070328d5ae5c596f2f00fffb50167e31439e932f29e7d/scipy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:08b900519463543aa604a06bec02461558a6e1cef8fdbb8098f77a48a83c8118", size = 37612763, upload-time = "2026-02-23T00:21:37.247Z" }, + { url = "https://files.pythonhosted.org/packages/4b/39/f0e8ea762a764a9dc52aa7dabcfad51a354819de1f0d4652b6a1122424d6/scipy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:3877ac408e14da24a6196de0ddcace62092bfc12a83823e92e49e40747e52c19", size = 37290984, upload-time = "2026-02-23T00:22:35.023Z" }, + { url = "https://files.pythonhosted.org/packages/7c/56/fe201e3b0f93d1a8bcf75d3379affd228a63d7e2d80ab45467a74b494947/scipy-1.17.1-cp314-cp314-win_arm64.whl", hash = "sha256:f8885db0bc2bffa59d5c1b72fad7a6a92d3e80e7257f967dd81abb553a90d293", size = 25192877, upload-time = "2026-02-23T00:22:39.798Z" }, + { url = "https://files.pythonhosted.org/packages/96/ad/f8c414e121f82e02d76f310f16db9899c4fcde36710329502a6b2a3c0392/scipy-1.17.1-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:1cc682cea2ae55524432f3cdff9e9a3be743d52a7443d0cba9017c23c87ae2f6", size = 31949750, upload-time = "2026-02-23T00:21:42.289Z" }, + { url = "https://files.pythonhosted.org/packages/7c/b0/c741e8865d61b67c81e255f4f0a832846c064e426636cd7de84e74d209be/scipy-1.17.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:2040ad4d1795a0ae89bfc7e8429677f365d45aa9fd5e4587cf1ea737f927b4a1", size = 28585858, upload-time = "2026-02-23T00:21:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1b/3985219c6177866628fa7c2595bfd23f193ceebbe472c98a08824b9466ff/scipy-1.17.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:131f5aaea57602008f9822e2115029b55d4b5f7c070287699fe45c661d051e39", size = 20757723, upload-time = "2026-02-23T00:21:52.039Z" }, + { url = "https://files.pythonhosted.org/packages/c0/19/2a04aa25050d656d6f7b9e7b685cc83d6957fb101665bfd9369ca6534563/scipy-1.17.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9cdc1a2fcfd5c52cfb3045feb399f7b3ce822abdde3a193a6b9a60b3cb5854ca", size = 23043098, upload-time = "2026-02-23T00:21:56.185Z" }, + { url = "https://files.pythonhosted.org/packages/86/f1/3383beb9b5d0dbddd030335bf8a8b32d4317185efe495374f134d8be6cce/scipy-1.17.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e3dcd57ab780c741fde8dc68619de988b966db759a3c3152e8e9142c26295ad", size = 33030397, upload-time = "2026-02-23T00:22:01.404Z" }, + { url = "https://files.pythonhosted.org/packages/41/68/8f21e8a65a5a03f25a79165ec9d2b28c00e66dc80546cf5eb803aeeff35b/scipy-1.17.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9956e4d4f4a301ebf6cde39850333a6b6110799d470dbbb1e25326ac447f52a", size = 35281163, upload-time = "2026-02-23T00:22:07.024Z" }, + { url = "https://files.pythonhosted.org/packages/84/8d/c8a5e19479554007a5632ed7529e665c315ae7492b4f946b0deb39870e39/scipy-1.17.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a4328d245944d09fd639771de275701ccadf5f781ba0ff092ad141e017eccda4", size = 35116291, upload-time = "2026-02-23T00:22:12.585Z" }, + { url = "https://files.pythonhosted.org/packages/52/52/e57eceff0e342a1f50e274264ed47497b59e6a4e3118808ee58ddda7b74a/scipy-1.17.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a77cbd07b940d326d39a1d1b37817e2ee4d79cb30e7338f3d0cddffae70fcaa2", size = 37682317, upload-time = "2026-02-23T00:22:18.513Z" }, + { url = "https://files.pythonhosted.org/packages/11/2f/b29eafe4a3fbc3d6de9662b36e028d5f039e72d345e05c250e121a230dd4/scipy-1.17.1-cp314-cp314t-win_amd64.whl", hash = "sha256:eb092099205ef62cd1782b006658db09e2fed75bffcae7cc0d44052d8aa0f484", size = 37345327, upload-time = "2026-02-23T00:22:24.442Z" }, + { url = "https://files.pythonhosted.org/packages/07/39/338d9219c4e87f3e708f18857ecd24d22a0c3094752393319553096b98af/scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21", size = 25489165, upload-time = "2026-02-23T00:22:29.563Z" }, +] + +[[package]] +name = "scipy" +version = "1.18.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13'", + "python_full_version == '3.12.*'", +] +dependencies = [ + { name = "numpy", version = "2.5.2", source = { registry = "https://pypi.org/simple" } }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7e/74/66de6258867beb2ef08f35f9f2ac017a52cacd5081714d239ff1a442d458/scipy-1.18.1.tar.gz", hash = "sha256:52c4b7422442aba924d03ad4019852b08a92e64ea187b933135687bfe2747307", size = 30781235, upload-time = "2026-08-21T23:28:50.599Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/f7/240c110c08693826b4513a52f5717d62ec7c7af72f2920821247c03b17b3/scipy-1.18.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:457fd7a2a8edeb044ab6ffbc0aa03ff6cd18491356e5e0c834d76ce621b916d1", size = 31111061, upload-time = "2026-08-21T23:23:44.522Z" }, + { url = "https://files.pythonhosted.org/packages/05/4a/78c6285577c375e7cf27277ea8ee6961224327f1e1a0c44af5f17f23635c/scipy-1.18.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:e708533e8b2ae2497d65346538a7dcc92814410b25b81432eac66de0f2af8265", size = 28733332, upload-time = "2026-08-21T23:23:50.015Z" }, + { url = "https://files.pythonhosted.org/packages/a5/f6/a5b82f8abbe14d134691b8b903696f701d25a081353a29dc655c364d9e62/scipy-1.18.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:7bbf207c4453ce1ad2e00b17313852b33310b83090c2311bdaf97f93c0380d12", size = 20475078, upload-time = "2026-08-21T23:23:54.138Z" }, + { url = "https://files.pythonhosted.org/packages/23/22/0858a0bbd6b3e825ceb8cd9baf9eaf3b2f2b1d77727eb6be40500bcdc92f/scipy-1.18.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:78c0665edead396b1abb4897c41a5c1d9bf090c8a637a4c20a61678e0a264e66", size = 23108904, upload-time = "2026-08-21T23:23:57.824Z" }, + { url = "https://files.pythonhosted.org/packages/75/9a/2e71719f31eaefe0e3a1706c4a1ded94e664bfd95ffca2b219a671faee01/scipy-1.18.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c085faa2cfa879c5141df483f836f4d691045a078224a670fa570fa01612d89", size = 34025113, upload-time = "2026-08-21T23:24:02.209Z" }, + { url = "https://files.pythonhosted.org/packages/df/64/ff35eb9e54894cf471ff4716abd3c81eb0a0626869217ce3e6ba4ccf17d7/scipy-1.18.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f55fa87b6c612ecd6b058f167c53231b1d14e412efe361d3d6e38b3631c73218", size = 35344199, upload-time = "2026-08-21T23:24:07.844Z" }, + { url = "https://files.pythonhosted.org/packages/d3/af/c5538be1792f7034c12c7db6ee67cace58253c7b87b122d68253eaf5de89/scipy-1.18.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c35d74ce0e193ff740c2f2be2ac913ddc232fe6c1ff40b26cfecb9c670c63314", size = 35639587, upload-time = "2026-08-21T23:24:13.05Z" }, + { url = "https://files.pythonhosted.org/packages/91/4c/075e4f66471bac101141ac739e9e135549be1bae584571bd03a530c056e1/scipy-1.18.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d2924a03db38dc2e848bca2fe9f077dafb891480b91a00a0963a8cf86dfc31c1", size = 37480330, upload-time = "2026-08-21T23:24:19.608Z" }, + { url = "https://files.pythonhosted.org/packages/39/e7/979fd14e75008623df31ba70d6bb144700f68feadcea042021c06a05bf82/scipy-1.18.1-cp312-cp312-win_amd64.whl", hash = "sha256:5e4d44984abc0020154ea81b247adeddcc3ac5527b975ff798bd1ba0adc513c2", size = 36658278, upload-time = "2026-08-21T23:24:25.463Z" }, + { url = "https://files.pythonhosted.org/packages/c7/0b/e1525354ff9d7d5feb6d1b31af6d14072e5c91e9607b421fa1ec889660b3/scipy-1.18.1-cp312-cp312-win_arm64.whl", hash = "sha256:d65d448389b8436493abcf629cc94ad0cf32aecaf06e1acca1de53cc795f2f12", size = 24400588, upload-time = "2026-08-21T23:24:30.579Z" }, + { url = "https://files.pythonhosted.org/packages/b6/55/4540ee0f9c42a9ad7109d0d1a8cc70de54c3572b01c6693a2b1c70e90ceb/scipy-1.18.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:3ab3523da44749156e1f68b464dc56af11ae4cbc5c739a49d05f32b982eca9f3", size = 31089958, upload-time = "2026-08-21T23:24:35.8Z" }, + { url = "https://files.pythonhosted.org/packages/2a/f5/769f36d14922b8071a43e95d24d18b6bdafad10d7f5cf647867e1ac052bc/scipy-1.18.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:e6fb6a55cc0ba97b59a1f288fb86dc6fce8bdfc0fffcbfd015e3a954bf2a2d93", size = 28715106, upload-time = "2026-08-21T23:24:40.775Z" }, + { url = "https://files.pythonhosted.org/packages/9a/d7/21d890274f75ea37a8209d5519e72da3da90302e3b9fb8397a0918386a62/scipy-1.18.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:ea324d9dd34c38bfb9bec8ca4d1b407db97dbb74029f566b8e322b1b6fe56fe6", size = 20456846, upload-time = "2026-08-21T23:24:45.066Z" }, + { url = "https://files.pythonhosted.org/packages/ec/01/798430ecea2e78ec7c02663d5f71c007bb6abeca931080debd40d7fa55ea/scipy-1.18.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:75b00eb8fb802090aa903f4ea1c7f5a584779f967361e68b7e98e531cc2d7174", size = 23087986, upload-time = "2026-08-21T23:24:49.539Z" }, + { url = "https://files.pythonhosted.org/packages/e6/5f/4634e9d35c68496e4e34cb6946eafab044458e6cedab42b40b6588e475b6/scipy-1.18.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d416b16cccfd70fbf62400e84d0bb2f4e6af519a45557f1692c749b37f14b315", size = 33998146, upload-time = "2026-08-21T23:24:54.714Z" }, + { url = "https://files.pythonhosted.org/packages/41/48/6450ed9243315322bbc19ac57b9b70d66a20bf1d38d124c96bc4bf6af9ea/scipy-1.18.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fdaf5ea890a6183d0565f51a61799d67081bd5b1cf03c5f4b3fd3732108625c9", size = 35312578, upload-time = "2026-08-21T23:25:00.44Z" }, + { url = "https://files.pythonhosted.org/packages/00/bd/bf5a4be6a3525676499f6dff307991739ff6fdcad1481b1aeb6745339f58/scipy-1.18.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c825cef2f49e46753726a7181a8e199804a912b29519ada542c6ebc654951899", size = 35612621, upload-time = "2026-08-21T23:25:06.144Z" }, + { url = "https://files.pythonhosted.org/packages/bd/4e/3c45c33e00a77996c4b1cb707929f833ba7b1d522ee29f882512c330676d/scipy-1.18.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e3b417bf8c2c7c16e8f58ad91db17783ec911ac16e7b50eb6eab6e809b4f5b07", size = 37457323, upload-time = "2026-08-21T23:25:12.483Z" }, + { url = "https://files.pythonhosted.org/packages/93/0e/e0348fbc0dbab65c114cf78957e7dfeb49f8e8b556b4d930cc12ff195e18/scipy-1.18.1-cp313-cp313-win_amd64.whl", hash = "sha256:559ed65f60c1af5a03f3912605a1b5114f522c7c32fb23c3376ae8f03219fe28", size = 36622841, upload-time = "2026-08-21T23:25:18.722Z" }, + { url = "https://files.pythonhosted.org/packages/50/a8/6a77f5f267c555108f0a864b6db714363dab567a8266422a79a385f9232b/scipy-1.18.1-cp313-cp313-win_arm64.whl", hash = "sha256:cd479fc04dd9401e3b4f49e76518768ef99c4f517a98c284eb091fd725719adf", size = 24399315, upload-time = "2026-08-21T23:25:23.458Z" }, + { url = "https://files.pythonhosted.org/packages/06/d5/d8eb4e280ddb56a4ab2c6f02ee49b56b23f6e977cf0802fd6d68dbef14f5/scipy-1.18.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:83de5453a7799afc9048b4616bd085cef126e36412f0ea2f6370c36a2a3a51e7", size = 31090936, upload-time = "2026-08-21T23:25:28.686Z" }, + { url = "https://files.pythonhosted.org/packages/2a/49/59ea385dc3a62ff498ddf3cfff7c2b41b0f9f9d3c4122b3f1dcb6d6327fe/scipy-1.18.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:9554bcc6d715ee87a633a3cc8e7703c6628b100dd29cb8a2efc4c0533c7ff729", size = 28725221, upload-time = "2026-08-21T23:25:33.244Z" }, + { url = "https://files.pythonhosted.org/packages/70/e8/6b0c288c50942d78193696c9f15f9a0874f5178aa0ddf40f83d9924b3e8d/scipy-1.18.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:011413b7426b75012840e35649e00fe0a2c3bae89fed433876e3a99251572efc", size = 20466839, upload-time = "2026-08-21T23:25:37.516Z" }, + { url = "https://files.pythonhosted.org/packages/4b/e0/54fd3793c729e3b936782f181b59cbb1205bf250ab605a16cb1ba61cdd5e/scipy-1.18.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:88f0e784020649f88ea48c9f5ddfa403bf9205820667c0914740b392035afb82", size = 23089121, upload-time = "2026-08-21T23:25:42.019Z" }, + { url = "https://files.pythonhosted.org/packages/0b/56/030af62bea3cf878e0028515dff78c123b01633606a879b63f42d2db99cc/scipy-1.18.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d3ab0e8c69a17dd3559eab8cbb88f258e285c94d572c2719033f90f83290c89", size = 34053851, upload-time = "2026-08-21T23:25:47.998Z" }, + { url = "https://files.pythonhosted.org/packages/6b/89/2a844506d49651e9aa1af6ef95b6bd8031cb1d5a4375edec6155037e04cf/scipy-1.18.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ac0333bdf38309aa3dcbe7e3fa7ea29e7a2c37c6ea306a757b700ded8e4596ad", size = 35329183, upload-time = "2026-08-21T23:25:53.522Z" }, + { url = "https://files.pythonhosted.org/packages/eb/56/c7370c3640e92ac9613cbf26cb3f729f9b12ddf1727b55b94b53b24d6f48/scipy-1.18.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:911de823097db8b63f034299d12662db93344e6ffa0b881cbb57748974b70168", size = 35672551, upload-time = "2026-08-21T23:25:59.387Z" }, + { url = "https://files.pythonhosted.org/packages/24/16/ec8536f351421f8bf60a1120930638f83790f4710b8230446aca3d6159d4/scipy-1.18.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:95298364e251be3e60249facbeeca03631d3bb7584f85879516ec55ac717b81f", size = 37469416, upload-time = "2026-08-21T23:26:05.432Z" }, + { url = "https://files.pythonhosted.org/packages/52/94/d73da0d28f16c45bb9b0a5691b91610b0275c5ef0eb5e43c87cf2dc1bf31/scipy-1.18.1-cp314-cp314-win_amd64.whl", hash = "sha256:78a0d7c918e74a232394117160e7e3db503377572a45bcef8826e4ab8a35feba", size = 37362755, upload-time = "2026-08-21T23:26:11.366Z" }, + { url = "https://files.pythonhosted.org/packages/89/25/e996e4dc74e10e227b1e14db5eaf6608bb6dd33884a64851c38f18dd4249/scipy-1.18.1-cp314-cp314-win_arm64.whl", hash = "sha256:cbf38d043c1aa4ab306e1ada6ab6eddacc3322a20b7af1b30bc93254b366fe09", size = 25036090, upload-time = "2026-08-21T23:26:15.887Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c9/c00213f92309d753b48903e6a451b87eb52ff5b7a16e789d1568bbf221c4/scipy-1.18.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:0fcb3c93519f27bb4f0c4b0f7802cdcaca7fcf93267b75edda2e9f4e8a55cbd7", size = 31485550, upload-time = "2026-08-21T23:26:20.776Z" }, + { url = "https://files.pythonhosted.org/packages/74/b2/e3067c487982d4eeab2938928529410370c06fea84a4d3f4925e7d96647d/scipy-1.18.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:ddef79fb382df40104a19bb7151b3b23e57c1778fcf857c71ceecd9bd264513f", size = 29174642, upload-time = "2026-08-21T23:26:25.395Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ab/374c9fe2d1ec014e576c781a4b5d8e1ba340e8f6b4638c16f711d2b194f0/scipy-1.18.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:0e82073ecc7acc6436fac4b31674109c7e1d3e596789767eda01258a8c9e8123", size = 20916357, upload-time = "2026-08-21T23:26:30.112Z" }, + { url = "https://files.pythonhosted.org/packages/90/38/223915c88a17317cafbf8ca2a42b11c265a9fb1e804aa665544132b5fe8a/scipy-1.18.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:8bcf3c1ba5d6456e2effd30fcbd3459b044d683fcdac79a2e6830f0bdf7de487", size = 23482611, upload-time = "2026-08-21T23:26:34.846Z" }, + { url = "https://files.pythonhosted.org/packages/c4/d1/db0948da8ca57a80b36520ef0a768b967d99f3af65f4b6f1bf6362ad4dd4/scipy-1.18.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cfbf154f2ba187f2ed6cce2639efff7d105f1140573642c0161615b6d91d6a87", size = 34143202, upload-time = "2026-08-21T23:26:40.4Z" }, + { url = "https://files.pythonhosted.org/packages/87/53/39d046cc7574ed6acacb6bd5723e220107ece80bff12faaf3efc4ddeede4/scipy-1.18.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a1d33a7836f7ddc1993427966a0823468ec41bcbdb1a9f9942d1d7e57f803ba3", size = 35380876, upload-time = "2026-08-21T23:26:46.1Z" }, + { url = "https://files.pythonhosted.org/packages/f9/da/32e0e799d875a85ca57d9bde6c78148afcc0e38276df683d95854eadc8c3/scipy-1.18.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7f4b8bc363b6d65ee2152bec57568e3c52639bb34c46057b09857a307ed5e21d", size = 35770885, upload-time = "2026-08-21T23:26:51.533Z" }, + { url = "https://files.pythonhosted.org/packages/88/2e/f97a666d362fee68b18f41c9c30ed502ca5c98b549749bfcb52a8b74d1eb/scipy-1.18.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:11c423f1049c5755ad4409af52a9ada1cff96fe9b50795d4af3619f292901239", size = 37525424, upload-time = "2026-08-21T23:26:56.751Z" }, + { url = "https://files.pythonhosted.org/packages/ca/d5/a9e765a84654ebba8479a1fd1b059ced1af72b168a3b2a3a46540ea38d20/scipy-1.18.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c24acac1e18912761c4700239bbc1fd32f615af690f1584d49b35859be51324d", size = 37416961, upload-time = "2026-08-21T23:27:01.546Z" }, + { url = "https://files.pythonhosted.org/packages/ee/16/e79e0d1c63ef698879d85439d37e9fb434e3b804e506a6991038d086ebd9/scipy-1.18.1-cp314-cp314t-win_arm64.whl", hash = "sha256:9f2897bf7737392ad0d5213ea7b6add72a4edf5679b3153106aeb88b6507b3b9", size = 25331848, upload-time = "2026-08-21T23:27:05.884Z" }, + { url = "https://files.pythonhosted.org/packages/be/4f/1bd37c883b67163e2ca1f60977a399500e6879c15defecac62831c8d078d/scipy-1.18.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:eb0dfcf4e28a99c12c999744a2ff67c9b06200e20401c7c88186e33552a46331", size = 31091484, upload-time = "2026-08-21T23:27:11.051Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c5/ba929d7feb9b2332f96827c12e0e924b61973b59b4dea383b603372c65ce/scipy-1.18.1-cp315-cp315-macosx_12_0_arm64.whl", hash = "sha256:30f464bee641fa8e282577c7dce027308403213c6ca8270bba73285c91024bc5", size = 28725057, upload-time = "2026-08-21T23:27:15.9Z" }, + { url = "https://files.pythonhosted.org/packages/a4/19/68f1c50f609d955d230e66d25d02bd3e1e167ec540232135354fb9a4b9e3/scipy-1.18.1-cp315-cp315-macosx_14_0_arm64.whl", hash = "sha256:1bca3b943fc2567ea49cd02c99abde49da4d5178ec46f624bd8255cda8755beb", size = 20466734, upload-time = "2026-08-21T23:27:20.044Z" }, + { url = "https://files.pythonhosted.org/packages/ef/6d/319fa29b73d1802fa80b32a6eaf3f5be456ef81526da2716a9493bcb5501/scipy-1.18.1-cp315-cp315-macosx_14_0_x86_64.whl", hash = "sha256:c9d18a33309122074ea483dd92dd444189166b8b2ec429fe9ed5ac73c7a0aa23", size = 23089664, upload-time = "2026-08-21T23:27:24.345Z" }, + { url = "https://files.pythonhosted.org/packages/b7/db/30992f9b51a63de671daf3888ffd18378b6cb9ec9f2c972264238ffa7fd6/scipy-1.18.1-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:82f201b4c878551d48558337aab270d3c6cca5507b8737c8d8a608d234cccde0", size = 34054035, upload-time = "2026-08-21T23:27:29.409Z" }, + { url = "https://files.pythonhosted.org/packages/91/d4/bf3e735dc0b9d5a8ff45079d2540e17d3aff7a2f0048dd8f552ffd031d2b/scipy-1.18.1-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ac49ea97594532dd44b7136094d35f5440fa06e6d9c6384a74c01764df388c5", size = 35333883, upload-time = "2026-08-21T23:27:34.293Z" }, + { url = "https://files.pythonhosted.org/packages/19/93/12d78ce9f871fe945fca588d32644e6e63f553c2a35c564d73f3b22a3313/scipy-1.18.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:ceb30a00ce7c92d459819443d29ca486d882b83fb6738bdcbb2a1cce94ac5daa", size = 35673124, upload-time = "2026-08-21T23:27:39.059Z" }, + { url = "https://files.pythonhosted.org/packages/70/cd/886219313a1012a48e6ae0ec4f302c837151beb92e1ff0d709ef8fdfc488/scipy-1.18.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:f29633129f9fa7e88a3f0fca835de2d030bfc9643f7799e1a0c46cee24d38fc7", size = 37470753, upload-time = "2026-08-21T23:27:44.435Z" }, + { url = "https://files.pythonhosted.org/packages/17/6c/a776888ce618bee54fbde26172f0f46ac1da70d27b63861797fe78e1904b/scipy-1.18.1-cp315-cp315-win_amd64.whl", hash = "sha256:92c14f5bdbfb6216315ce33e78080474082de8b3830122ba97809bfbe65f75c0", size = 37361483, upload-time = "2026-08-21T23:27:49.334Z" }, + { url = "https://files.pythonhosted.org/packages/ab/09/97b651691322ebee97999b017ffc18a15a0b815103844c97e8da9d469731/scipy-1.18.1-cp315-cp315-win_arm64.whl", hash = "sha256:e402cf31eb68f453dbb2d36fc6d722b33f24a55d68b2ae1d92fa6305ca71c298", size = 25035883, upload-time = "2026-08-21T23:27:53.596Z" }, + { url = "https://files.pythonhosted.org/packages/ed/0f/9ec20467bbabd0d44e2a77d0fd3d124f884b4d67df92af82c91d2d6a486f/scipy-1.18.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:2a0b02f9fc46f8520330c23d45e6560db7e3a0d927232139427637f98943e11d", size = 31474926, upload-time = "2026-08-21T23:27:57.993Z" }, + { url = "https://files.pythonhosted.org/packages/8a/58/dcb79161e56efbedc50079fcd2f5fe427a0ebb53022eb476aa73c015ad8f/scipy-1.18.1-cp315-cp315t-macosx_12_0_arm64.whl", hash = "sha256:1d73131e358976663dd969e1fb4ed1404b815cd977eaaedc3b3a133ba2d81c35", size = 29164940, upload-time = "2026-08-21T23:28:03.062Z" }, + { url = "https://files.pythonhosted.org/packages/71/d3/1eeea80c817fcb8ef7bd4a05a58824977a0e57a375cfc3d7ea7c911c01ad/scipy-1.18.1-cp315-cp315t-macosx_14_0_arm64.whl", hash = "sha256:bff0b729edd992766136b34e39cc76bc2fad905aa58897ee72a9cd000a6d8443", size = 20906742, upload-time = "2026-08-21T23:28:07.642Z" }, + { url = "https://files.pythonhosted.org/packages/54/46/e59350428b6099301a20128108c995e2eb175a43f383af9a346e38824f9b/scipy-1.18.1-cp315-cp315t-macosx_14_0_x86_64.whl", hash = "sha256:10ac20c69d880f77f375db44c22e3e6a644f9fefa291d4cd2fb9790a89fc99fd", size = 23472183, upload-time = "2026-08-21T23:28:12.109Z" }, + { url = "https://files.pythonhosted.org/packages/89/31/cc91623fa98f0621766a0f0aaaadb2c66de74a7ea7e3837164f6e4354260/scipy-1.18.1-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:33a834464fdabc0f26a45508df31b3cc5d028e04dbf6c5ed398541418e0a12fe", size = 34130796, upload-time = "2026-08-21T23:28:17.906Z" }, + { url = "https://files.pythonhosted.org/packages/fc/3e/8572ef536957ddb8aa81bb4090d9e25f257e3b4e05d97deb54319deb8a3a/scipy-1.18.1-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:49023963c193dacee096301452f223ee24d86ec5807f8df93c0f7221d119e305", size = 35374253, upload-time = "2026-08-21T23:28:23.732Z" }, + { url = "https://files.pythonhosted.org/packages/b5/c6/59fdeffb4f1435299f93d9dc8140b43ad2916e6cfc944be6c3041fcec86d/scipy-1.18.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:d84a09d0dad90ba6525d8ac1c2334b33e64bf3ccfe9e841f02feb867a22681e4", size = 35758543, upload-time = "2026-08-21T23:28:29.431Z" }, + { url = "https://files.pythonhosted.org/packages/cf/d9/135be205d9de8783193aff9cc3bf483a03a38e4b29432c954e8cb66ac14e/scipy-1.18.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:179ce34a8d0fe273d8883ba59e17e052247d08973dfcb743ca52bb1cce2d60b0", size = 37521946, upload-time = "2026-08-21T23:28:35.245Z" }, + { url = "https://files.pythonhosted.org/packages/5c/a2/5b7d5270621ab7cfa3f7766067bf95dc360b5efb6394694e8143b4156e2b/scipy-1.18.1-cp315-cp315t-win_amd64.whl", hash = "sha256:5632e3ae3d09197c446310cd5187de63e28448ce22f0f67b2b93d97503c0c230", size = 37408295, upload-time = "2026-08-21T23:28:40.724Z" }, + { url = "https://files.pythonhosted.org/packages/63/ad/741c19fcb66755ff953daf9243af8480e4bf3d7fbe57583c178c7d2b6b51/scipy-1.18.1-cp315-cp315t-win_arm64.whl", hash = "sha256:eda632a7981f69730d6281f451db9c1c370993a2c0d7ddb43e2a809a2862b83a", size = 25319710, upload-time = "2026-08-21T23:28:45.713Z" }, +] + +[[package]] +name = "screeninfo" +version = "0.8.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cython", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ec/bb/e69e5e628d43f118e0af4fc063c20058faa8635c95a1296764acc8167e27/screeninfo-0.8.1.tar.gz", hash = "sha256:9983076bcc7e34402a1a9e4d7dabf3729411fd2abb3f3b4be7eba73519cd2ed1", size = 10666, upload-time = "2022-09-09T11:35:23.419Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/bf/c5205d480307bef660e56544b9e3d7ff687da776abb30c9cb3f330887570/screeninfo-0.8.1-py3-none-any.whl", hash = "sha256:e97d6b173856edcfa3bd282f81deb528188aff14b11ec3e195584e7641be733c", size = 12907, upload-time = "2022-09-09T11:35:21.351Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "skia-pathops" +version = "0.9.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4a/f6/ab37d6fa21f25965d4ad059745c76f13ddfb92a2c06a842a42ad77961c24/skia_pathops-0.9.2.tar.gz", hash = "sha256:4b6d8459f6f4a69282cb26fca0c2bb0b321cc58a9bf9cc6579a52a391edc0319", size = 65115346, upload-time = "2026-02-16T10:30:22.753Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/10/45af3ea2dee706a340601da315e450c449f76aaa18651cfe2677b0c65161/skia_pathops-0.9.2-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:c7a925b919c050df1e0545d26a69b64d9c8e1959e4dca73b06df088bf01d30f0", size = 2897494, upload-time = "2026-02-16T10:30:00.265Z" }, + { url = "https://files.pythonhosted.org/packages/b4/20/711d733b252696aedd78175e0f60acd42299de680d5a50a2750b96578841/skia_pathops-0.9.2-cp310-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d94c18a60b1e56910240a65dcc841cca37067f6432a4881d20baff17b6d77915", size = 3300314, upload-time = "2026-02-16T10:30:02.53Z" }, + { url = "https://files.pythonhosted.org/packages/95/47/bccbdb7ee12bbcc43c1051f9fd6933d16e3ffc8f20263155bd03709bde9e/skia_pathops-0.9.2-cp310-abi3-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70b28883f209a0df84da14e980e7a07c55ed93b913955694d90ddef2f29a261b", size = 2999370, upload-time = "2026-02-16T10:30:04.445Z" }, + { url = "https://files.pythonhosted.org/packages/5e/55/d5a1925d61ab683aaf08a3a80749371991f82fdeb53e3dbeae0046ce80ac/skia_pathops-0.9.2-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:8122e50e03472657781c951f3019d9b85d9b1cbc93ebac635b673f99b18895b6", size = 3955374, upload-time = "2026-02-16T10:30:06.308Z" }, + { url = "https://files.pythonhosted.org/packages/36/49/c81e1a551726b8153c7a600b041e4480ac85db13985b7b260a01d85e0a6c/skia_pathops-0.9.2-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f8c72c70e57dd6655f880ae63642ff26753bb888f4c6411709a12cc10bee005e", size = 4316667, upload-time = "2026-02-16T10:30:08.326Z" }, + { url = "https://files.pythonhosted.org/packages/bb/b4/484ba09325c75d4470578b3f16bfe5a30cc348ece2566df59702c487084c/skia_pathops-0.9.2-cp310-abi3-win32.whl", hash = "sha256:3b9ef6629d2c8cb8604a7e22d4d2fe2c0160788b9792145a2cf0f968ec245751", size = 1450912, upload-time = "2026-02-16T10:30:09.762Z" }, + { url = "https://files.pythonhosted.org/packages/ff/96/bd1bfaf5335242a86d8af940ff2124861e9bc6a5520cf96fda374fd10945/skia_pathops-0.9.2-cp310-abi3-win_amd64.whl", hash = "sha256:9ae72d801b5f4c5dbb937fb25476a12fa2da70860f28dd637da2b529dbd8271c", size = 1782901, upload-time = "2026-02-16T10:30:11.546Z" }, + { url = "https://files.pythonhosted.org/packages/28/b0/d617e285ec16282bb646d28a7fdd846682d6c8205ba3ecf3d744742c6d5e/skia_pathops-0.9.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:22e99c3a4ed54dd957ffc1e4fa653bab863524fcdc2027ce68131ee293e969b8", size = 1570679, upload-time = "2026-02-16T10:30:12.868Z" }, + { url = "https://files.pythonhosted.org/packages/94/1e/5b670c8e1a7b893f6564d0b9982beabdfc4a3d75714fe64ad476936b3e1e/skia_pathops-0.9.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:297b174254fd607afba67294de0012f60ca93abfecc22c062d6bc4e83fe81b1b", size = 1308208, upload-time = "2026-02-16T10:30:14.155Z" }, + { url = "https://files.pythonhosted.org/packages/e2/99/d6ad60fbbe549781dc77d6e32f87c64cf48145c6c38a9e03fa839d2cace9/skia_pathops-0.9.2-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:49f8b0340316f3a43ac8ee797746018d131b70f0b4d3f40f3d2136bfd8b0ecf5", size = 2339788, upload-time = "2026-02-16T10:30:16.163Z" }, + { url = "https://files.pythonhosted.org/packages/88/34/fa72e85e1d74dd7f39a390639354e6929be241d9a32fe10af8c7927e1fe1/skia_pathops-0.9.2-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83b218c84cd04a44f149191076916354413c2071280c49ac717cf3d53ef82319", size = 2027123, upload-time = "2026-02-16T10:30:18.205Z" }, + { url = "https://files.pythonhosted.org/packages/5a/38/846754e2e29a308d08d8b94ed6ff6e6c296dffb0294fd1eeda805eef5092/skia_pathops-0.9.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:e834d60788e112fe53bd9185209ffb7d4e19db2789a40abbd01a25bfc01a5515", size = 1779426, upload-time = "2026-02-16T10:30:20.052Z" }, +] + +[[package]] +name = "soundfile" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "numpy", version = "2.5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d2/db/949331952a6fb1c5b12e9de80fd08747966c2039d1a61db4764fbd3981c2/soundfile-0.14.0.tar.gz", hash = "sha256:ba1c1a2d618bca5c406647c83b89f07cc8810fa506a50622a6993ba130c1de11", size = 47842, upload-time = "2026-06-06T08:58:47.869Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/d1/5e338af9ca6ed0786cd5bb03f6d60de1c325728c1189014f3b59aae7403c/soundfile-0.14.0-py2.py3-none-any.whl", hash = "sha256:8ba81ae3a89fd5ab3bef8a8eb481fbbe794e806309675a89b4df48b8d31908a8", size = 26799, upload-time = "2026-06-06T08:58:33.269Z" }, + { url = "https://files.pythonhosted.org/packages/7e/72/c6b21e58d3113596e7e8de0a08d6f1d95173492cfbca0a4db14148cbba2a/soundfile-0.14.0-py2.py3-none-macosx_10_9_x86_64.whl", hash = "sha256:19be05428da76ed61a4cad29b8e4bcf43a3e5c100089d2ec81dc961eed1b0dd4", size = 1144568, upload-time = "2026-06-06T08:58:35.231Z" }, + { url = "https://files.pythonhosted.org/packages/63/7a/dfdd6f8c748988427119f75eb860a3cedd858d1aea1fe28f39ad8559ef22/soundfile-0.14.0-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:d828d35a059626da52f1415b5faee610aeab393319cb3fc4a9aef47b619fc14c", size = 1103726, upload-time = "2026-06-06T08:58:37.948Z" }, + { url = "https://files.pythonhosted.org/packages/4a/f8/fc39fad6f879633461d27394cd1ddaf1f769ffa0597dca35872f51b16461/soundfile-0.14.0-py2.py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:e85724a90bc99a6e8062c0b4ddf725f53b2a3b70afd4da875e9d2cfc4e92f377", size = 1238050, upload-time = "2026-06-06T08:58:39.932Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a2/70fd4432b924684c372df8b0a45708c36c057ef3596c9eb53e0a806b980b/soundfile-0.14.0-py2.py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:1e38bac1853412871318e82a1ba69a8be677619b56025bbfcccdb41b6cafe82d", size = 1315963, upload-time = "2026-06-06T08:58:41.716Z" }, + { url = "https://files.pythonhosted.org/packages/d9/34/c9e80783d83eab739a9531fdee03675d53e0bf1b2ccb4bb3af5844675046/soundfile-0.14.0-py2.py3-none-win32.whl", hash = "sha256:0a6ae43c50c71b4e020cc55382925cb89451c1ed1a0c3d0f5d802da269226849", size = 902199, upload-time = "2026-06-06T08:58:43.289Z" }, + { url = "https://files.pythonhosted.org/packages/ed/97/b39c18ac1df45e755ca22b8b00e872929da5d107998a207a5e4ac831bfda/soundfile-0.14.0-py2.py3-none-win_amd64.whl", hash = "sha256:299491d3499460fb1b74bb4bd78b57ffc2d243a5fafa7b6ec1b264875c78453e", size = 1021480, upload-time = "2026-06-06T08:58:45.016Z" }, + { url = "https://files.pythonhosted.org/packages/f4/83/55c65e61cf457805ce2ec157c1c6ae17715d0851aa2374422de0538838ca/soundfile-0.14.0-py2.py3-none-win_arm64.whl", hash = "sha256:e090704718e124e7c844695236f1fce8d18a5e761eaf7c82dfcd124620805f98", size = 888858, upload-time = "2026-06-06T08:58:46.593Z" }, +] + +[[package]] +name = "soupsieve" +version = "2.9.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/69/99/a6ca3beb3ccacb41fb3321d8a60e5566f9e6467601ef8eba6a17e1b89778/soupsieve-2.9.2.tar.gz", hash = "sha256:4a55d8cf158a9c2e587fa4922f1bbb91d68ac829e2d6f25403a85747c71daf74", size = 122445, upload-time = "2026-08-07T00:57:24.801Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/dc/ad025c1ee131eba60c69f4dd5779b18fcf1e6b21a343e2162a84d5d133c7/soupsieve-2.9.2-py3-none-any.whl", hash = "sha256:8089a26fd974ca7a1f30276d3d8492ab266ab15af581642dfe8aa162e0c1c823", size = 37370, upload-time = "2026-08-07T00:57:23.524Z" }, +] + +[[package]] +name = "soxr" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "numpy", version = "2.5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/11/27cebce4a108f77afea7c80545115536b45e3f11ebfb914f638fdd9ba847/soxr-1.1.0.tar.gz", hash = "sha256:9f228ae21c78fa9359ca98d8a5e8e91f30639e438e574133dace62c5b5309e44", size = 173067, upload-time = "2026-05-03T00:15:18.214Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/96/6b335638dd3ef4e5d50b9a0a7497e8433ab10fb45457497010074dd3c734/soxr-1.1.0-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:9564d82f7fa6bf548e5f18bb86235dff20eea8bd30727b64d49783c95c34fb8d", size = 205270, upload-time = "2026-05-03T00:14:38.391Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0e/79e479b38f014757af877755c6eeea10c8750f66ef1e2231709f1d5dc7ab/soxr-1.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9443e5eb82152d8952422b7285692192cc7dcffa5218bb511b096203018bc273", size = 167049, upload-time = "2026-05-03T00:14:40.226Z" }, + { url = "https://files.pythonhosted.org/packages/b3/aa/52759e223bd5b4923e518d6312161887c96d42b81df5425198cf9f3371e2/soxr-1.1.0-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:588c7de1abafe59e66face9a074514658ac0398c85a774cdbb8efac131192692", size = 210589, upload-time = "2026-05-03T00:14:41.863Z" }, + { url = "https://files.pythonhosted.org/packages/38/89/a6550d26ebeb17f83e03cf6cde2d084b8d292900a027d43a6696cbce5c5a/soxr-1.1.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:26925618945f1a44dfbd783cc572874f0685e9ecdf46b96f4000f6b8c9c8b825", size = 245032, upload-time = "2026-05-03T00:14:43.341Z" }, + { url = "https://files.pythonhosted.org/packages/79/c6/2e47f17fa4461ba047f5f38a592f39120c21e6af8786268b0be2ef870318/soxr-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:b2e94c713b7d96fb92841947b785bcee6606124bc852273fab70454b51bfe270", size = 176571, upload-time = "2026-05-03T00:14:45.035Z" }, + { url = "https://files.pythonhosted.org/packages/8e/49/3e6bc84f87439f222f40b616e9a29a170f41fb564710ea510df19dc26907/soxr-1.1.0-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:34cc92208c3c412c046813e69da639c04a792c6a41fbfd7d909d359cd3e97a2d", size = 205699, upload-time = "2026-05-03T00:14:46.67Z" }, + { url = "https://files.pythonhosted.org/packages/2f/94/216f46096a85b07d1e6ba7fd44491402e912a3d688cd4f36f0a600ca155f/soxr-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bd30f7201eac896ebf5db7b09156e6f1a1b82601900d29d9c8449bdad8365b11", size = 167381, upload-time = "2026-05-03T00:14:48.012Z" }, + { url = "https://files.pythonhosted.org/packages/94/cb/06caa463b8181ec1981bd6376d4a873748b7008193188b8cfb60391eb131/soxr-1.1.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1577865e993f98ffb261257c3060fa76ec3db44ed3f181b16464268000424464", size = 210938, upload-time = "2026-05-03T00:14:49.768Z" }, + { url = "https://files.pythonhosted.org/packages/86/47/d5964551ca818b7f0c7ef7f3899056263b60ef098a801066350a9672ca8f/soxr-1.1.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3da87e3ffa3e41823d873b051c7ecb2acebd8d1b6b46b752f5facf10a0d84ab9", size = 245268, upload-time = "2026-05-03T00:14:51.422Z" }, + { url = "https://files.pythonhosted.org/packages/8f/29/371467eb86c7ba6810df0bfe9409bcd9c52ec5615b111190fafe23e4d2e1/soxr-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:ae30c48ac795378cf23ba3c7c640b8ff794af714ac388b9fd6b31a40b39e6e86", size = 176779, upload-time = "2026-05-03T00:14:53.09Z" }, + { url = "https://files.pythonhosted.org/packages/06/8a/f3da7973b5f1b05d2d7e94d5376b881dcbc05297900cae6c3d33d95b209b/soxr-1.1.0-cp312-abi3-macosx_10_14_x86_64.whl", hash = "sha256:e0e09fa633ce2e67df08b298afced4d184f6e753fc330f241022250f1d0d61da", size = 204124, upload-time = "2026-05-03T00:14:54.505Z" }, + { url = "https://files.pythonhosted.org/packages/03/dc/200013a74641f8774664bbcd2346c695c05c2e300ea792adcb40a293eed0/soxr-1.1.0-cp312-abi3-macosx_11_0_arm64.whl", hash = "sha256:d6a7ad82b8d5f3fcc04b1d2ca055562b96af571e1d4fa7c6c61d0fb509ac43b4", size = 165457, upload-time = "2026-05-03T00:14:56.007Z" }, + { url = "https://files.pythonhosted.org/packages/88/2b/2e5eba817a762a2ec589ff165b8bc5955b25a0ad140045f7cd8e45410543/soxr-1.1.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf98c0d7b7d5ef5bf072fee8d3020e8b664f2d195933ea7bc5089267c2e22a06", size = 206529, upload-time = "2026-05-03T00:14:57.646Z" }, + { url = "https://files.pythonhosted.org/packages/5c/f1/0e55195893228609c9a08c3b13b7a83a46c3a992cd00d3304f0f320cfb07/soxr-1.1.0-cp312-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b033078e86f3c4a658e5697fac8995764fad9e799563616b630136b613167f1", size = 240413, upload-time = "2026-05-03T00:14:59.363Z" }, + { url = "https://files.pythonhosted.org/packages/b0/4d/621e4150e4815246ad552d215a8a294a90143fedd19ee442cf82d3b3abc8/soxr-1.1.0-cp312-abi3-win_amd64.whl", hash = "sha256:6ae2a174bffea94e8ead857dad85999d3f49f091774dbad5b046c0417d7092f4", size = 174357, upload-time = "2026-05-03T00:15:00.724Z" }, + { url = "https://files.pythonhosted.org/packages/76/cd/77b74f1e95af0e11e52e9a034421aece7f7b45afd15a909afd41d5a5d102/soxr-1.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a941f5aaa0b8abced24318105c1ea22576afcc1138c19f625716ce4e2f76ad64", size = 207990, upload-time = "2026-05-03T00:15:02.1Z" }, + { url = "https://files.pythonhosted.org/packages/30/86/600cc31f982288167a59972746f117790162012546f995a32b5a55394b16/soxr-1.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:feebcba99ac99adb8009d46c8f4c1956b8c167576b0ae8a6fb47502e9a6f78e7", size = 169288, upload-time = "2026-05-03T00:15:03.75Z" }, + { url = "https://files.pythonhosted.org/packages/39/e4/80cd9aae0645513db1076d4384e8b2d895faf5009218b4a04348012c54fc/soxr-1.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:52c9ca84e3dc656d83acc424574770e20ea8e0704dc3842d4e27b0fe9d3ba449", size = 211405, upload-time = "2026-05-03T00:15:05.395Z" }, + { url = "https://files.pythonhosted.org/packages/a6/d6/cc3c80ac9b2289da4cf46c5d53b05e4327e6f5560a25868d06f9e2213af1/soxr-1.1.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4977323ef9c3aa3c2a26ff5fe0191c84b8fd759daf7afb1f25a91a55ad8b730", size = 244617, upload-time = "2026-05-03T00:15:07.134Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/f7af5fae841ffe32ed8440234ea2ad6adecca3bd92b6101076268c429000/soxr-1.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e17d4ef9b0185214b2c0935605ae63f827ea423bc74964be44763d68d2b6c21e", size = 187253, upload-time = "2026-05-03T00:15:08.813Z" }, +] + +[[package]] +name = "srt" +version = "3.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/66/b7/4a1bc231e0681ebf339337b0cd05b91dc6a0d701fa852bb812e244b7a030/srt-3.5.3.tar.gz", hash = "sha256:4884315043a4f0740fd1f878ed6caa376ac06d70e135f306a6dc44632eed0cc0", size = 28296, upload-time = "2023-03-28T02:35:44.007Z" } + +[[package]] +name = "svgelements" +version = "1.9.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5d/29/1c93c94a2289675ba2ff898612f9c9a03f46d69f253bdf4da0dfc08a599d/svgelements-1.9.6.tar.gz", hash = "sha256:7c02ad6404cd3d1771fd50e40fbfc0550b0893933466f86a6eb815f3ba3f37f7", size = 162145, upload-time = "2023-08-17T02:01:51.822Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/2c/6c9bb53db56c8a12a736d2158a8b842a5993b96daabc29d90a098e840280/svgelements-1.9.6-py2.py3-none-any.whl", hash = "sha256:8a5cf2cc066d98e713d5b875b1d6e5eeb9b92e855e835ebd7caab2713ae1dcad", size = 137856, upload-time = "2023-08-17T02:01:48.76Z" }, +] + +[[package]] +name = "threadpoolctl" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274, upload-time = "2025-03-13T13:49:23.031Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" }, +] + +[[package]] +name = "tqdm" +version = "4.70.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/21/3b/6c24bec5be5e743ffd99576daa5cc077722fc7d5bbc00bd133fa0c698dc6/tqdm-4.70.0.tar.gz", hash = "sha256:55b0b0dbd97462d06ebee91e4dac24ed4d4702be82b24f07e6c1d27e08cea220", size = 795438, upload-time = "2026-07-27T11:33:15.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/1c/01bfd571a64e7f270e6bab5e33777debe0edc56759233ce84f27dec92d14/tqdm-4.70.0-py3-none-any.whl", hash = "sha256:7f585706bfddbdebf89daac705b2dfcc16890130727d3197ca62c732b4310953", size = 80184, upload-time = "2026-07-27T11:33:13.167Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +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 = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +] + +[[package]] +name = "video-use" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "librosa", version = "0.11.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "librosa", version = "1.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "matplotlib", version = "3.10.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "matplotlib", version = "3.11.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "numpy", version = "2.5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "pillow" }, + { name = "requests" }, +] + +[package.optional-dependencies] +animations = [ + { name = "manim", version = "0.19.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "manim", version = "0.21.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] + +[package.metadata] +requires-dist = [ + { name = "librosa" }, + { name = "manim", marker = "extra == 'animations'" }, + { name = "matplotlib" }, + { name = "numpy" }, + { name = "pillow" }, + { name = "requests" }, +] +provides-extras = ["animations"] + +[[package]] +name = "watchdog" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/56/90994d789c61df619bfc5ce2ecdabd5eeff564e1eb47512bd01b5e019569/watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26", size = 96390, upload-time = "2024-11-01T14:06:24.793Z" }, + { url = "https://files.pythonhosted.org/packages/55/46/9a67ee697342ddf3c6daa97e3a587a56d6c4052f881ed926a849fcf7371c/watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112", size = 88389, upload-time = "2024-11-01T14:06:27.112Z" }, + { url = "https://files.pythonhosted.org/packages/44/65/91b0985747c52064d8701e1075eb96f8c40a79df889e59a399453adfb882/watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3", size = 89020, upload-time = "2024-11-01T14:06:29.876Z" }, + { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393, upload-time = "2024-11-01T14:06:31.756Z" }, + { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392, upload-time = "2024-11-01T14:06:32.99Z" }, + { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019, upload-time = "2024-11-01T14:06:34.963Z" }, + { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" }, + { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" }, + { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" }, + { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, + { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, + { url = "https://files.pythonhosted.org/packages/30/ad/d17b5d42e28a8b91f8ed01cb949da092827afb9995d4559fd448d0472763/watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881", size = 87902, upload-time = "2024-11-01T14:06:53.119Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ca/c3649991d140ff6ab67bfc85ab42b165ead119c9e12211e08089d763ece5/watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11", size = 88380, upload-time = "2024-11-01T14:06:55.19Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, + { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, + { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, + { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, + { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, + { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, +] From 3e5ecc561e2143ea7939c84319bffe4c76882154 Mon Sep 17 00:00:00 2001 From: Ismael Date: Wed, 2 Sep 2026 14:02:53 -0700 Subject: [PATCH 12/19] add measured scope and legibility limits to the explainer contract --- SKILL.md | 4 ++- .../references/concept-explainer.md | 26 +++++++++++++++++++ .../references/production-quality.md | 2 +- 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/SKILL.md b/SKILL.md index 495dc0f9..f6ff40ba 100644 --- a/SKILL.md +++ b/SKILL.md @@ -212,7 +212,9 @@ independently renderable Manim class per narrative chapter with named internal beats, and build persistent semantic objects from `skills/manim-video/assets/teaching.py` and `skills/manim-video/assets/domains/`. Narration is part of this deliverable by default unless the user explicitly -requests silence. This requirement does not apply to clip editing or an isolated +requests silence. Teach one mechanism per explainer under a minute, keep chapters to +three, never time stretch recorded narration to fit, and keep labels at font size 24 +or larger; the full contract is in `skills/manim-video/references/concept-explainer.md`. This requirement does not apply to clip editing or an isolated illustration overlay. Read `skills/manim-video/references/concept-explainer.md` before authoring a Manim scene. diff --git a/skills/manim-video/references/concept-explainer.md b/skills/manim-video/references/concept-explainer.md index c0c46f53..080ac4aa 100644 --- a/skills/manim-video/references/concept-explainer.md +++ b/skills/manim-video/references/concept-explainer.md @@ -10,6 +10,32 @@ explicitly requests silence. Do not create a silent placeholder track: generate speech, use its measured duration for timing, and verify the assembled audio has finite loudness and audible samples before normalization. +## Scope and legibility contract + +These limits come from measured runs and are not taste. Treat them as hard. + +1. **One mechanism per explainer under a minute.** A 45 second piece teaches one idea + completely. If the narration needs a second mechanism to make sense, cut the first + one down or ask for a longer runtime. At most three chapters for 45 seconds. +2. **Narration pace 150 to 185 words per minute.** Land the runtime by editing words or by + setting the voice speed before locking the take. Never time stretch a recorded + narration to fit; a take more than five percent long is a script problem. +3. **Original explainers are Manim with the teaching assets.** PIL is for overlay cards + only. A whole explainer rendered as raster frames is a failed handoff even when the + layout manifest passes. +4. **Chapters share geometry.** Build persistent objects through one base scene or shared + builders so every carried object has identical position and size at a chapter boundary. + Compare the exit frame of each chapter with the entry frame of the next before rendering. +5. **Legibility floor at 1920x1080.** Labels use font_size 24 or larger, titles 36 or larger, + raster text at least 28 px cap height. At most four foreground groups on screen at any + critical frame. The bottom 16 percent of the frame is the caption rail and stays empty. +6. **Declare color roles once.** The plan names the role of each color (key, hash path, + success, warning) and those roles never change during the video. +7. **Plan depth.** The beat table has about one row per four seconds of runtime, each row + naming its objects, the state change, and what the beat proves. +8. **Hold the payoff.** Narration ends at least two seconds before the video does and the + final frame holds still. + ## Efficient Production Order 1. Research the topic and verify every factual claim and worked value. diff --git a/skills/manim-video/references/production-quality.md b/skills/manim-video/references/production-quality.md index aa2a869e..6436caf4 100644 --- a/skills/manim-video/references/production-quality.md +++ b/skills/manim-video/references/production-quality.md @@ -28,7 +28,7 @@ if text.width > config.frame_width - 1.0: text.scale_to_fit_width(config.frame_width - 1.0) ``` -- Keep ordinary text at `font_size=18` or larger. +- Keep labels at `font_size=24` or larger and titles at 36 or larger; keep at most four foreground groups on screen at any critical frame. - Use the approved typography; monospace is optional, not universal. - Test selected fonts on the render host. - Run `assert_inside_frame()` and `assert_no_overlap()` for every chapter ending. From decab15b48a5ac88b20004589d177fdd200d3c00 Mon Sep 17 00:00:00 2001 From: Ismael Date: Wed, 2 Sep 2026 14:19:32 -0700 Subject: [PATCH 13/19] convert file headers to module docstrings --- skills/manim-video/assets/__init__.py | 3 --- skills/manim-video/assets/concept_explainer.py | 4 ---- skills/manim-video/assets/domains/__init__.py | 3 --- skills/manim-video/assets/domains/_common.py | 4 ---- skills/manim-video/assets/domains/biology.py | 3 --- skills/manim-video/assets/domains/computing.py | 3 --- skills/manim-video/assets/domains/finance.py | 3 --- skills/manim-video/assets/domains/math.py | 3 --- skills/manim-video/assets/domains/physics.py | 3 --- skills/manim-video/assets/domains/systems.py | 3 --- skills/manim-video/assets/teaching.py | 4 ---- skills/manim-video/scripts/preview_scene.py | 4 ---- tests/fixtures/manim_domain_gallery.py | 5 +++-- tests/test_manim_domains.py | 5 +++-- tests/test_manim_gallery.py | 5 +++-- tests/test_manim_teaching.py | 5 +++-- tests/test_preview_scene.py | 5 +++-- 17 files changed, 15 insertions(+), 50 deletions(-) diff --git a/skills/manim-video/assets/__init__.py b/skills/manim-video/assets/__init__.py index 6bc5ff2a..d8df1d77 100644 --- a/skills/manim-video/assets/__init__.py +++ b/skills/manim-video/assets/__init__.py @@ -1,6 +1,3 @@ -# public entry point for the reusable manim explainer assets -# it re exports the teaching primitives so scripts can import them from one place - """Reusable style-neutral Manim explainer assets.""" from .teaching import ( diff --git a/skills/manim-video/assets/concept_explainer.py b/skills/manim-video/assets/concept_explainer.py index 4ff13b46..ae6811b8 100644 --- a/skills/manim-video/assets/concept_explainer.py +++ b/skills/manim-video/assets/concept_explainer.py @@ -1,7 +1,3 @@ -# layout guards for narrated 16 by 9 manim explainers that share the frame with captions -# it defines split composition regions and helpers that fit text and panels and assert nothing leaves its bounds -# a script imports these so authored visuals stay above the caption rail before rendering - """Small layout guards for narrated 16:9 Manim explainers. Import these helpers into a project's ``script.py``. They keep authored visuals diff --git a/skills/manim-video/assets/domains/__init__.py b/skills/manim-video/assets/domains/__init__.py index 2146ef3e..3e439595 100644 --- a/skills/manim-video/assets/domains/__init__.py +++ b/skills/manim-video/assets/domains/__init__.py @@ -1,6 +1,3 @@ -# package entry point for the semantic domain component kit -# it gathers every domain class from biology computing finance math physics and systems into one namespace - """Broad semantic component kit for original Manim explainers.""" from .biology import CellProcess, PopulationFlow, SequenceProcess diff --git a/skills/manim-video/assets/domains/_common.py b/skills/manim-video/assets/domains/_common.py index 1498f318..fa90a35a 100644 --- a/skills/manim-video/assets/domains/_common.py +++ b/skills/manim-video/assets/domains/_common.py @@ -1,7 +1,3 @@ -# shared construction helpers for the semantic domain kit -# it builds labeled boxes circle nodes arrows and rows and validates indices amounts and themes -# every domain module imports from here so the components share one look - """Shared construction helpers for the style-neutral semantic domain kit.""" from __future__ import annotations diff --git a/skills/manim-video/assets/domains/biology.py b/skills/manim-video/assets/domains/biology.py index 9e806825..227778e5 100644 --- a/skills/manim-video/assets/domains/biology.py +++ b/skills/manim-video/assets/domains/biology.py @@ -1,6 +1,3 @@ -# semantic components for cellular sequence and population processes -# it defines CellProcess SequenceProcess and PopulationFlow built on SemanticMobject - """Semantic components for cellular, sequence, and population processes.""" from __future__ import annotations diff --git a/skills/manim-video/assets/domains/computing.py b/skills/manim-video/assets/domains/computing.py index 5ff42538..c61b47bc 100644 --- a/skills/manim-video/assets/domains/computing.py +++ b/skills/manim-video/assets/domains/computing.py @@ -1,6 +1,3 @@ -# semantic components for algorithms machine learning and ai -# it defines ArrayModel GraphModel StateMachine TokenFlow and NeuralLayer built on SemanticMobject - """Semantic components for algorithms, machine learning, and AI.""" from __future__ import annotations diff --git a/skills/manim-video/assets/domains/finance.py b/skills/manim-video/assets/domains/finance.py index d992c971..98d2b032 100644 --- a/skills/manim-video/assets/domains/finance.py +++ b/skills/manim-video/assets/domains/finance.py @@ -1,6 +1,3 @@ -# semantic components for finance business and resource explanations -# it defines CashFlow CompoundTimeline Funnel FeedbackLoop and ResourceFlow built on SemanticMobject - """Semantic components for finance, business, and resource explanations.""" from __future__ import annotations diff --git a/skills/manim-video/assets/domains/math.py b/skills/manim-video/assets/domains/math.py index d2e3f919..db040fb8 100644 --- a/skills/manim-video/assets/domains/math.py +++ b/skills/manim-video/assets/domains/math.py @@ -1,6 +1,3 @@ -# semantic components for mathematical explanations -# it defines NumberLineModel VectorMap MatrixMap LinkedPlot and ProbabilityMass built on SemanticMobject - """Semantic components for mathematical explanations.""" from __future__ import annotations diff --git a/skills/manim-video/assets/domains/physics.py b/skills/manim-video/assets/domains/physics.py index 0d2d200b..fed771c7 100644 --- a/skills/manim-video/assets/domains/physics.py +++ b/skills/manim-video/assets/domains/physics.py @@ -1,6 +1,3 @@ -# semantic components for mechanics waves and circuits -# it defines BodySystem ForceVector WaveField and CircuitFlow built on SemanticMobject - """Semantic components for mechanics, waves, and circuits.""" from __future__ import annotations diff --git a/skills/manim-video/assets/domains/systems.py b/skills/manim-video/assets/domains/systems.py index aa6404ec..8f1327dc 100644 --- a/skills/manim-video/assets/domains/systems.py +++ b/skills/manim-video/assets/domains/systems.py @@ -1,6 +1,3 @@ -# semantic components for software systems and data architecture -# it defines RequestFlow ServiceGraph QueueModel and DataPipeline built on SemanticMobject - """Semantic components for software systems and data architecture.""" from __future__ import annotations diff --git a/skills/manim-video/assets/teaching.py b/skills/manim-video/assets/teaching.py index 38005fc8..0c106c23 100644 --- a/skills/manim-video/assets/teaching.py +++ b/skills/manim-video/assets/teaching.py @@ -1,7 +1,3 @@ -# semantic building blocks for teaching scenes built on manim -# it defines themes named parts anchors linked values and the base scene classes chapters extend -# everything here is meant to be reused across the domain kits so scenes stay consistent - """Style-neutral semantic authoring primitives for Manim Community explainers. The classes in this module manage meaning, continuity, and attention. They do diff --git a/skills/manim-video/scripts/preview_scene.py b/skills/manim-video/scripts/preview_scene.py index 5b49e109..4bf50ecc 100755 --- a/skills/manim-video/scripts/preview_scene.py +++ b/skills/manim-video/scripts/preview_scene.py @@ -1,8 +1,4 @@ #!/usr/bin/env python3 -# command line tool that renders chosen manim scene classes at low quality into a project edit verify folder -# it probes the output with ffprobe and extracts first last and contact sheet frames with ffmpeg -# every step raises PreviewError with a clear message so failures are easy to act on - """Render selected Manim chapters and build small authoring-review artifacts.""" from __future__ import annotations diff --git a/tests/fixtures/manim_domain_gallery.py b/tests/fixtures/manim_domain_gallery.py index 16eccead..b96e5376 100644 --- a/tests/fixtures/manim_domain_gallery.py +++ b/tests/fixtures/manim_domain_gallery.py @@ -1,5 +1,6 @@ -# gallery of tiny manim scenes used by the rendering test -# each scene shows one domain component or exercises the teaching scene api +"""gallery of tiny manim scenes used by the rendering test +each scene shows one domain component or exercises the teaching scene api +""" from __future__ import annotations diff --git a/tests/test_manim_domains.py b/tests/test_manim_domains.py index 6b2b6a88..153b94f6 100644 --- a/tests/test_manim_domains.py +++ b/tests/test_manim_domains.py @@ -1,5 +1,6 @@ -# tests that every domain component is semantic frame safe and theme aware -# it also checks a handful of component actions update their state +"""tests that every domain component is semantic frame safe and theme aware +it also checks a handful of component actions update their state +""" from __future__ import annotations diff --git a/tests/test_manim_gallery.py b/tests/test_manim_gallery.py index 6cce7080..b92a7282 100644 --- a/tests/test_manim_gallery.py +++ b/tests/test_manim_gallery.py @@ -1,5 +1,6 @@ -# renders the gallery fixture scenes at low quality through the manim cli -# it skips when manim is missing and checks each scene produced a video +"""renders the gallery fixture scenes at low quality through the manim cli +it skips when manim is missing and checks each scene produced a video +""" from __future__ import annotations diff --git a/tests/test_manim_teaching.py b/tests/test_manim_teaching.py index 612b2189..649592a0 100644 --- a/tests/test_manim_teaching.py +++ b/tests/test_manim_teaching.py @@ -1,5 +1,6 @@ -# tests for the semantic teaching primitives in teaching py -# it covers part and anchor registration focus and restore transforms linked values and the three d scene +"""tests for the semantic teaching primitives in teaching py +it covers part and anchor registration focus and restore transforms linked values and the three d scene +""" from __future__ import annotations diff --git a/tests/test_preview_scene.py b/tests/test_preview_scene.py index 3ef28da2..f2af9a05 100644 --- a/tests/test_preview_scene.py +++ b/tests/test_preview_scene.py @@ -1,5 +1,6 @@ -# tests for the preview_scene script -# it loads the script from its path and checks error handling and one real low quality render +"""tests for the preview_scene script +it loads the script from its path and checks error handling and one real low quality render +""" from __future__ import annotations From 0ab716bf3550696ccfcd3592b522ae981ea5a2fa Mon Sep 17 00:00:00 2001 From: Ismael Date: Wed, 2 Sep 2026 14:23:32 -0700 Subject: [PATCH 14/19] mention original explainers and the manim extra in the readme and install guide --- README.md | 4 ++++ install.md | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0f7b2649..4d086b4a 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ Try video-use in [Browser Use Cloud](https://cloud.browser-use.com/v4?utm_campai - **Burns subtitles** in your style — 2-word UPPERCASE chunks by default, fully customizable - **Generates animation overlays** via [HyperFrames](https://github.com/heygen-com/hyperframes), [Remotion](https://www.remotion.dev/), [Manim](https://www.manim.community/), or PIL — spawned in parallel sub-agents, one per animation - **Self-evaluates the rendered output** at every cut boundary before showing you anything +- **Builds original narrated explainers from a topic** — semantic Manim teaching assets, chapter previews, generated narration with word-timed captions - **Persists session memory** in `project.md` so next week's session picks up where you left off ## Setup prompt @@ -66,6 +67,9 @@ brew install yt-dlp # optional, for downloading online sources # 3. Add your ElevenLabs API key cp .env.example .env $EDITOR .env # ELEVENLABS_API_KEY=... + +# 4. Optional: Manim for original explainers built from a topic +uv sync --extra animations ``` ## How it works diff --git a/install.md b/install.md index b3b4d7ff..8445d8e4 100644 --- a/install.md +++ b/install.md @@ -46,7 +46,7 @@ If the repo is already there, `git pull --ff-only` and continue. command -v uv >/dev/null && uv sync || pip install -e . ``` -`pyproject.toml` lists `requests`, `librosa`, `matplotlib`, `pillow`, `numpy`. No console scripts — helpers are invoked directly as `python helpers/.py`. +`pyproject.toml` lists `requests`, `librosa`, `matplotlib`, `pillow`, `numpy`. No console scripts — helpers are invoked directly as `python helpers/.py`. Manim is an optional extra for original explainers: `uv sync --extra animations` installs it now, or leave it for the first Manim slot. ### 3. Install ffmpeg (+ optional yt-dlp) @@ -132,6 +132,7 @@ Run one real thing. Prefer the lightest verification that still proves the pipel ```bash python ~/Developer/video-use/helpers/timeline_view.py --help >/dev/null && echo "helpers OK" ffprobe -version | head -1 +cd ~/Developer/video-use && python -m pytest -q # proves every helper imports; Manim tests skip when it is not installed ``` Full transcription test is optional at install time — it burns Scribe credits. Better to wait until the user hands you their first clip. From 49197831aae691e827e98e12282fb289ded26186 Mon Sep 17 00:00:00 2001 From: Ismael Date: Wed, 2 Sep 2026 16:25:14 -0700 Subject: [PATCH 15/19] export easing names from the concept explainer asset keep swapped cells in order and clear the footer margin --- skills/manim-video/assets/concept_explainer.py | 6 +++++- skills/manim-video/assets/domains/computing.py | 9 +++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/skills/manim-video/assets/concept_explainer.py b/skills/manim-video/assets/concept_explainer.py index ae6811b8..75f887c1 100644 --- a/skills/manim-video/assets/concept_explainer.py +++ b/skills/manim-video/assets/concept_explainer.py @@ -10,6 +10,9 @@ from manim import DOWN, UP, Mobject, Rectangle, RoundedRectangle, Text, VGroup, config +# re exported so chapter scripts import portable easing names from this asset +from manim.utils.rate_functions import ease_in_out_cubic, ease_out_cubic # noqa: F401 + CAPTION_RAIL_FRACTION = 0.16 @@ -235,5 +238,6 @@ def source_footer( color=color, ) rail_height = config.frame_height * CAPTION_RAIL_FRACTION - footer.to_edge(DOWN, buff=rail_height + 0.12) + # the buffer must clear the margin that assert_inside_frame enforces + footer.to_edge(DOWN, buff=rail_height + 0.2) return assert_inside_frame(footer) diff --git a/skills/manim-video/assets/domains/computing.py b/skills/manim-video/assets/domains/computing.py index c61b47bc..c92543ba 100644 --- a/skills/manim-video/assets/domains/computing.py +++ b/skills/manim-video/assets/domains/computing.py @@ -85,9 +85,14 @@ def swap(self, first: int, second: int) -> AnimationGroup: left_position = cells[left].get_center().copy() right_position = cells[right].get_center().copy() self.values[left], self.values[right] = self.values[right], self.values[left] + left_cell, right_cell = cells[left], cells[right] + # reorder the group and the anchors so later index lookups match the new visual order + cells.submobjects[left], cells.submobjects[right] = right_cell, left_cell + self._semantic_anchors[f"index_{left}"] = right_cell + self._semantic_anchors[f"index_{right}"] = left_cell return AnimationGroup( - cells[left].animate.move_to(right_position), - cells[right].animate.move_to(left_position), + left_cell.animate.move_to(right_position), + right_cell.animate.move_to(left_position), lag_ratio=0, ) From 8502d15a4b53a9c91d8391af40cdbf894c191434 Mon Sep 17 00:00:00 2001 From: Ismael Date: Thu, 17 Sep 2026 16:01:11 -0700 Subject: [PATCH 16/19] isolate chapter preview attempts and expose render quality controls --- skills/manim-video/scripts/preview_scene.py | 35 ++++++-- tests/test_preview_scene.py | 99 +++++++++++++++++++++ 2 files changed, 128 insertions(+), 6 deletions(-) diff --git a/skills/manim-video/scripts/preview_scene.py b/skills/manim-video/scripts/preview_scene.py index 4bf50ecc..0584514c 100755 --- a/skills/manim-video/scripts/preview_scene.py +++ b/skills/manim-video/scripts/preview_scene.py @@ -5,7 +5,9 @@ import argparse import ast +import hashlib import json +import math import os import shutil import subprocess @@ -61,7 +63,7 @@ def require_executable(value: str, *, purpose: str) -> str: path = shutil.which(value) if path is None: raise PreviewError(f"{purpose} is unavailable: executable {value!r} was not found") - return path + return str(Path(path).absolute()) # run a command through the injected runner and turn failures into PreviewError @@ -211,6 +213,10 @@ def build_review_frames( temp_dir = Path(temp_name) frame_paths: list[Path] = [] for index, proportion in enumerate((0.0, 0.25, 0.5, 0.75, 1.0)): + # endpoints already exist at full resolution; do not decode them twice + if index in (0, 4): + frame_paths.append(initial if index == 0 else final) + continue frame = temp_dir / f"frame_{index}.png" _extract_frame( video, @@ -283,7 +289,15 @@ def render_scene( ffprobe_bin: str = "ffprobe", runner: Runner = subprocess.run, timeout_s: float = 900.0, + fps: float | None = None, + quality: str = "low", ) -> dict[str, Any]: + if quality not in ("low", "medium", "high"): + raise PreviewError("quality must be low, medium or high") + if fps is not None and (not math.isfinite(fps) or fps <= 0): + raise PreviewError("fps must be positive and finite") + if not math.isfinite(timeout_s) or timeout_s <= 0: + raise PreviewError("timeout must be positive and finite") script = script.resolve() if not script.is_file(): raise PreviewError(f"Manim script does not exist: {script}") @@ -291,20 +305,24 @@ def render_scene( raise PreviewError(f"scene class {scene_name!r} was not found in {script.name}") resolved_edit = resolve_edit_dir(script, edit_dir) verify_root = (resolved_edit / "verify").resolve() - destination = verify_root / "manim_previews" / script.stem / scene_name - destination.mkdir(parents=True, exist_ok=True) + source_id = hashlib.sha256(str(script).encode("utf-8")).hexdigest()[:16] + scene_root = verify_root / "manim_previews" / f"{script.stem}-{source_id}" / scene_name # refuse to write anywhere outside the verify folder - if not destination.resolve().is_relative_to(verify_root): + if not scene_root.resolve().is_relative_to(verify_root): raise PreviewError("refusing to write preview artifacts outside edit/verify") manim = require_executable(manim_bin, purpose="Manim") ffmpeg = require_executable(ffmpeg_bin, purpose="ffmpeg") ffprobe = require_executable(ffprobe_bin, purpose="ffprobe") + scene_root.mkdir(parents=True, exist_ok=True) + # isolate every attempt so older videos cannot validate a render that wrote nothing + destination = Path(tempfile.mkdtemp(prefix="attempt-", dir=scene_root)) media_dir = destination / "media" command = [ manim, "render", - "-ql", + {"low": "-ql", "medium": "-qm", "high": "-qh"}[quality], + *(["--fps", f"{fps:g}"] if fps is not None else []), "--save_sections", "--media_dir", str(media_dir), @@ -343,6 +361,7 @@ def render_scene( ) report = { "scene": scene_name, + "script": str(script), "status": "ok", "video": str(scene_video), **frames, @@ -379,6 +398,8 @@ def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace: parser.add_argument("--edit-dir", type=Path, help="Explicit project edit directory") parser.add_argument("--manim-bin", default="manim", help="Manim executable name or path") parser.add_argument("--timeout-s", type=float, default=900.0, help="Per-command timeout") + parser.add_argument("--fps", type=float, help="Explicit frame clock; use the delivery FPS when checking synchronization") + parser.add_argument("--quality", choices=("low", "medium", "high"), default="low", help="Low for motion drafts; high for delivery-size layout inspection") return parser.parse_args(argv) @@ -392,8 +413,10 @@ def main(argv: Sequence[str] | None = None) -> int: edit_dir=args.edit_dir, manim_bin=args.manim_bin, timeout_s=args.timeout_s, + fps=args.fps, + quality=args.quality, ) - except PreviewError as exc: + except (PreviewError, OSError) as exc: print(f"preview failed: {exc}", file=sys.stderr) return 1 print(json.dumps(report, indent=2, sort_keys=True)) diff --git a/tests/test_preview_scene.py b/tests/test_preview_scene.py index f2af9a05..2dfb2030 100644 --- a/tests/test_preview_scene.py +++ b/tests/test_preview_scene.py @@ -10,6 +10,7 @@ from pathlib import Path import pytest +from PIL import Image SCRIPT_PATH = ( @@ -26,6 +27,47 @@ SPEC.loader.exec_module(preview_scene) +# endpoints are decoded once and reused for the sheet rather than extracted again +def test_review_decodes_five_not_seven_frames(tmp_path, monkeypatch): + calls = [] + + # synthesize stills while recording requested decode timestamps + def extract(video, output, timestamp_s, **kwargs): + calls.append(timestamp_s) + Image.new('RGB', (640, 360)).save(output) + + monkeypatch.setattr(preview_scene, '_extract_frame', extract) + result = preview_scene.build_review_frames(tmp_path / 'v.mp4', tmp_path, + {'duration_s': 2, 'frame_rate': 30}, ffmpeg_bin='unused', runner=None, timeout_s=10) + assert len(calls) == 5 and len(set(calls)) == 5 + assert Image.open(result['initial_frame']).size == (640, 360) + + +# options reject invalid clocks before invoking any executable +@pytest.mark.parametrize('kwargs', [{'fps': 0}, {'fps': float('nan')}, + {'fps': float('inf')}, {'quality': 'ultra'}, {'timeout_s': -1}]) +def test_invalid_render_options(tmp_path, kwargs): + with pytest.raises(preview_scene.PreviewError): + preview_scene.render_scene(tmp_path / 'absent.py', 'Demo', **kwargs) + + +# explicit delivery clocks and quality are passed to Manim without changing defaults +def test_explicit_clock_and_quality(tmp_path): + script = _scene_script(tmp_path / 'edit' / 'scene.py') + commands = [] + + # stop after capturing the exact command passed to Manim + def runner(command, **kwargs): + commands.append(command) + return subprocess.CompletedProcess(command, 1, '', 'intentional stop') + + with pytest.raises(preview_scene.PreviewError, match='intentional stop'): + preview_scene.render_scene(script, 'Demo', fps=30, quality='high', + runner=runner, manim_bin='/usr/bin/true', ffmpeg_bin='/usr/bin/true', ffprobe_bin='/usr/bin/true') + assert '-qh' in commands[0] + assert commands[0][commands[0].index('--fps') + 1] == '30' + + # write a tiny manim script with one scene class def _scene_script(path: Path, name: str = "Demo") -> Path: path.parent.mkdir(parents=True, exist_ok=True) @@ -96,6 +138,63 @@ def successful_runner(*_args, **_kwargs): ) +# an older successful preview cannot satisfy a later render that writes no media +def test_preview_rejects_old_video_and_preserves_previous_report(tmp_path, monkeypatch): + script = _scene_script(tmp_path / "edit" / "scene.py") + attempts = [] + + # create output on the first invocation and leave the second invocation empty + def runner(command, **kwargs): + media = Path(command[command.index("--media_dir") + 1]) + attempts.append(media) + if len(attempts) == 1: + media.mkdir(parents=True) + (media / "Demo.mp4").write_bytes(b"first successful render") + return subprocess.CompletedProcess(command, 0, "", "") + + monkeypatch.setattr(preview_scene, "probe_video", lambda *args, **kwargs: + {"duration_s": 1, "width": 640, "height": 360, "frame_rate": 30}) + monkeypatch.setattr(preview_scene, "build_review_frames", lambda *args, **kwargs: {}) + options = dict(runner=runner, manim_bin="/usr/bin/true", + ffmpeg_bin="/usr/bin/true", ffprobe_bin="/usr/bin/true") + report = preview_scene.render_scene(script, "Demo", **options) + old_report = Path(report["report"]).read_bytes() + with pytest.raises(preview_scene.PreviewError, match="produced no video"): + preview_scene.render_scene(script, "Demo", **options) + assert attempts[0] != attempts[1] + assert Path(report["video"]).read_bytes() == b"first successful render" + assert Path(report["report"]).read_bytes() == old_report + assert not (attempts[1].parent / "report.json").exists() + + +# scripts with matching names retain separate source identities under one project +def test_preview_separates_equal_source_names(tmp_path): + scripts = [_scene_script(tmp_path / "edit" / folder / "scene.py") + for folder in ("first", "second")] + attempts = [] + + # inspect the attempted destination before any media work occurs + def runner(command, **kwargs): + attempts.append(Path(command[command.index("--media_dir") + 1])) + return subprocess.CompletedProcess(command, 0, "", "") + + for script in scripts: + with pytest.raises(preview_scene.PreviewError, match="produced no video"): + preview_scene.render_scene(script, "Demo", runner=runner, + manim_bin="/usr/bin/true", ffmpeg_bin="/usr/bin/true", ffprobe_bin="/usr/bin/true") + assert attempts[0].parents[2] != attempts[1].parents[2] + + +# a relative executable stays valid after the renderer changes its working directory +def test_preview_resolves_relative_executables(tmp_path, monkeypatch): + executable = tmp_path / "bin" / "renderer" + executable.parent.mkdir() + executable.write_text("#!/bin/sh\nexit 0\n") + executable.chmod(0o755) + monkeypatch.chdir(tmp_path) + assert preview_scene.require_executable("bin/renderer", purpose="renderer") == str(executable) + + # a real render lands under edit verify with the expected metadata and artifacts def test_preview_real_manim_render_stays_under_edit_verify(tmp_path: Path) -> None: pytest.importorskip("manim") From 010fb8c2483a6813f4d2938d031731c3982482db Mon Sep 17 00:00:00 2001 From: Ismael Date: Thu, 17 Sep 2026 16:01:11 -0700 Subject: [PATCH 17/19] export measured layout bounds and document preview review limits --- .../manim-video/assets/concept_explainer.py | 45 +++++++++++++++++ .../references/concept-explainer.md | 12 +++++ skills/manim-video/references/teaching-api.md | 11 +++++ tests/test_manim_concept_asset.py | 49 +++++++++++++++++++ 4 files changed, 117 insertions(+) create mode 100644 tests/test_manim_concept_asset.py diff --git a/skills/manim-video/assets/concept_explainer.py b/skills/manim-video/assets/concept_explainer.py index 75f887c1..d3b95d98 100644 --- a/skills/manim-video/assets/concept_explainer.py +++ b/skills/manim-video/assets/concept_explainer.py @@ -6,6 +6,9 @@ from __future__ import annotations +import math +from typing import Any + from itertools import combinations from manim import DOWN, UP, Mobject, Rectangle, RoundedRectangle, Text, VGroup, config @@ -241,3 +244,45 @@ def source_footer( # the buffer must clear the margin that assert_inside_frame enforces footer.to_edge(DOWN, buff=rail_height + 0.2) return assert_inside_frame(footer) + + +# measure named foreground bounds in delivery pixels without hiding clipping +def measured_layout_frame( + time: float, + objects: dict[str, Mobject], + *, + width: int = 1920, + height: int = 1080, + camera_frame: Mobject | None = None, +) -> dict[str, Any]: + """Measure a 2D teaching state for layout_qc without clipping its bounds. + + Supply the current unrotated moving-camera frame when it pans or zooms. + Perspective/rotated cameras need screen-space measurements instead. Choose + independent foreground groups; inspect internal labels separately. This + builds evidence, not a validation result or automatic overlap exemption. + """ + if not math.isfinite(time) or time < 0 or width <= 0 or height <= 0: + raise ValueError('layout time and canvas dimensions must be finite and valid') + if not math.isfinite(width) or not math.isfinite(height) or not objects: + raise ValueError('provide finite canvas dimensions and named objects') + frame_width = float(camera_frame.width) if camera_frame is not None else float(config.frame_width) + frame_height = float(camera_frame.height) if camera_frame is not None else float(config.frame_height) + left = float(camera_frame.get_left()[0]) if camera_frame is not None else -frame_width / 2 + top = float(camera_frame.get_top()[1]) if camera_frame is not None else frame_height / 2 + if not all(math.isfinite(v) for v in (frame_width, frame_height, left, top)) or min(frame_width, frame_height) <= 0: + raise ValueError('camera frame must have finite positive dimensions') + elements = [] + for name, obj in objects.items(): + if not isinstance(name, str) or not name.strip() or name != name.strip(): + raise ValueError('layout objects require nonempty unpadded names') + rect = { + 'x': (float(obj.get_left()[0]) - left) / frame_width * width, + 'y': (top - float(obj.get_top()[1])) / frame_height * height, + 'width': float(obj.width) / frame_width * width, + 'height': float(obj.height) / frame_height * height, + } + if not all(math.isfinite(v) for v in rect.values()): + raise ValueError(f'nonfinite measured bounds for {name}') + elements.append({'id': name, 'rect': rect}) + return {'time': float(time), 'elements': elements} diff --git a/skills/manim-video/references/concept-explainer.md b/skills/manim-video/references/concept-explainer.md index 080ac4aa..eea9b9e5 100644 --- a/skills/manim-video/references/concept-explainer.md +++ b/skills/manim-video/references/concept-explainer.md @@ -201,3 +201,15 @@ Inspect only a few candidate moments from each source. If a source cannot be cleared or does not teach the beat better than illustration, omit it rather than lowering relevance. Keep provenance in a restrained source footer instead of adding decorative title hierarchy. + +## Preview clocks and fresh attempts + +The preview_scene.py script accepts --fps and --quality low, medium or high. +Low remains the default for fast drafts. Set delivery FPS explicitly when +checking synchronization, and use high for a larger layout review. The preview +path contains the source path hash and a fresh attempt directory, preventing an +older video from validating a failed or empty new render. Successful attempts +retain full-resolution endpoint frames and a five-position contact sheet; +endpoints are decoded once. Failed attempts remain available for diagnosis. +Source hashes identify paths, not file contents, and this is not a render cache. +Inspect the resulting motion and actual encoded frame rate before delivery. diff --git a/skills/manim-video/references/teaching-api.md b/skills/manim-video/references/teaching-api.md index 891adf5a..6bd6416b 100644 --- a/skills/manim-video/references/teaching-api.md +++ b/skills/manim-video/references/teaching-api.md @@ -47,3 +47,14 @@ author controls timing and composition with `self.play()`. - Restore focus and highlights before starting another attention context. The helpers preserve the previous opacity and style exactly. - Use `begin_beat()` for inspectable `next_section()` metadata inside a chapter. + +## Measured layout evidence + +Use `measured_layout_frame(time, objects, width=1920, height=1080)` from +`assets/concept_explainer.py` to export named foreground rectangles in delivery +pixels. Clipped bounds remain outside the canvas rather than being clamped away. +For an unrotated moving camera, pass its frame as camera_frame. Rotated and +perspective cameras require separate screen-space measurements. This exports +evidence only; it does not validate overlap or visibility, inspect internal labels, +or automatically approve layout. The optional layout_qc helper proposed in #148 +can consume these records; it is not required to export measurements. diff --git a/tests/test_manim_concept_asset.py b/tests/test_manim_concept_asset.py new file mode 100644 index 00000000..13a27ea1 --- /dev/null +++ b/tests/test_manim_concept_asset.py @@ -0,0 +1,49 @@ +"""test manim concept asset support for video use""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path + +import pytest + + +manim = pytest.importorskip("manim") + + +ASSET_PATH = ( + Path(__file__).parents[1] + / "skills" + / "manim-video" + / "assets" + / "concept_explainer.py" +) +SPEC = importlib.util.spec_from_file_location("concept_explainer_asset", ASSET_PATH) +assert SPEC is not None and SPEC.loader is not None +concept_explainer = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(concept_explainer) + + +# measured evidence retains out of frame geometry for the canonical QC validator +def test_measured_layout_retains_clipping(): + object = manim.Rectangle(width=2, height=1).shift(manim.RIGHT * float(manim.config.frame_width) / 2) + frame = concept_explainer.measured_layout_frame(1, {'object': object}) + assert frame['elements'][0]['rect']['x'] + frame['elements'][0]['rect']['width'] > 1920 + + +# camera pan and zoom affect measured screen coordinates while overlaps stay visible +def test_measured_layout_camera_and_overlap(): + camera = manim.Rectangle(width=8, height=4).shift(manim.RIGHT * 3) + object = manim.Rectangle(width=2, height=1).move_to(camera) + frame = concept_explainer.measured_layout_frame(0, {'first': object, 'second': object.copy()}, + width=800, height=400, camera_frame=camera) + assert frame['elements'][0]['rect'] == pytest.approx({'x': 300, 'y': 150, 'width': 200, 'height': 100}) + assert frame['elements'][0]['rect'] == frame['elements'][1]['rect'] + + +# malformed measurement inputs fail before exporting misleading evidence +@pytest.mark.parametrize('time,objects,kwargs', [(-1, {}, {}), (float('nan'), {}, {}), + (0, {}, {}), (0, {'': None}, {}), (0, {'name': None}, {'width': float('inf')})]) +def test_measured_layout_invalid(time, objects, kwargs): + with pytest.raises(ValueError): + concept_explainer.measured_layout_frame(time, objects, **kwargs) From 563c5a984afdd8f7a242f009ee1686258bd8daee Mon Sep 17 00:00:00 2001 From: Ismael Date: Sun, 20 Sep 2026 13:19:45 -0700 Subject: [PATCH 18/19] validate semantic values and restore live three dimensional cameras --- .../manim-video/assets/concept_explainer.py | 12 ++++++---- skills/manim-video/assets/domains/_common.py | 6 +++++ skills/manim-video/assets/domains/math.py | 2 ++ skills/manim-video/assets/teaching.py | 23 ++++++++++--------- tests/test_manim_domains.py | 15 ++++++++++++ tests/test_manim_teaching.py | 13 +++++++++++ 6 files changed, 55 insertions(+), 16 deletions(-) diff --git a/skills/manim-video/assets/concept_explainer.py b/skills/manim-video/assets/concept_explainer.py index 75f887c1..20f161a4 100644 --- a/skills/manim-video/assets/concept_explainer.py +++ b/skills/manim-video/assets/concept_explainer.py @@ -150,6 +150,8 @@ def assert_inside_frame( margin: float = 0.18, ) -> Mobject: """Raise before render if a mobject leaves the frame or caption-safe area.""" + if not np.isfinite(margin) or margin < 0 or not np.isfinite(caption_rail_fraction) or not 0 <= caption_rail_fraction < 1: + raise ValueError("frame margin and caption rail must be finite nonnegative bounds") # compute the allowed bounds with the caption rail folded into the bottom edge left = -config.frame_width / 2 + margin right = config.frame_width / 2 - margin @@ -187,13 +189,13 @@ def assert_inside_region( # describe a final state mobject as a normalized rectangle the edl can protect def normalized_bounds(mobject: Mobject, *, padding: float = 0.0) -> dict[str, float]: """Return an EDL-ready protected rectangle for a final-state mobject.""" - if padding < 0: + if not np.isfinite(padding) or padding < 0: raise ValueError("padding must be non-negative") # clamp the padded bounds to the frame then convert to top left normalized units - left = max(-config.frame_width / 2, mobject.get_left()[0] - padding) - right = min(config.frame_width / 2, mobject.get_right()[0] + padding) - top = min(config.frame_height / 2, mobject.get_top()[1] + padding) - bottom = max(-config.frame_height / 2, mobject.get_bottom()[1] - padding) + left = np.clip(mobject.get_left()[0] - padding, -config.frame_width / 2, config.frame_width / 2) + right = np.clip(mobject.get_right()[0] + padding, -config.frame_width / 2, config.frame_width / 2) + top = np.clip(mobject.get_top()[1] + padding, -config.frame_height / 2, config.frame_height / 2) + bottom = np.clip(mobject.get_bottom()[1] - padding, -config.frame_height / 2, config.frame_height / 2) return { "x": (left + config.frame_width / 2) / config.frame_width, "y": (config.frame_height / 2 - top) / config.frame_height, diff --git a/skills/manim-video/assets/domains/_common.py b/skills/manim-video/assets/domains/_common.py index fa90a35a..cf0fcb21 100644 --- a/skills/manim-video/assets/domains/_common.py +++ b/skills/manim-video/assets/domains/_common.py @@ -178,6 +178,8 @@ def frame_safe(component: Mobject) -> Mobject: # validate that an index falls inside zero to size minus one def ensure_index(index: int, size: int, *, name: str = "index") -> int: + if isinstance(index, bool) or not isinstance(index, (int, np.integer)): + raise ValueError(f"{name} must be an integer") if not 0 <= int(index) < size: raise IndexError(f"{name} {index} is outside 0..{size - 1}") return int(index) @@ -186,6 +188,10 @@ def ensure_index(index: int, size: int, *, name: str = "index") -> int: # validate a non negative amount and optionally that it does not exceed what is available def ensure_amount(amount: float, *, available: float | None = None) -> float: value = float(amount) + if not np.isfinite(value): + raise ValueError("amount must be finite") + if available is not None and not np.isfinite(available): + raise ValueError("available amount must be finite") if value < 0: raise ValueError("amount cannot be negative") if available is not None and value > available + 1e-9: diff --git a/skills/manim-video/assets/domains/math.py b/skills/manim-video/assets/domains/math.py index db040fb8..3a0aba5e 100644 --- a/skills/manim-video/assets/domains/math.py +++ b/skills/manim-video/assets/domains/math.py @@ -233,6 +233,8 @@ def __init__( ) graph = axes.plot(function, x_range=self.x_range, color=theme.primary) self.x_value = float(x_value) + if not np.isfinite(self.x_value) or not x_range[0] <= self.x_value <= x_range[1]: + raise ValueError("initial x value must be finite and inside the axis range") point = Dot(axes.c2p(self.x_value, function(self.x_value)), color=theme.accent) guide = DashedLine( axes.c2p(self.x_value, 0), diff --git a/skills/manim-video/assets/teaching.py b/skills/manim-video/assets/teaching.py index 0c106c23..b63f9acc 100644 --- a/skills/manim-video/assets/teaching.py +++ b/skills/manim-video/assets/teaching.py @@ -316,7 +316,10 @@ def register( if isinstance(name, Mobject): dependent = name callback = mobject - binding_name = f"dependent_{len(self._bindings)}" + index = 0 + while f"dependent_{index}" in self._bindings: + index += 1 + binding_name = f"dependent_{index}" else: binding_name = name dependent = mobject @@ -664,11 +667,11 @@ class TeachingThreeDScene(_TeachingSceneMixin, ThreeDScene): # capture every camera setting that focus may change def _camera_values(self) -> dict[str, Any]: return { - "phi": float(self.camera.phi), - "theta": float(self.camera.theta), - "gamma": float(self.camera.gamma), - "zoom": float(self.camera.zoom), - "focal_distance": float(self.camera.focal_distance), + "phi": float(self.camera.get_phi()), + "theta": float(self.camera.get_theta()), + "gamma": float(self.camera.get_gamma()), + "zoom": float(self.camera.get_zoom()), + "focal_distance": float(self.camera.get_focal_distance()), "frame_center": np.asarray(self.camera.frame_center, dtype=float).copy(), } @@ -690,7 +693,7 @@ def _play_focus( ) self.move_camera( frame_center=group.get_center(), - zoom=min(float(self.camera.zoom) * scale, 8.0), + zoom=min(float(self.camera.get_zoom()) * scale, 8.0), added_anims=dim_animations, run_time=run_time, ) @@ -704,8 +707,7 @@ def _set_focus_camera_immediately(self, targets: list[Mobject], *, margin: float float(config.frame_width) / max(group.width + 2 * margin, 1e-6), float(config.frame_height) / max(group.height + 2 * margin, 1e-6), ) - self.camera.frame_center = group.get_center() - self.camera.zoom = min(float(self.camera.zoom) * scale, 8.0) + self.set_camera_orientation(frame_center=group.get_center(), zoom=min(float(self.camera.get_zoom()) * scale, 8.0)) # restore the saved three d camera settings by animation or direct assignment def _restore_focus_camera(self, *, run_time: float, animate: bool) -> None: @@ -714,8 +716,7 @@ def _restore_focus_camera(self, *, run_time: float, animate: bool) -> None: if animate: self.move_camera(**self._camera_context, run_time=run_time) else: - for name, value in self._camera_context.items(): - setattr(self.camera, name, value) + self.set_camera_orientation(**self._camera_context) self._camera_context = None diff --git a/tests/test_manim_domains.py b/tests/test_manim_domains.py index 153b94f6..8cde6f3e 100644 --- a/tests/test_manim_domains.py +++ b/tests/test_manim_domains.py @@ -71,3 +71,18 @@ def test_domain_actions_update_semantic_state() -> None: timeline.compound(2) assert timeline.balance_at(2) == pytest.approx(121) + + +# invalid transfers cannot poison balances with nonfinite values +@pytest.mark.parametrize('amount', [float('nan'), float('inf'), -float('inf')]) +def test_review_nonfinite_transfer(amount): + from domains._common import ensure_amount + with pytest.raises(ValueError): + ensure_amount(amount, available=10) + + +# fractional indices cannot silently select a different element +def test_review_fractional_index(): + from domains._common import ensure_index + with pytest.raises(ValueError): + ensure_index(1.5, 3) diff --git a/tests/test_manim_teaching.py b/tests/test_manim_teaching.py index 649592a0..94354784 100644 --- a/tests/test_manim_teaching.py +++ b/tests/test_manim_teaching.py @@ -159,3 +159,16 @@ def test_three_d_scene_supports_registry_and_immediate_focus(theme: VisualTheme) assert scene.recall("focus") is focus np.testing.assert_allclose(scene.camera.frame_center, original_center) + + +# immediate focus uses and restores live camera trackers after prior camera movement +def test_review_three_d_camera_trackers(theme): + scene = TeachingThreeDScene() + scene.set_camera_orientation(phi=0.8, theta=-0.7, gamma=0.2, zoom=1.3) + scene.remember('focus', manim.Sphere(radius=0.25)) + scene.focus_on('focus', animate=False) + assert scene.camera.get_zoom() > 1.3 + scene.restore_context(animate=False) + assert scene.camera.get_zoom() == pytest.approx(1.3) + assert scene.camera.get_phi() == pytest.approx(0.8) + assert scene.camera.get_theta() == pytest.approx(-0.7) From 4e9a8aa6ad944e111bab09178911df24b18279cc Mon Sep 17 00:00:00 2001 From: Ismael Date: Sun, 20 Sep 2026 13:20:35 -0700 Subject: [PATCH 19/19] render chapter previews from the project configuration directory --- skills/manim-video/references/teaching-api.md | 2 +- skills/manim-video/scripts/preview_scene.py | 2 +- tests/test_preview_scene.py | 13 +++++++++++++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/skills/manim-video/references/teaching-api.md b/skills/manim-video/references/teaching-api.md index 6bd6416b..56272bfd 100644 --- a/skills/manim-video/references/teaching-api.md +++ b/skills/manim-video/references/teaching-api.md @@ -1,6 +1,6 @@ # Semantic Teaching API -Copy `assets/teaching.py` and the needed `assets/domains/` modules into an +Copy `assets/teaching.py`, `assets/concept_explainer.py`, and the needed `assets/domains/` modules into an original explainer workspace. Pass one shared `VisualTheme` to every component. These APIs manage semantic identity, linked state, attention, and continuity; ordinary Manim remains available for custom visuals. diff --git a/skills/manim-video/scripts/preview_scene.py b/skills/manim-video/scripts/preview_scene.py index 0584514c..63d31283 100755 --- a/skills/manim-video/scripts/preview_scene.py +++ b/skills/manim-video/scripts/preview_scene.py @@ -336,7 +336,7 @@ def render_scene( _run( command, runner=runner, - cwd=script.parent, + cwd=resolved_edit.parent, timeout_s=timeout_s, purpose=f"Manim render for {scene_name}", ) diff --git a/tests/test_preview_scene.py b/tests/test_preview_scene.py index 2dfb2030..53decbfe 100644 --- a/tests/test_preview_scene.py +++ b/tests/test_preview_scene.py @@ -220,3 +220,16 @@ def test_preview_real_manim_render_stays_under_edit_verify(tmp_path: Path) -> No artifact = Path(report[key]).resolve() assert artifact.is_file() assert artifact.is_relative_to((edit_dir / "verify").resolve()) + + +# preview renders load the same project configuration and relative assets as production +def test_review_project_working_directory(tmp_path): + script = _scene_script(tmp_path / 'project' / 'edit' / 'animations' / 'scene.py') + calls = [] + # stop after observing the requested working directory + def runner(command, **kwargs): + calls.append(kwargs['cwd']) + return subprocess.CompletedProcess(command, 1, '', 'intentional stop') + with pytest.raises(preview_scene.PreviewError, match='intentional stop'): + preview_scene.render_scene(script, 'Demo', runner=runner, manim_bin='/usr/bin/true', ffmpeg_bin='/usr/bin/true', ffprobe_bin='/usr/bin/true') + assert calls == [str(tmp_path / 'project')]